Appearance
Hook Input & Output
Input Format
All hooks receive JSON on stdin:
typescript
{
session_id: string
transcript_path: string // Path to conversation .jsonl
cwd: string // Current working directory
hook_event_name: string // Event type
// Plus event-specific fields
}Exit Codes
The simplest way to communicate from a hook:
| Exit Code | Meaning |
|---|---|
0 | Success |
2 | Blocking error — stderr is fed back to Claude |
| Other | Non-blocking error — stderr is shown to user, execution continues |
WARNING
On exit code 0, Claude does NOT see stdout — except for UserPromptSubmit, where stdout is injected as context.
Exit Code 2 Behavior by Event
| Hook Event | Behavior |
|---|---|
PreToolUse | Blocks the tool call, shows stderr to Claude |
PostToolUse | Shows stderr to Claude (tool already ran) |
Notification | Shows stderr to user only |
UserPromptSubmit | Blocks prompt processing, erases prompt, shows stderr to user |
Stop | Blocks stoppage, shows stderr to Claude |
SubagentStop | Blocks stoppage, shows stderr to Claude subagent |
PreCompact | Shows stderr to user only |
SessionStart | Shows stderr to user only |
JSON Output (Advanced)
For more control, return JSON from stdout:
Common Fields (All Hook Types)
json
{
"continue": true, // Whether Claude should continue (default: true)
"stopReason": "string", // Shown to user when continue is false
"suppressOutput": true // Hide stdout from transcript mode (default: false)
}PreToolUse — Decision Control
json
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Reason shown to user"
}
}permissionDecision | Effect |
|---|---|
"allow" | Bypasses permission system, proceeds immediately |
"deny" | Prevents tool call, shows reason to Claude |
"ask" | Prompts user to confirm the tool call |
PostToolUse — Decision Control
json
{
"decision": "block",
"reason": "Explanation fed back to Claude"
}"block" automatically prompts Claude with the reason. undefined does nothing.
UserPromptSubmit — Decision + Context Injection
json
{
"decision": "block",
"reason": "Shown to user, prompt is erased",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "Injected into Claude's context if not blocked"
}
}Stop/SubagentStop — Continuation Control
json
{
"decision": "block",
"reason": "Must be provided — tells Claude how to proceed"
}"block" prevents Claude from stopping. Must include reason so Claude knows what to do.
SessionStart — Context Injection
json
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Today's date, recent git log, open issues, etc."
}
}Practical Reading: Common Patterns
Read stdin in Python
python
import json
import sys
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON: {e}", file=sys.stderr)
sys.exit(1)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})Block with reason to Claude
python
import json
import sys
# Block and tell Claude why
output = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Cannot modify .env files — use environment variable management"
}
}
print(json.dumps(output))
sys.exit(0)Inject context at session start
python
import json
import subprocess
import sys
# Get recent git activity
git_log = subprocess.run(
["git", "log", "--oneline", "-10"],
capture_output=True, text=True
).stdout
output = {
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": f"Recent commits:\n{git_log}"
}
}
print(json.dumps(output))
sys.exit(0)