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:

FileScopeAlways active?
~/.pizzapi/config.jsonGlobal — applies to every project✅ Yes
.pizzapi/config.jsonProject-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.

EventWhen it firesCan block?Uses matchers?
PreToolUseBefore any tool callYes (exit 2)Yes
PostToolUseAfter a tool completesNo (can inject context)Yes
InputWhen user sends inputYes (exit 2), can transformNo
BeforeAgentStartBefore agent turn beginsCan override system promptNo
UserBashUser runs ! or !! shell commandYes (exit 2)No
SessionBeforeSwitchBefore switching sessionsYes (exit 2)No
SessionBeforeForkBefore forking a sessionYes (exit 2)No
SessionShutdownOn process exitNo (best-effort)No
SessionBeforeCompactBefore context compactionYes (exit 2)No
SessionBeforeTreeBefore session tree navigationYes (exit 2)No
ModelSelectWhen model is selectedNo (observability only)No

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

Exit codeMeaning
0Allow — action proceeds normally
2Block — action is cancelled (stderr is shown as the reason)
Other non-zeroError — treated as a block (fail-closed for safety)

Hooks can return JSON on stdout to influence the agent:

FieldUsed byPurpose
additionalContextPreToolUse, PostToolUse, BeforeAgentStartText injected into the agent’s context window
permissionDecisionPreToolUse"allow", "deny", or "ask"
textInputTransformed input text to replace the original
actionInput"continue", "transform", or "handled"
systemPromptBeforeAgentStartOverride the system prompt for this turn

The JSON payload varies by event. Tool hooks receive:

{
"tool_name": "Bash",
"tool_input": { "command": "rm -rf /tmp/build" }
}

PostToolUse hooks additionally receive:

{
"tool_name": "Write",
"tool_input": { "command": "...", "file_path": "src/index.ts" },
"tool_response": "File written successfully."
}

Event hooks receive an event field plus event-specific data:

{
"event": "Input",
"text": "refactor the auth module",
"source": "user"
}

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 }
]
}
}
FieldTypeRequiredDefaultDescription
commandstringYesShell command to execute. Receives JSON on stdin.
timeoutnumberNo10000Timeout in milliseconds. Hook is killed on expiry (fail-closed).

Matchers are regex patterns that filter which tools trigger a tool hook. They only apply to PreToolUse and PostToolUse.

PatternMatches
"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 nameDisplay name
bashBash
readRead
writeWrite
editEdit
grepGrep
findFind
lsLs

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 a hooks/hooks.json file. PizzaPi’s plugin adapter automatically discovers these and registers them alongside your config-defined hooks.

For full plugin documentation, see Claude Code Plugins.


Rewrites Bash commands through an RTK (Read-Transform-Keep) compressor to reduce token usage in tool responses:

~/.pizzapi/hooks/rtk-compress.sh
#!/bin/bash
# PreToolUse hook — rewrites bash commands to pipe through rtk
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 silently ignored by default.
  3. To enable project hooks, set allowProjectHooks: true in your global config.
  4. The project config cannot set allowProjectHooks — only the global config controls this gate.

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.