Hooks
Hooks are shell scripts or commands that run at specific points in the agent’s lifecycle. They let you:
- Observe — log tool calls, track model switches, audit user input
- Transform — rewrite input text, inject context into the agent’s window, override the system prompt
- Block — prevent tool calls, cancel session switches, reject dangerous commands
Hooks receive JSON on stdin and communicate back via exit codes and stdout JSON.
Where to Define Hooks
Section titled “Where to Define Hooks”Hooks live in your PizzaPi config files under the hooks key:
| File | Scope | Always active? |
|---|---|---|
~/.pizzapi/config.json |
Global — applies to every project | ✅ Yes |
.pizzapi/config.json |
Project-local — only this repo | ⚠️ Requires allowProjectHooks |
Hook Events
Section titled “Hook Events”Every event fires at a specific lifecycle point. Some can block the action, and tool hooks support matchers to target specific tools.
| Event | When it runs | Config shape | Exit code can block? | Stdout can change behavior? |
|---|---|---|---|---|
PreToolUse |
Before a tool executes | HookMatcher[] |
Yes | Block, rewrite tool input, inject context |
PostToolUse |
After a tool result is available | HookMatcher[] |
No | Inject context into the tool result |
Input |
When user text is received, before expansion | HookEntry[] |
Yes | Transform or consume input |
UserBash |
When the user runs a ! / !! shell command |
HookEntry[] |
Yes | No |
BeforeAgentStart |
After user prompt handling, before the agent loop starts | HookEntry[] |
No | Inject context or override the system prompt |
ModelSelect |
When a model is selected | HookEntry[] |
No | No |
SessionStart |
On startup, reload, new session, resume, or fork | HookEntry[] |
No | No |
SessionShutdown |
On session shutdown | HookEntry[] |
No | No |
SessionBeforeCompact |
Before context compaction | HookEntry[] |
Yes | No |
SessionBeforeFork |
Before a session fork | HookEntry[] |
Yes | No |
SessionBeforeSwitch |
Before switching sessions | HookEntry[] |
Yes | No |
SessionBeforeTree |
Before session-tree navigation | HookEntry[] |
Yes | No |
TurnEnd |
After each agent turn | HookEntry[] |
No | No |
Writing a Hook
Section titled “Writing a Hook”A hook is any executable that reads JSON from stdin and communicates its decision via exit code and optional stdout JSON.
Exit codes
Section titled “Exit codes”| Event group | 0 |
2 |
Other non-zero / timeout |
|---|---|---|---|
PreToolUse |
Continue; parse stdout JSON if present | Block tool call; stderr is the reason | Block tool call; stderr or exit code is the reason |
Input, UserBash, SessionBeforeCompact, SessionBeforeFork, SessionBeforeSwitch, SessionBeforeTree |
Continue; parse stdout JSON where supported | Block/cancel; stderr is the reason | Block/cancel; stderr or exit code is the reason |
BeforeAgentStart |
Continue; parse stdout JSON if present | Logged as non-fatal | Logged as non-fatal |
PostToolUse |
Parse stdout JSON if present | Ignored | Ignored |
ModelSelect, SessionStart, SessionShutdown, TurnEnd |
Ignored | Ignored | Ignored |
Stdout JSON fields
Section titled “Stdout JSON fields”Stdout must be a single JSON object. PizzaPi also accepts Claude Code-style nested output under hookSpecificOutput.
| Field | Used by | Purpose |
|---|---|---|
additionalContext |
PreToolUse |
Queued and appended to the following tool result as [Hook] .... |
additionalContext |
PostToolUse |
Appended to the tool result as [Hook] .... |
additionalContext |
BeforeAgentStart |
Injected as collapsed hook context before the agent starts. |
permissionDecision: "deny" |
PreToolUse |
Blocks the tool call. The reason is additionalContext or Denied by hook. |
permissionDecision: "allow" / "ask" |
PreToolUse |
Parsed but not acted on. |
updatedInput |
PreToolUse |
Object merged into the tool input before execution. |
text |
Input |
Replaces the user input text. |
action: "transform" + text |
Input |
Replaces the user input text. |
action: "handled" |
Input |
Consumes the input; the agent does not see it. |
action: "continue" |
Input |
Parsed, but normal continuation is already the default. |
systemPrompt |
BeforeAgentStart |
Overrides the system prompt for this turn. |
decision |
Any | Parsed for compatibility, but config-defined hooks do not currently act on it. |
Stdin payload
Section titled “Stdin payload”The JSON payload varies by event.
Tool hooks
Section titled “Tool hooks”PreToolUse hooks receive:
{ "tool_name": "bash", "tool_input": { "command": "bun test" }}PostToolUse hooks additionally receive tool_response:
{ "tool_name": "write", "tool_input": { "path": "src/index.ts", "file_path": "src/index.ts" }, "tool_response": "File written successfully."}tool_input is normalized so file tools include both path and file_path when either key is present.
Event hooks
Section titled “Event hooks”| Event | Stdin JSON fields |
|---|---|
Input |
event, text, source |
UserBash |
event, command, exclude_from_context, cwd, tool_input: { command } |
BeforeAgentStart |
event, prompt, system_prompt |
ModelSelect |
event, model: { provider, id, name }, previous_model (null or same shape), source |
SessionStart |
event, reason, previous_session_file, session_id, model (null or { provider, id, name }) |
SessionShutdown |
event, session_file |
SessionBeforeCompact |
event, custom_instructions |
SessionBeforeFork |
event, entry_id |
SessionBeforeSwitch |
event, reason, target_session_file |
SessionBeforeTree |
event, target_id, old_leaf_id, user_wants_summary |
TurnEnd |
event, turn_index, stop_reason, session_id |
Simple example
Section titled “Simple example”Here’s a minimal hook that logs every Bash command to a file:
#!/bin/bashINPUT=$(cat)COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')echo "$(date -Iseconds) $COMMAND" >> ~/.pizzapi/hooks/bash.logexit 0Config Format
Section titled “Config Format”Hooks use two config patterns depending on the event type.
Tool hooks (PreToolUse, PostToolUse)
Section titled “Tool hooks (PreToolUse, PostToolUse)”Tool hooks use HookMatcher[] — each entry pairs a regex matcher with an array of hooks:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash|Edit|Write", "hooks": [ { "command": "~/.pizzapi/hooks/lint.sh", "timeout": 10000 } ] } ] }}Event hooks (everything else)
Section titled “Event hooks (everything else)”All other events use HookEntry[] directly — no matcher needed:
{ "hooks": { "Input": [ { "command": "~/.pizzapi/hooks/input-guard.sh" } ], "SessionShutdown": [ { "command": "~/.pizzapi/hooks/cleanup.sh", "timeout": 5000 } ] }}HookEntry fields
Section titled “HookEntry fields”| Field | Type | Required | Default | Description |
|---|---|---|---|---|
command |
string |
Yes | — | Shell command to run. PizzaPi expands variables in the command string, runs it from the session cwd, sends the event JSON on stdin, and sets PIZZAPI_PROJECT_DIR to the session cwd. |
timeout |
number |
No | 10000 |
Timeout in milliseconds. Timeout kills the hook and is fail-closed for blocking events. |
Hooks run through a POSIX shell: /bin/sh -c on Unix/macOS; on Windows, PizzaPi resolves Git for Windows bash.exe or falls back to bash in PATH.
HookMatcher fields
Section titled “HookMatcher fields”| Field | Type | Required | Description |
|---|---|---|---|
matcher |
string |
Yes | Tool-name pattern. Supports ` |
hooks |
HookEntry[] |
Yes | Hooks to run when the matcher matches. |
Matchers
Section titled “Matchers”Matchers are regex patterns that filter which tools trigger a tool hook. They only apply to PreToolUse and PostToolUse.
| Pattern | Matches |
|---|---|
"Bash" |
Only the Bash tool |
"Edit|Write" |
Edit or Write |
".*" |
All tools (wildcard) |
"mcp__(github|filesystem)__.*" |
MCP tools from specific servers |
Omitting the matcher or setting it to ".*" matches every tool.
Tool name mapping
Section titled “Tool name mapping”Pi’s internal tool names are mapped to display names for matching:
| Internal name | Display name |
|---|---|
bash |
Bash |
read |
Read |
write |
Write |
edit |
Edit |
grep |
Grep |
find |
Find |
ls |
Ls |
MCP tool names are passed as-is (e.g., mcp__tavily__tavily_search). Matching is case-insensitive — both the internal name and display name are checked.
Claude Code Compatibility
Section titled “Claude Code Compatibility”Claude Code plugins can bundle hooks in one or more *.json files inside a hooks/ directory (commonly hooks/hooks.json). PizzaPi’s plugin adapter automatically discovers these and registers them alongside your config-defined hooks.
For full plugin documentation, see Claude Code Plugins.
Examples
Section titled “Examples”1. Advisory RTK suggestion (PreToolUse)
Section titled “1. Advisory RTK suggestion (PreToolUse)”Injects a suggestion to pipe verbose Bash output through rtk to reduce token usage:
#!/bin/bash# PreToolUse hook — suggests RTK compression for verbose Bash commandsINPUT=$(cat)TOOL=$(echo "$INPUT" | jq -r '.tool_name')
# Only intercept Bash callsif [ "$TOOL" != "Bash" ]; then exit 0fi
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
# Skip if already piped through rtk, or if it's a short commandif echo "$COMMAND" | grep -q "| rtk"; then exit 0fi
# Inject advisory context suggesting RTK compressioncat <<EOF{ "additionalContext": "Consider piping verbose output through \`rtk\` to compress tokens: \`${COMMAND} | rtk\`"}EOFexit 0Config:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "command": "~/.pizzapi/hooks/rtk-compress.sh" } ] } ] }}2. Input Guard (Input)
Section titled “2. Input Guard (Input)”Blocks user input containing potentially dangerous patterns:
#!/bin/bash# Input hook — blocks prompts that ask for credential exfiltrationINPUT=$(cat)TEXT=$(echo "$INPUT" | jq -r '.text')
# Block requests to exfiltrate secretsif echo "$TEXT" | grep -qiE "(send|post|upload|exfil).*(secret|token|key|password|credential)"; then echo "Blocked: input appears to request credential exfiltration." >&2 exit 2fi
# Allow everything elseexit 0Config:
{ "hooks": { "Input": [ { "command": "~/.pizzapi/hooks/input-guard.sh" } ] }}3. Pre-Compaction Checkpoint (SessionBeforeCompact)
Section titled “3. Pre-Compaction Checkpoint (SessionBeforeCompact)”Saves a snapshot of the current working tree before context compaction, so you can diff what changed:
#!/bin/bash# SessionBeforeCompact hook — stash uncommitted changes before compaction
CHECKPOINT_DIR="${PIZZAPI_PROJECT_DIR:-.}/.pizzapi/checkpoints"mkdir -p "$CHECKPOINT_DIR"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)STASH_FILE="$CHECKPOINT_DIR/pre-compact-$TIMESTAMP.patch"
cd "${PIZZAPI_PROJECT_DIR:-.}"
# Save a diff of uncommitted changesif git diff --quiet HEAD 2>/dev/null; then # No changes — nothing to checkpoint exit 0fi
git diff HEAD > "$STASH_FILE"echo "Checkpoint saved: $STASH_FILE" >&2exit 0Config:
{ "hooks": { "SessionBeforeCompact": [ { "command": "~/.pizzapi/hooks/pre-compact-checkpoint.sh", "timeout": 5000 } ] }}Project Hooks Trust Model
Section titled “Project Hooks Trust Model”PizzaPi enforces a strict trust boundary for project-local hooks:
- Global hooks (
~/.pizzapi/config.json) always run — you control your own machine. - Project hooks (
.pizzapi/config.jsonin a repo) are ignored by default. - To enable project hooks, set
allowProjectHooks: truein your global config, or setPIZZAPI_ALLOW_PROJECT_HOOKS=1in the environment. - The project config cannot set
allowProjectHooks— only the global config or environment controls this gate.
When both trusted sources define hooks, PizzaPi concatenates their arrays. For matcher-based hooks, global matcher groups run before project matcher groups. For entry-based hooks, global entries run before project entries.
The environment variable PIZZAPI_ALLOW_PROJECT_HOOKS=1 can also enable project hooks, which is useful for CI or automated environments where you manage config externally.