8.9 KiB
Input/Output Schemas
Complete JSON schemas for all hook types.
Common Input Fields
All hooks receive these fields:
{
session_id: string // Unique session identifier
transcript_path: string // Path to session transcript (.jsonl file)
cwd: string // Current working directory
permission_mode: string // "default" | "plan" | "acceptEdits" | "bypassPermissions"
hook_event_name: string // Name of the hook event
}
PreToolUse
Input:
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../session.jsonl",
"cwd": "/Users/username/project",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "npm install",
"description": "Install dependencies"
}
}
Output (optional, for control):
{
"decision": "approve" | "block",
"reason": "Explanation for the decision",
"permissionDecision": "allow" | "deny" | "ask",
"permissionDecisionReason": "Why this permission decision",
"updatedInput": {
"command": "npm install --save-exact"
},
"systemMessage": "Message displayed to user",
"suppressOutput": false,
"continue": true
}
Fields:
decision: Whether to allow the tool callreason: Explanation (required if blocking)permissionDecision: Override permission systemupdatedInput: Modified tool input (partial update)systemMessage: Message shown to usersuppressOutput: Hide hook output from usercontinue: If false, stop execution
PostToolUse
Input:
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../session.jsonl",
"cwd": "/Users/username/project",
"permission_mode": "default",
"hook_event_name": "PostToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.js",
"content": "const x = 1;"
},
"tool_output": "File created successfully at: /path/to/file.js"
}
Output (optional):
{
"systemMessage": "Code formatted successfully",
"suppressOutput": false
}
Fields:
systemMessage: Additional message to displaysuppressOutput: Hide tool output from user
UserPromptSubmit
Input:
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../session.jsonl",
"cwd": "/Users/username/project",
"permission_mode": "default",
"hook_event_name": "UserPromptSubmit",
"prompt": "Write a function to calculate factorial"
}
Output:
{
"decision": "approve" | "block",
"reason": "Prompt is clear and actionable",
"systemMessage": "Optional message to user"
}
Fields:
decision: Whether to allow the promptreason: Explanation (required if blocking)systemMessage: Message shown to user
Stop
Input:
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../session.jsonl",
"cwd": "/Users/username/project",
"permission_mode": "default",
"hook_event_name": "Stop",
"stop_hook_active": false
}
Output:
{
"decision": "block" | undefined,
"reason": "Tests are still failing - please fix before stopping",
"continue": true,
"stopReason": "Cannot stop yet",
"systemMessage": "Additional context"
}
Fields:
decision:"block"to prevent stopping,undefinedto allowreason: Why Claude should continue (required if blocking)continue: If true and blocking, Claude continues workingstopReason: Message shown when stopping is blockedsystemMessage: Additional context for Claudestop_hook_active: If true, don't block again (prevents infinite loops)
Important: Always check stop_hook_active to avoid infinite loops:
if (input.stop_hook_active) {
return { decision: undefined }; // Don't block again
}
SubagentStop
Input: Same as Stop
Output: Same as Stop
Usage: Same as Stop, but for subagent completion
SessionStart
Input:
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../session.jsonl",
"cwd": "/Users/username/project",
"permission_mode": "default",
"hook_event_name": "SessionStart",
"source": "startup" | "continue" | "checkpoint"
}
Output:
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Current sprint: Sprint 23\nFocus: User authentication\nDeadline: Friday"
}
}
Fields:
additionalContext: Text injected into session context- Multiple SessionStart hooks' contexts are concatenated
SessionEnd
Input:
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../session.jsonl",
"cwd": "/Users/username/project",
"permission_mode": "default",
"hook_event_name": "SessionEnd",
"reason": "exit" | "error" | "timeout" | "compact"
}
Output: None (ignored)
Usage: Cleanup tasks only. Cannot prevent session end.
PreCompact
Input:
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../session.jsonl",
"cwd": "/Users/username/project",
"permission_mode": "default",
"hook_event_name": "PreCompact",
"trigger": "manual" | "auto",
"custom_instructions": "Preserve all git commit messages"
}
Output:
{
"decision": "approve" | "block",
"reason": "Safe to compact" | "Wait until task completes"
}
Fields:
trigger: How compaction was initiatedcustom_instructions: User's compaction preferences (if manual)decision: Whether to proceed with compactionreason: Explanation
Notification
Input:
{
"session_id": "abc123",
"transcript_path": "~/.claude/projects/.../session.jsonl",
"cwd": "/Users/username/project",
"permission_mode": "default",
"hook_event_name": "Notification"
}
Output: None (hook just performs notification action)
Usage: Trigger external notifications (desktop, sound, status bar)
Common Output Fields
These fields can be returned by any hook:
{
"continue": true | false,
"stopReason": "Reason shown when stopping",
"suppressOutput": true | false,
"systemMessage": "Additional context or message"
}
Fields:
continue: If false, stop Claude's execution immediatelystopReason: Message displayed when execution stopssuppressOutput: If true, hide hook's stdout/stderr from usersystemMessage: Context added to Claude's next message
LLM Prompt Hook Response
When using type: "prompt", the LLM must return JSON:
{
"decision": "approve" | "block",
"reason": "Detailed explanation",
"systemMessage": "Optional message",
"continue": true | false,
"stopReason": "Optional stop message"
}
Example prompt:
Evaluate this command: $ARGUMENTS
Check if it's safe to execute.
Return JSON:
{
"decision": "approve" or "block",
"reason": "your explanation"
}
The $ARGUMENTS placeholder is replaced with the hook's input JSON.
Tool-Specific Input Fields
Different tools provide different tool_input fields:
Bash
{
"tool_input": {
"command": "npm install",
"description": "Install dependencies",
"timeout": 120000,
"run_in_background": false
}
}
Write
{
"tool_input": {
"file_path": "/path/to/file.js",
"content": "const x = 1;"
}
}
Edit
{
"tool_input": {
"file_path": "/path/to/file.js",
"old_string": "const x = 1;",
"new_string": "const x = 2;",
"replace_all": false
}
}
Read
{
"tool_input": {
"file_path": "/path/to/file.js",
"offset": 0,
"limit": 100
}
}
Grep
{
"tool_input": {
"pattern": "function.*",
"path": "/path/to/search",
"output_mode": "content"
}
}
MCP tools
{
"tool_input": {
// MCP tool-specific parameters
}
}
Access these in hooks:
command=$(echo "$input" | jq -r '.tool_input.command')
file_path=$(echo "$input" | jq -r '.tool_input.file_path')
Modifying Tool Input
PreToolUse hooks can modify tool_input before execution:
Original input:
{
"tool_input": {
"command": "npm install lodash"
}
}
Hook output:
{
"decision": "approve",
"reason": "Adding --save-exact flag",
"updatedInput": {
"command": "npm install --save-exact lodash"
}
}
Result: Tool executes with modified input.
Partial updates: Only specify fields you want to change:
{
"updatedInput": {
"timeout": 300000 // Only update timeout, keep other fields
}
}
Error Handling
Command hooks: Return non-zero exit code to indicate error
if [ error ]; then
echo '{"decision": "block", "reason": "Error occurred"}' >&2
exit 1
fi
Prompt hooks: LLM should return valid JSON. If malformed, hook fails gracefully.
Timeout: Set timeout (ms) to prevent hanging:
{
"type": "command",
"command": "/path/to/slow-script.sh",
"timeout": 30000
}
Default: 60000ms (60s)