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.
- Install the package:
pi install npm:@hsingjui/pi-hooks- Add hooks to
.pi/settings.json(or~/.pi/agent/settings.json):
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "echo 'agent finished'"
}
]
}
]
}
}- Run
/reload, then send a message to test it.
- Only
type: "command"is supported - Supports the
iffield on individual hook handlers for tool events only - Supported events:
SessionStartSessionEndPreCompactPostCompactPreToolUsePostToolUsePostToolUseFailureUserPromptSubmitStop
- Not supported:
http,prompt,agent
SessionStart.startup→resources_discover(reason="startup")SessionStart.startup→session_switch(reason="new")SessionStart.resume→session_switch(reason="resume")SessionStart.compact→session_compactSessionEnd.other→session_shutdownStop→agent_end(best-effort emulation of Claude Code's “after response completes” behavior)
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'"
}
]
}
]
}
}To stay aligned with Claude Code, matcher is a single regex string.
- Omitted
matchermeans 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:
Matches source:
startupresumecompact
Matches reason:
other
Matches tool_name.
Note: this uses Pi's raw tool names directly, so names are usually lowercase, for example:
bashreadwriteeditgrepfindlsmcp__.*
Notes:
SessionEndis triggered fromsession_shutdown- When
matcheris omitted, it defaults tootherforSessionEnd UserPromptSubmitandStopdo not supportmatcher; if provided, it is ignored
Following Claude Code's approach, if is configured on each individual hook handler and only works for tool events:
PreToolUsePostToolUsePostToolUseFailure
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:
ifsyntax isToolName(pattern)ToolNameis compared case-insensitivelypatternuses simple wildcard matching where*means any stringbashmainly matchestool_input.commandread,write, andeditmainly matchtool_input.path(orfile_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\"}}'"
}
]
}
]
}
}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
{
"session_id": "session-file-path",
"transcript_path": "/path/to/session.jsonl",
"cwd": "/current/working/directory",
"hook_event_name": "SessionStart",
"source": "startup"
}{
"session_id": "session-file-path",
"transcript_path": "/path/to/session.jsonl",
"cwd": "/current/working/directory",
"hook_event_name": "SessionEnd",
"reason": "other"
}{
"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:
UserPromptSubmitdoes not supportmatcher; if configured, it is ignored- It runs after the user submits input and before the agent loop starts
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:
Stopruns after the current agent turn finishesStopdoes not supportmatcher; if configured, it is ignoredstop_hook_activeindicates whether the current continuation was triggered by a previousStophooklast_assistant_messagetries 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
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:
PreToolUseruns before the tool actually executes- It maps to Pi's
tool_call, nottool_execution_start matcheris supported and is applied totool_namepermission_modeis not includedtool_nameuses Pi's original event value without case conversiontool_inputcomes fromtool_call.event.inputtool_use_idcomes fromtool_call.event.toolCallId
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:
PostToolUseruns after successful tool execution- It maps to Pi's
tool_result permission_modeis not includedtool_nameuses Pi's original event value without case conversiontool_inputcomes fromtool_result.event.inputtool_responseis the Claude Code-style compatible tool result objecttool_use_idcomes fromtool_result.event.toolCallId- Failed tool results are routed to
PostToolUseFailureinstead ofPostToolUse
{
"decision": "block",
"reason": "Explanation for decision",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "My additional context here"
}
}Notes:
- For
UserPromptSubmit, the only meaningfuldecisionvalue is"block" - Omitting
decisionmeans allow - Other values are ignored
reasonis shown to the user but not injected into contextadditionalContextis injected as hidden context into the current 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 meaningfuldecisionvalue is"block" - Omitting
decisionmeans finish normally reasonis injected as hidden context into the follow-up agent turnadditionalContextis injected together withreasonwhendecision: "block"; if no continuation happens, it is not kept for later user inputstop_hook_activebecomestruein follow-upStopevents triggered by a previousStophook, which helps avoid infinite loops- The current implementation is based on Pi's
agent_end+sendMessage(..., { triggerTurn: true }), so behavior is best-effort
Available output fields:
permissionDecision:"allow" | "deny" | "ask"permissionDecisionReason: the reason shown to the user/callerupdatedInput: rewrites tool input before executionadditionalContext: 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 returnspermissionDecisionReasonto the agent; it does not directly stop the entire current processing flowpermissionDecision: "allow"lets the tool run; ifupdatedInputis provided, the input is merged before executionpermissionDecision: "ask"is kept for compatibility only; this extension does not open an additional permission UIupdatedInputis merged intoevent.input, while unspecified fields keep their original valuesadditionalContextdoes 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
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.hookEventNameis recognized following Claude Code behaviordecision: "block"does not roll back an already executed tool; instead,reasonis appended to model context as feedbackadditionalContextis 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 messagesystemMessageis silent by default for tool-related events (PreToolUse,PostToolUse,PostToolUseFailure) and does not normally generate an extra UI messagecontinue: falsestops current processing in tool events on a best-effort basis; this is different fromPreToolUse.permissionDecision: "deny", which only blocks the current tool and returns a reason to the agent- For
PostToolUseandPostToolUseFailure, 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.updatedToolResultupdatedMCPToolOutput(for MCP tool output replacement)hookSpecificOutput.updatedMCPToolOutput
- top-level
piThen run:
/pi-hooks
/pi-hooks-reset
pi install npm:@hsingjui/pi-hooksSource code lives in src/:
src/pi-hooks.ts- extension entry pointsrc/config.ts- config loading and mergingsrc/executor.ts- command hook executorsrc/hooks/shared.ts- shared parsing and execution helperssrc/hooks/session-hooks.ts-SessionStart/SessionEndsrc/hooks/compact-hooks.ts-PreCompact/PostCompactsrc/hooks/prompt-hooks.ts-UserPromptSubmitsrc/hooks/tool-hooks.ts-PreToolUse/PostToolUse/PostToolUseFailuresrc/hooks/stop-hooks.ts-Stopsrc/types.ts- type definitions
- Hook commands run in the current session
cwd - Global config and project config are merged by concatenating event arrays
PostToolUseandPostToolUseFailuresupport 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