@hsingjui/pi-hooks
Claude Code-compatible command hooks for the Pi coding agent
Package details
Install @hsingjui/pi-hooks from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@hsingjui/pi-hooks- Package
@hsingjui/pi-hooks- Version
0.0.2- Published
- May 8, 2026
- Downloads
- 124/mo · 37/wk
- Author
- hsingjui
- License
- MIT
- Types
- extension
- Size
- 76.5 KB
- Dependencies
- 0 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./src/pi-hooks.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
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
- 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.
Current Support
- Only
type: "command"is supported - Supports the
iffield on individual hook handlers for tool events only - Supported events:
SessionStartSessionEndPreCompactPostCompactPreToolUsePostToolUsePostToolUseFailureUserPromptSubmitStop
- Not supported:
http,prompt,agent
Event Mapping
SessionStart.startup→session_start(reason="startup")SessionStart.startup→session_start(reason="new")SessionStart.resume→session_start(reason="resume")SessionStart.compact→session_compactSessionEnd.other→session_shutdownStop→agent_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
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:
SessionStart
Matches source:
startupresumecompact
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:
bashreadwriteeditgrepfindlsmcp__.*
Notes:
SessionEndis triggered fromsession_shutdown- When
matcheris omitted, it defaults tootherforSessionEnd UserPromptSubmitandStopdo not supportmatcher; 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:
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\"}}'"
}
]
}
]
}
}
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:
UserPromptSubmitdoes not supportmatcher; 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:
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
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:
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
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:
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
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 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
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 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
PreToolUse: deny or rewrite input
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
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.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
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 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
Notes
- 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