Files
gh-glittercowboy-taches-cc-…/skills/create-hooks/references/input-output-schemas.md
2025-11-29 18:28:37 +08:00

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 call
  • reason: Explanation (required if blocking)
  • permissionDecision: Override permission system
  • updatedInput: Modified tool input (partial update)
  • systemMessage: Message shown to user
  • suppressOutput: Hide hook output from user
  • continue: 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 display
  • suppressOutput: 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 prompt
  • reason: 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, undefined to allow
  • reason: Why Claude should continue (required if blocking)
  • continue: If true and blocking, Claude continues working
  • stopReason: Message shown when stopping is blocked
  • systemMessage: Additional context for Claude
  • stop_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 initiated
  • custom_instructions: User's compaction preferences (if manual)
  • decision: Whether to proceed with compaction
  • reason: 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 immediately
  • stopReason: Message displayed when execution stops
  • suppressOutput: If true, hide hook's stdout/stderr from user
  • systemMessage: 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)