Skip to content

hooks

Claude Code-style hooks that intercept any point in the agent lifecycle. Three hook types:

  • Command: run a shell command. Event data passed via PI_HOOK_EVENT env var. Exit 0 = allow, exit 2 = block (stderr becomes the reason).
  • Prompt: single LLM call for evaluation (not yet implemented).
  • Agent: spawn a subagent with tool access and a prompt template. Use $ARGUMENTS for event data injection.

Hooks can be sync (blocking, can return accept/reject decisions) or async (fire-and-forget background tasks). Matchers filter which tools or events trigger each hook using regex patterns.

Configure in hooks.json at the project root or in settings. Only tool_call and input events support blocking. All other events are observe-only.

Why it matters: Hooks are the enforcement layer. Auto-format after every write. Reject edits to protected files. Run linters after code changes. Send notifications when tasks complete. All without the agent knowing or needing to cooperate.

All pi events

Every event you can hook into:

Tool lifecycle

EventDescriptionMatcher fieldCan block?
tool_callBefore a tool executestoolNameYes
tool_resultAfter a tool returnstoolNameNo
user_bashWhen bash runs via the built-in toolNo

Agent lifecycle

EventDescriptionMatcher fieldCan block?
before_agent_startBefore the agent starts, can inject contextNo
agent_startWhen the agent starts processingNo
agent_endWhen the agent finishes processingNo
turn_startAt the start of each agent turnNo
turn_endAt the end of each agent turnNo
inputWhen the user submits inputYes

Session lifecycle

EventDescriptionMatcher fieldCan block?
session_startWhen a session starts or resumesNo
session_shutdownWhen a session is shutting downNo
session_compactAfter context is compactedNo
session_before_compactBefore context compaction, can preserve dataNo
session_forkAfter a session forkNo
session_before_forkBefore a session forkNo
session_switchAfter switching sessionsNo
session_before_switchBefore switching sessionsNo
session_treeAfter session tree operationsNo
session_before_treeBefore session tree operationsNo

Other

EventDescriptionMatcher fieldCan block?
contextContext filtering, can modify messages sent to the modelNo
model_selectWhen the model changes (set, cycle, or restore)No

Multi-source merge

Hooks are loaded from all sources and merged additively at session start. Matchers are concatenated per event — nothing is replaced. Every source participates:

  1. ~/.pi/agent/hooks.json (global standalone)
  2. ~/.pi/agent/settings.jsonhooks key (global settings)
  3. .pi/hooks.json (project standalone)
  4. .pi/settings.jsonhooks key (project settings)
  5. ~/.pi/agent/extensions/*/hooks.json (global extension hooks)
  6. .pi/extensions/*/hooks.json (project extension hooks)
  7. Runtime: hooks:merge event bus (other extensions, e.g. cc-plugins)

This means any extension can ship a hooks.json alongside its code and it automatically participates in the hook system. A hook from a CC plugin, a global extension, and your project’s hooks.json all fire on the same event if their matchers match.

Included hook

pi-code ships one basic starter hook: a tool_call command hook that matches bash invocations. It’s intentionally minimal, just enough to demonstrate the pattern.

{
"hooks": {
"tool_call": [
{
"matcher": "bash",
"hooks": [
{
"type": "command",
"command": "~/.pi/agent/extensions/hooks/tool-call/tool-call.sh",
"timeout": 5
}
]
}
]
}
}

Adding your own hooks

The pattern is straightforward. Here are a few examples to start from:

Auto-review after writes

Spawn a reviewer agent asynchronously after every write or edit. The agent checks the change and queues its result for the next turn.

{
"tool_result": [
{
"matcher": "write|edit",
"hooks": [
{
"type": "agent",
"agent": "reviewer",
"prompt": "Review this file change for issues:\n\n$ARGUMENTS",
"timeout": 60,
"async": true
}
]
}
]
}

Block destructive bash commands

A sync command hook that inspects the bash input before execution. The shell script exits 2 to block, with the reason on stderr.

{
"tool_call": [
{
"matcher": "bash",
"hooks": [
{
"type": "command",
"command": "~/.pi/agent/hooks/check-destructive.sh",
"timeout": 5
}
]
}
]
}
check-destructive.sh
#!/bin/bash
# PI_HOOK_EVENT contains JSON with the tool call details
INPUT=$(echo "$PI_HOOK_EVENT" | jq -r '.input.command // empty')
if echo "$INPUT" | grep -qE 'rm\s+-rf\s+/'; then
echo "Blocked: refusing to rm -rf a root path" >&2
exit 2
fi
exit 0

Completion check on agent end

After the agent finishes, spawn a reviewer to verify all tasks were completed.

{
"agent_end": [
{
"hooks": [
{
"type": "agent",
"agent": "reviewer",
"prompt": "Check if all tasks were completed. Return { \"ok\": true } or { \"ok\": false, \"reason\": \"...\" }.\n\n$ARGUMENTS",
"async": true
}
]
}
]
}