Skip to content

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.


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

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

A hook is any executable that reads JSON from stdin and communicates its decision via exit code and optional stdout JSON.

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 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.

The JSON payload varies by event.

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 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

Here’s a minimal hook that logs every Bash command to a file:

~/.pizzapi/hooks/log-bash.sh
#!/bin/bash
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
echo "$(date -Iseconds) $COMMAND" >> ~/.pizzapi/hooks/bash.log
exit 0

Hooks use two config patterns depending on the event type.

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 }
]
}
]
}
}

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 }
]
}
}
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.

Field Type Required Description
matcher string Yes Tool-name pattern. Supports `
hooks HookEntry[] Yes Hooks to run when the matcher matches.

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.

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 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.


Injects a suggestion to pipe verbose Bash output through rtk to reduce token usage:

~/.pizzapi/hooks/rtk-compress.sh
#!/bin/bash
# PreToolUse hook — suggests RTK compression for verbose Bash commands
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
# Only intercept Bash calls
if [ "$TOOL" != "Bash" ]; then
exit 0
fi
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
# Skip if already piped through rtk, or if it's a short command
if echo "$COMMAND" | grep -q "| rtk"; then
exit 0
fi
# Inject advisory context suggesting RTK compression
cat <<EOF
{
"additionalContext": "Consider piping verbose output through \`rtk\` to compress tokens: \`${COMMAND} | rtk\`"
}
EOF
exit 0

Config:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "command": "~/.pizzapi/hooks/rtk-compress.sh" }
]
}
]
}
}

Blocks user input containing potentially dangerous patterns:

~/.pizzapi/hooks/input-guard.sh
#!/bin/bash
# Input hook — blocks prompts that ask for credential exfiltration
INPUT=$(cat)
TEXT=$(echo "$INPUT" | jq -r '.text')
# Block requests to exfiltrate secrets
if echo "$TEXT" | grep -qiE "(send|post|upload|exfil).*(secret|token|key|password|credential)"; then
echo "Blocked: input appears to request credential exfiltration." >&2
exit 2
fi
# Allow everything else
exit 0

Config:

{
"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:

~/.pizzapi/hooks/pre-compact-checkpoint.sh
#!/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 changes
if git diff --quiet HEAD 2>/dev/null; then
# No changes — nothing to checkpoint
exit 0
fi
git diff HEAD > "$STASH_FILE"
echo "Checkpoint saved: $STASH_FILE" >&2
exit 0

Config:

{
"hooks": {
"SessionBeforeCompact": [
{ "command": "~/.pizzapi/hooks/pre-compact-checkpoint.sh", "timeout": 5000 }
]
}
}

PizzaPi enforces a strict trust boundary for project-local hooks:

  1. Global hooks (~/.pizzapi/config.json) always run — you control your own machine.
  2. Project hooks (.pizzapi/config.json in a repo) are ignored by default.
  3. To enable project hooks, set allowProjectHooks: true in your global config, or set PIZZAPI_ALLOW_PROJECT_HOOKS=1 in the environment.
  4. 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.