Skip to content

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 CodeMeaning
0Success
2Blocking error — stderr is fed back to Claude
OtherNon-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 EventBehavior
PreToolUseBlocks the tool call, shows stderr to Claude
PostToolUseShows stderr to Claude (tool already ran)
NotificationShows stderr to user only
UserPromptSubmitBlocks prompt processing, erases prompt, shows stderr to user
StopBlocks stoppage, shows stderr to Claude
SubagentStopBlocks stoppage, shows stderr to Claude subagent
PreCompactShows stderr to user only
SessionStartShows 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"
  }
}
permissionDecisionEffect
"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)

Agentics SOP — AI Developer Workflow & Agentic Coding Reference