Skip to content

hsingjui/pi-hooks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pi-hooks

English | 简体中文

Claude Code-compatible command hooks for the Pi coding agent.

This package adapts Claude Code's hook configuration format to Pi's extension event system so existing command hook workflows can be reused with minimal changes.

Quick Setup

  1. Install the package:
pi install npm:@hsingjui/pi-hooks
  1. Add hooks to .pi/settings.json (or ~/.pi/agent/settings.json):
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo 'agent finished'"
          }
        ]
      }
    ]
  }
}
  1. Run /reload, then send a message to test it.

Current Support

  • Only type: "command" is supported
  • Supports the if field on individual hook handlers for tool events only
  • Supported events:
    • SessionStart
    • SessionEnd
    • PreCompact
    • PostCompact
    • PreToolUse
    • PostToolUse
    • PostToolUseFailure
    • UserPromptSubmit
    • Stop
  • Not supported: http, prompt, agent

Event Mapping

  • SessionStart.startupresources_discover(reason="startup")
  • SessionStart.startupsession_switch(reason="new")
  • SessionStart.resumesession_switch(reason="resume")
  • SessionStart.compactsession_compact
  • SessionEnd.othersession_shutdown
  • Stopagent_end (best-effort emulation of Claude Code's “after response completes” behavior)

Configuration Format

Configure hooks in ~/.pi/agent/settings.json or .pi/settings.json:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Session started'"
          }
        ]
      },
      {
        "matcher": "resume",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Session resumed'"
          }
        ]
      },
      {
        "matcher": "compact",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Context compacted'"
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "echo 'Session ended on shutdown/exit'"
          }
        ]
      }
    ]
  }
}

matcher Behavior

To stay aligned with Claude Code, matcher is a single regex string.

  • Omitted matcher means match everything
  • "" means match everything
  • "*" means match everything
  • Any other value is treated as a regular expression
  • If the regex is invalid, it falls back to exact string matching

Example:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "bash",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'bash only'"
          }
        ]
      },
      {
        "matcher": "write|edit",
        "hooks": [
          {
            "type": "command",
            "command": "echo 'write or edit'"
          }
        ]
      }
    ]
  }
}

Event-specific matching fields:

SessionStart

Matches source:

  • startup
  • resume
  • compact

SessionEnd

Matches reason:

  • other

PreToolUse / PostToolUse / PostToolUseFailure

Matches tool_name.

Note: this uses Pi's raw tool names directly, so names are usually lowercase, for example:

  • bash
  • read
  • write
  • edit
  • grep
  • find
  • ls
  • mcp__.*

Notes:

  • SessionEnd is triggered from session_shutdown
  • When matcher is omitted, it defaults to other for SessionEnd
  • UserPromptSubmit and Stop do not support matcher; if provided, it is ignored

if Conditions

Following Claude Code's approach, if is configured on each individual hook handler and only works for tool events:

  • PreToolUse
  • PostToolUse
  • PostToolUseFailure

If if is set on other event types, that hook will not run.

Currently supported forms:

  • Bash(git *)
  • bash(git *)
  • Edit(*.ts)
  • Write(*.md)
  • mcp__memory__create_entities(*)

Rules:

  • if syntax is ToolName(pattern)
  • ToolName is compared case-insensitively
  • pattern uses simple wildcard matching where * means any string
  • bash mainly matches tool_input.command
  • read, write, and edit mainly match tool_input.path (or file_path)
  • Other tools first try common primary fields, then fall back to the JSON string of tool_input

Example: block only git push

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git push*)",
            "command": "printf '%s\n' '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"git push is blocked\"}}'"
          }
        ]
      }
    ]
  }
}

Hook Input

Input fields are designed to be as close as possible to Claude Code hooks:

  • Common fields: session_id, transcript_path, cwd, hook_event_name
  • Event-specific fields such as source, reason, tool_name, tool_input, tool_response
  • Pi-specific extra fields may also be included, but they should not break Claude Code-style scripts

SessionStart

{
  "session_id": "session-file-path",
  "transcript_path": "/path/to/session.jsonl",
  "cwd": "/current/working/directory",
  "hook_event_name": "SessionStart",
  "source": "startup"
}

SessionEnd

{
  "session_id": "session-file-path",
  "transcript_path": "/path/to/session.jsonl",
  "cwd": "/current/working/directory",
  "hook_event_name": "SessionEnd",
  "reason": "other"
}

UserPromptSubmit

{
  "session_id": "session-file-path",
  "transcript_path": "/path/to/session.jsonl",
  "cwd": "/current/working/directory",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "Write a function to calculate the factorial of a number"
}

Notes:

  • UserPromptSubmit does not support matcher; if configured, it is ignored
  • It runs after the user submits input and before the agent loop starts

Stop

Mapped from Pi's agent_end event.

{
  "session_id": "session-file-path",
  "transcript_path": "/path/to/session.jsonl",
  "cwd": "/current/working/directory",
  "hook_event_name": "Stop",
  "stop_hook_active": false,
  "last_assistant_message": "I have completed the task."
}

Notes:

  • Stop runs after the current agent turn finishes
  • Stop does not support matcher; if configured, it is ignored
  • stop_hook_active indicates whether the current continuation was triggered by a previous Stop hook
  • last_assistant_message tries to extract the last assistant text content; if none exists, it is an empty string
  • When decision: "block" is returned, the extension best-effort simulates Claude Code's “prevent stopping and continue” behavior by injecting hidden context and starting another agent turn

PreToolUse

Mapped from Pi's tool_call event.

{
  "session_id": "session-file-path",
  "transcript_path": "/path/to/session.jsonl",
  "cwd": "/current/working/directory",
  "hook_event_name": "PreToolUse",
  "tool_name": "bash",
  "tool_input": {
    "command": "ls -la"
  },
  "tool_use_id": "toolu_123"
}

Notes:

  • PreToolUse runs before the tool actually executes
  • It maps to Pi's tool_call, not tool_execution_start
  • matcher is supported and is applied to tool_name
  • permission_mode is not included
  • tool_name uses Pi's original event value without case conversion
  • tool_input comes from tool_call.event.input
  • tool_use_id comes from tool_call.event.toolCallId

PostToolUse

Mapped from Pi's tool_result event and only fired when the tool succeeds.

{
  "session_id": "session-file-path",
  "transcript_path": "/path/to/session.jsonl",
  "cwd": "/current/working/directory",
  "hook_event_name": "PostToolUse",
  "tool_name": "bash",
  "tool_input": {
    "command": "pwd"
  },
  "tool_response": {
    "content": [
      {
        "type": "text",
        "text": "/tmp/project"
      }
    ],
    "details": {},
    "is_error": false,
    "output": "/tmp/project"
  },
  "tool_use_id": "toolu_123"
}

Notes:

  • PostToolUse runs after successful tool execution
  • It maps to Pi's tool_result
  • permission_mode is not included
  • tool_name uses Pi's original event value without case conversion
  • tool_input comes from tool_result.event.input
  • tool_response is the Claude Code-style compatible tool result object
  • tool_use_id comes from tool_result.event.toolCallId
  • Failed tool results are routed to PostToolUseFailure instead of PostToolUse

Hook Output

UserPromptSubmit: block the prompt or inject extra context

{
  "decision": "block",
  "reason": "Explanation for decision",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "My additional context here"
  }
}

Notes:

  • For UserPromptSubmit, the only meaningful decision value is "block"
  • Omitting decision means allow
  • Other values are ignored
  • reason is shown to the user but not injected into context
  • additionalContext is injected as hidden context into the current turn

Stop: prevent stopping and continue for one more turn

{
  "decision": "block",
  "reason": "Run a final self-check before stopping",
  "hookSpecificOutput": {
    "hookEventName": "Stop",
    "additionalContext": "Verify there are no missing tests."
  }
}

Notes:

  • For Stop, the only meaningful decision value is "block"
  • Omitting decision means finish normally
  • reason is injected as hidden context into the follow-up agent turn
  • additionalContext is injected together with reason when decision: "block"; if no continuation happens, it is not kept for later user input
  • stop_hook_active becomes true in follow-up Stop events triggered by a previous Stop hook, which helps avoid infinite loops
  • The current implementation is based on Pi's agent_end + sendMessage(..., { triggerTurn: true }), so behavior is best-effort

PreToolUse: deny or rewrite input

Available output fields:

  • permissionDecision: "allow" | "deny" | "ask"
  • permissionDecisionReason: the reason shown to the user/caller
  • updatedInput: rewrites tool input before execution
  • additionalContext: appends extra context for later processing

Example: deny execution

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "deny",
    "permissionDecisionReason": "Dangerous command blocked"
  }
}

Example: allow and rewrite input

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "My reason here",
    "updatedInput": {
      "field_to_modify": "new value"
    },
    "additionalContext": "Current environment: production. Proceed with caution."
  }
}

Notes:

  • permissionDecision: "deny" blocks the current tool call and returns permissionDecisionReason to the agent; it does not directly stop the entire current processing flow
  • permissionDecision: "allow" lets the tool run; if updatedInput is provided, the input is merged before execution
  • permissionDecision: "ask" is kept for compatibility only; this extension does not open an additional permission UI
  • updatedInput is merged into event.input, while unspecified fields keep their original values
  • additionalContext does not block execution; it is only injected as hidden context and does not normally create an extra UI message
  • To explicitly stop the current processing flow, use Claude Code's generic field continue: false

PostToolUse: append context or patch tool results

Claude Code-style output example:

{
  "decision": "block",
  "reason": "Explanation for decision",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "Additional information for Claude"
  }
}

Or:

{
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "Command succeeded"
  }
}

Pi-specific direct result patching is also supported:

{
  "systemMessage": "Hook patched tool result",
  "content": [
    {
      "type": "text",
      "text": "patched result"
    }
  ],
  "isError": false
}

Claude Code generic output fields are also supported:

{
  "continue": false,
  "stopReason": "Stop current processing",
  "systemMessage": "Hook requested stop"
}

Notes:

  • hookSpecificOutput.hookEventName is recognized following Claude Code behavior
  • decision: "block" does not roll back an already executed tool; instead, reason is appended to model context as feedback
  • additionalContext is injected into the current agent flow as hidden context, approximating Claude Code's “append context for Claude” behavior; it does not normally produce an extra UI message
  • systemMessage is silent by default for tool-related events (PreToolUse, PostToolUse, PostToolUseFailure) and does not normally generate an extra UI message
  • continue: false stops current processing in tool events on a best-effort basis; this is different from PreToolUse.permissionDecision: "deny", which only blocks the current tool and returns a reason to the agent
  • For PostToolUse and PostToolUseFailure, stop-processing behavior does not add an extra local warning by default; the hook's own returned message/result takes precedence
  • In addition to Claude Code-compatible fields, Pi-specific patching is still supported:
    • top-level content, details, isError
    • hookSpecificOutput.updatedToolResult
    • updatedMCPToolOutput (for MCP tool output replacement)
    • hookSpecificOutput.updatedMCPToolOutput

Usage

Local development

pi

Then run:

/pi-hooks
/pi-hooks-reset

As an npm package

pi install npm:@hsingjui/pi-hooks

Project Structure

Source code lives in src/:

  • src/pi-hooks.ts - extension entry point
  • src/config.ts - config loading and merging
  • src/executor.ts - command hook executor
  • src/hooks/shared.ts - shared parsing and execution helpers
  • src/hooks/session-hooks.ts - SessionStart / SessionEnd
  • src/hooks/compact-hooks.ts - PreCompact / PostCompact
  • src/hooks/prompt-hooks.ts - UserPromptSubmit
  • src/hooks/tool-hooks.ts - PreToolUse / PostToolUse / PostToolUseFailure
  • src/hooks/stop-hooks.ts - Stop
  • src/types.ts - type definitions

Notes

  • Hook commands run in the current session cwd
  • Global config and project config are merged by concatenating event arrays
  • PostToolUse and PostToolUseFailure support Pi result patching
  • Input/output aims to be Claude Code-compatible where possible; anything that cannot be mapped exactly is handled in a best-effort way using Pi's event model

About

Claude Code-compatible command hooks for the Pi coding agent

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors