hook-developer

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Hook Developer

Hook 开发者指南

Complete reference for developing Claude Code hooks. Use this to write hooks with correct input/output schemas.
本文是开发Claude Code Hooks的完整参考手册,可帮助你编写符合正确输入/输出schema的hooks。

When to Use

适用场景

  • Creating a new hook
  • Debugging hook input/output format
  • Understanding what fields are available
  • Setting up hook registration in settings.json
  • Learning what hooks can block vs inject context
  • 创建新的hook
  • 调试hook的输入/输出格式
  • 了解可用字段
  • 在settings.json中配置hook注册
  • 了解哪些hook可拦截操作,哪些可注入上下文

Quick Reference

快速参考

HookFires WhenCan Block?Primary Use
PreToolUseBefore tool executesYESBlock/modify tool calls
PostToolUseAfter tool completesPartialReact to tool results
UserPromptSubmitUser sends promptYESValidate/inject context
PermissionRequestPermission dialog showsYESAuto-approve/deny
SessionStartSession beginsNOLoad context, set env vars
SessionEndSession endsNOCleanup/save state
StopAgent finishesYESForce continuation
SubagentStartSubagent spawnsNOPattern coordination
SubagentStopSubagent finishesYESForce continuation
PreCompactBefore compactionNOSave state
NotificationNotification sentNOCustom alerts
Hook type options:
type: "command"
(bash) or
type: "prompt"
(LLM evaluation)

Hook触发时机是否可拦截?主要用途
PreToolUse工具执行前拦截/修改工具调用
PostToolUse工具执行完成后部分支持响应工具执行结果
UserPromptSubmit用户发送提示词时验证提示词/注入上下文
PermissionRequest权限对话框弹出时自动批准/拒绝权限请求
SessionStart会话开始时加载上下文、设置环境变量
SessionEnd会话结束时清理/保存状态
StopAgent执行完成时强制Agent继续执行
SubagentStart子Agent生成时模式协调
SubagentStop子Agent执行完成时强制子Agent继续执行
PreCompact上下文压缩前保存状态
Notification发送通知时自定义告警
Hook类型选项:
type: "command"
(bash命令)或
type: "prompt"
(大语言模型评估)

Hook Input/Output Schemas

Hook 输入/输出 Schema

PreToolUse

PreToolUse

Purpose: Block or modify tool execution before it happens.
Input:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "default|plan|acceptEdits|bypassPermissions",
  "hook_event_name": "PreToolUse",
  "tool_name": "string",
  "tool_input": {
    "file_path": "string",
    "command": "string"
  },
  "tool_use_id": "string"
}
Output (JSON):
json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow|deny|ask",
    "permissionDecisionReason": "string",
    "updatedInput": {}
  },
  "continue": true,
  "stopReason": "string",
  "systemMessage": "string",
  "suppressOutput": true
}
Exit code 2: Blocks tool, stderr shown to Claude.
Common matchers:
Bash
,
Edit|Write
,
Read
,
Task
,
mcp__.*

用途: 在工具执行前拦截或修改其执行操作。
输入:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "default|plan|acceptEdits|bypassPermissions",
  "hook_event_name": "PreToolUse",
  "tool_name": "string",
  "tool_input": {
    "file_path": "string",
    "command": "string"
  },
  "tool_use_id": "string"
}
输出(JSON格式):
json
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow|deny|ask",
    "permissionDecisionReason": "string",
    "updatedInput": {}
  },
  "continue": true,
  "stopReason": "string",
  "systemMessage": "string",
  "suppressOutput": true
}
退出码2: 拦截工具执行,错误信息会展示给Claude。
常用匹配器:
Bash
,
Edit|Write
,
Read
,
Task
,
mcp__.*

PostToolUse

PostToolUse

Purpose: React to tool execution results, provide feedback to Claude.
Input:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "PostToolUse",
  "tool_name": "string",
  "tool_input": {},
  "tool_response": {
    "filePath": "string",
    "success": true,
    "output": "string",
    "exitCode": 0
  },
  "tool_use_id": "string"
}
CRITICAL: The response field is
tool_response
, NOT
tool_result
.
Output (JSON):
json
{
  "decision": "block",
  "reason": "string",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "string"
  },
  "continue": true,
  "stopReason": "string",
  "suppressOutput": true
}
Blocking:
"decision": "block"
with
"reason"
prompts Claude to address the issue.
Common matchers:
Edit|Write
,
Bash

用途: 响应工具执行结果,向Claude提供反馈。
输入:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "PostToolUse",
  "tool_name": "string",
  "tool_input": {},
  "tool_response": {
    "filePath": "string",
    "success": true,
    "output": "string",
    "exitCode": 0
  },
  "tool_use_id": "string"
}
重要提示: 响应字段是
tool_response
,而非
tool_result
输出(JSON格式):
json
{
  "decision": "block",
  "reason": "string",
  "hookSpecificOutput": {
    "hookEventName": "PostToolUse",
    "additionalContext": "string"
  },
  "continue": true,
  "stopReason": "string",
  "suppressOutput": true
}
拦截方式: 设置
"decision": "block"
并搭配
"reason"
,会提示Claude处理相关问题。
常用匹配器:
Edit|Write
,
Bash

UserPromptSubmit

UserPromptSubmit

Purpose: Validate user prompts, inject context before Claude processes.
Input:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "string"
}
Output (Plain text):
Any stdout text is added to context for Claude.
Output (JSON):
json
{
  "decision": "block",
  "reason": "string",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "string"
  }
}
Blocking:
"decision": "block"
erases prompt, shows
"reason"
to user only (not Claude).
Exit code 2: Blocks prompt, shows stderr to user only.

用途: 验证用户提示词,在Claude处理前注入上下文。
输入:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "string"
}
输出(纯文本格式):
标准输出的任何文本都会被添加到Claude的上下文中。
输出(JSON格式):
json
{
  "decision": "block",
  "reason": "string",
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "string"
  }
}
拦截方式: 设置
"decision": "block"
会清除提示词,仅向用户展示
"reason"
(不会展示给Claude)。
退出码2: 拦截提示词,仅向用户展示错误信息。

PermissionRequest

PermissionRequest

Purpose: Automate permission dialog decisions.
Input:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "PermissionRequest",
  "tool_name": "string",
  "tool_input": {}
}
Output:
json
{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow|deny",
      "updatedInput": {},
      "message": "string",
      "interrupt": false
    }
  }
}

用途: 自动化处理权限对话框的决策。
输入:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "PermissionRequest",
  "tool_name": "string",
  "tool_input": {}
}
输出:
json
{
  "hookSpecificOutput": {
    "hookEventName": "PermissionRequest",
    "decision": {
      "behavior": "allow|deny",
      "updatedInput": {},
      "message": "string",
      "interrupt": false
    }
  }
}

SessionStart

SessionStart

Purpose: Initialize session, load context, set environment variables.
Input:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "SessionStart",
  "source": "startup|resume|clear|compact"
}
Environment variable:
CLAUDE_ENV_FILE
- write
export VAR=value
to persist env vars.
Output (Plain text or JSON):
json
{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "string"
  },
  "suppressOutput": true
}
Plain text stdout is added as context.

用途: 初始化会话、加载上下文、设置环境变量。
输入:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "SessionStart",
  "source": "startup|resume|clear|compact"
}
环境变量:
CLAUDE_ENV_FILE
- 写入
export VAR=value
以持久化环境变量。
输出(纯文本或JSON格式):
json
{
  "hookSpecificOutput": {
    "hookEventName": "SessionStart",
    "additionalContext": "string"
  },
  "suppressOutput": true
}
标准输出的纯文本会被添加为上下文。

SessionEnd

SessionEnd

Purpose: Cleanup, save state, log session.
Input:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "SessionEnd",
  "reason": "clear|logout|prompt_input_exit|other"
}
Output: Cannot affect session (already ending). Use for cleanup only.

用途: 清理资源、保存状态、记录会话。
输入:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "SessionEnd",
  "reason": "clear|logout|prompt_input_exit|other"
}
输出: 无法影响会话(会话已进入结束流程),仅可用于清理操作。

Stop

Stop

Purpose: Control when Claude stops, force continuation.
Input:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "Stop",
  "stop_hook_active": false
}
CRITICAL: Check
stop_hook_active: true
to prevent infinite loops!
Output:
json
{
  "decision": "block",
  "reason": "string"
}
Blocking:
"decision": "block"
forces Claude to continue with
"reason"
as prompt.

用途: 控制Claude的停止时机,强制其继续执行。
输入:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "Stop",
  "stop_hook_active": false
}
重要提示: 需检查
stop_hook_active: true
以避免无限循环!
输出:
json
{
  "decision": "block",
  "reason": "string"
}
拦截方式: 设置
"decision": "block"
会强制Claude继续执行,
"reason"
会作为提示词。

SubagentStart

SubagentStart

Purpose: Run when a subagent (Task tool) is spawned.
Input:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "SubagentStart",
  "agent_id": "string"
}
Output: Context injection only (cannot block).

用途: 当子Agent(Task工具)生成时触发。
输入:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "SubagentStart",
  "agent_id": "string"
}
输出: 仅可注入上下文(无法拦截)。

SubagentStop

SubagentStop

Purpose: Control when subagents (Task tool) stop.
Input:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "SubagentStop",
  "stop_hook_active": false
}
Output: Same as Stop.

用途: 控制子Agent(Task工具)的停止时机。
输入:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "SubagentStop",
  "stop_hook_active": false
}
输出: 与Stop hook的输出格式一致。

PreCompact

PreCompact

Purpose: Save state before context compaction.
Input:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "PreCompact",
  "trigger": "manual|auto",
  "custom_instructions": "string"
}
Matchers:
manual
,
auto
Output:
json
{
  "continue": true,
  "systemMessage": "string"
}

用途: 在上下文压缩前保存状态。
输入:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "PreCompact",
  "trigger": "manual|auto",
  "custom_instructions": "string"
}
匹配器:
manual
,
auto
输出:
json
{
  "continue": true,
  "systemMessage": "string"
}

Notification

Notification

Purpose: Custom notification handling.
Input:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "Notification",
  "message": "string",
  "notification_type": "permission_prompt|idle_prompt|auth_success|elicitation_dialog"
}
Matchers:
permission_prompt
,
idle_prompt
,
auth_success
,
elicitation_dialog
,
*
Output:
json
{
  "continue": true,
  "suppressOutput": true,
  "systemMessage": "string"
}

用途: 自定义通知处理逻辑。
输入:
json
{
  "session_id": "string",
  "transcript_path": "string",
  "cwd": "string",
  "permission_mode": "string",
  "hook_event_name": "Notification",
  "message": "string",
  "notification_type": "permission_prompt|idle_prompt|auth_success|elicitation_dialog"
}
匹配器:
permission_prompt
,
idle_prompt
,
auth_success
,
elicitation_dialog
,
*
输出:
json
{
  "continue": true,
  "suppressOutput": true,
  "systemMessage": "string"
}

Registration in settings.json

在settings.json中注册Hook

Standard Structure

标准结构

json
{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolPattern",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}
json
{
  "hooks": {
    "EventName": [
      {
        "matcher": "ToolPattern",
        "hooks": [
          {
            "type": "command",
            "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

Matcher Patterns

匹配器模式

PatternMatches
Bash
Exactly Bash tool
Edit|Write
Edit OR Write
Read.*
Regex: Read*
mcp__.*__write.*
MCP write tools
*
All tools
Case-sensitive:
Bash
bash
模式匹配对象
Bash
精确匹配Bash工具
Edit|Write
匹配Edit或Write工具
Read.*
正则匹配:以Read开头的工具
mcp__.*__write.*
匹配MCP写入类工具
*
匹配所有工具
大小写敏感:
Bash
bash

Events Requiring Matchers

需要匹配器的事件

  • PreToolUse - YES (required)
  • PostToolUse - YES (required)
  • PermissionRequest - YES (required)
  • Notification - YES (optional)
  • SessionStart - YES (
    startup|resume|clear|compact
    )
  • PreCompact - YES (
    manual|auto
    )
  • PreToolUse - 是(必填)
  • PostToolUse - 是(必填)
  • PermissionRequest - 是(必填)
  • Notification - 是(可选)
  • SessionStart - 是(需匹配
    startup|resume|clear|compact
  • PreCompact - 是(需匹配
    manual|auto

Events Without Matchers

无需匹配器的事件

json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [{ "type": "command", "command": "/path/to/hook.sh" }]
      }
    ]
  }
}

json
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [{ "type": "command", "command": "/path/to/hook.sh" }]
      }
    ]
  }
}

Hook Types

Hook类型

Command Hooks (type: "command")

命令型Hook(type: "command")

Default type. Executes bash commands or scripts.
json
{
  "type": "command",
  "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh",
  "timeout": 60
}
默认类型,可执行bash命令或脚本。
json
{
  "type": "command",
  "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh",
  "timeout": 60
}

Prompt-Based Hooks (type: "prompt")

提示词型Hook(type: "prompt")

Uses LLM (Haiku) for context-aware decisions. Best for Stop/SubagentStop.
json
{
  "type": "prompt",
  "prompt": "Evaluate if Claude should stop. Context: $ARGUMENTS. Check if all tasks are complete.",
  "timeout": 30
}
Response schema:
json
{
  "decision": "approve" | "block",
  "reason": "Explanation",
  "continue": false,
  "stopReason": "Message to user",
  "systemMessage": "Warning"
}
使用大语言模型(Haiku)进行上下文感知决策,最适合用于Stop/SubagentStop场景。
json
{
  "type": "prompt",
  "prompt": "Evaluate if Claude should stop. Context: $ARGUMENTS. Check if all tasks are complete.",
  "timeout": 30
}
响应schema:
json
{
  "decision": "approve" | "block",
  "reason": "Explanation",
  "continue": false,
  "stopReason": "Message to user",
  "systemMessage": "Warning"
}

MCP Tool Naming

MCP工具命名规则

MCP tools use pattern
mcp__<server>__<tool>
:
PatternMatches
mcp__memory__.*
All memory server tools
mcp__.*__write.*
All MCP write tools
mcp__github__.*
All GitHub tools

MCP工具采用
mcp__<server>__<tool>
命名模式:
模式匹配对象
mcp__memory__.*
所有内存服务器工具
mcp__.*__write.*
所有MCP写入类工具
mcp__github__.*
所有GitHub工具

Environment Variables

环境变量

Available to All Hooks

所有Hook均可使用

VariableDescription
CLAUDE_PROJECT_DIR
Absolute path to project root
CLAUDE_CODE_REMOTE
"true" if remote/web, empty if local CLI
变量名描述
CLAUDE_PROJECT_DIR
项目根目录的绝对路径
CLAUDE_CODE_REMOTE
远程/网页版为"true",本地CLI版为空

SessionStart Only

仅SessionStart可用

VariableDescription
CLAUDE_ENV_FILE
Path to write
export VAR=value
lines
变量名描述
CLAUDE_ENV_FILE
写入
export VAR=value
语句的文件路径

Plugin Hooks Only

仅插件Hook可用

VariableDescription
CLAUDE_PLUGIN_ROOT
Absolute path to plugin directory

变量名描述
CLAUDE_PLUGIN_ROOT
插件目录的绝对路径

Exit Codes

退出码

Exit CodeBehaviorstdoutstderr
0SuccessJSON processedIgnored
2Blocking errorIGNOREDError message
OtherNon-blocking errorIgnoredVerbose mode
退出码行为标准输出标准错误
0执行成功处理JSON格式输出忽略
2拦截型错误忽略展示错误信息
其他非拦截型错误忽略仅在详细模式下展示

Exit Code 2 by Hook

各Hook使用退出码2的效果

HookEffect
PreToolUseBlocks tool, stderr to Claude
PostToolUsestderr to Claude (tool already ran)
UserPromptSubmitBlocks prompt, stderr to user only
StopBlocks stop, stderr to Claude

Hook效果
PreToolUse拦截工具执行,错误信息展示给Claude
PostToolUse错误信息展示给Claude(工具已执行完成)
UserPromptSubmit拦截提示词,错误信息仅展示给用户
Stop拦截停止操作,错误信息展示给Claude

Shell Wrapper Pattern

Shell包装器模式

bash
#!/bin/bash
set -e
cd "$CLAUDE_PROJECT_DIR/.claude/hooks"
cat | npx tsx src/my-hook.ts
Or for bundled:
bash
#!/bin/bash
set -e
cd "$HOME/.claude/hooks"
cat | node dist/my-hook.mjs

bash
#!/bin/bash
set -e
cd "$CLAUDE_PROJECT_DIR/.claude/hooks"
cat | npx tsx src/my-hook.ts
或者针对打包后的文件:
bash
#!/bin/bash
set -e
cd "$HOME/.claude/hooks"
cat | node dist/my-hook.mjs

TypeScript Handler Pattern

TypeScript处理器模式

typescript
import { readFileSync } from 'fs';

interface HookInput {
  session_id: string;
  hook_event_name: string;
  tool_name?: string;
  tool_input?: Record<string, unknown>;
  tool_response?: Record<string, unknown>;
  // ... other fields per hook type
}

function readStdin(): string {
  return readFileSync(0, 'utf-8');
}

async function main() {
  const input: HookInput = JSON.parse(readStdin());

  // Process input

  const output = {
    decision: 'block',  // or undefined to allow
    reason: 'Why blocking'
  };

  console.log(JSON.stringify(output));
}

main().catch(console.error);

typescript
import { readFileSync } from 'fs';

interface HookInput {
  session_id: string;
  hook_event_name: string;
  tool_name?: string;
  tool_input?: Record<string, unknown>;
  tool_response?: Record<string, unknown>;
  // ... 其他hook类型的字段
}

function readStdin(): string {
  return readFileSync(0, 'utf-8');
}

async function main() {
  const input: HookInput = JSON.parse(readStdin());

  // 处理输入

  const output = {
    decision: 'block',  // 或留空以允许执行
    reason: '拦截原因'
  };

  console.log(JSON.stringify(output));
}

main().catch(console.error);

Testing Hooks

测试Hook

Manual Test Commands

手动测试命令

bash
undefined
bash
undefined

PostToolUse (Write)

PostToolUse(Write工具)

echo '{"tool_name":"Write","tool_input":{"file_path":"test.md"},"tool_response":{"success":true},"session_id":"test"}' |
.claude/hooks/my-hook.sh
echo '{"tool_name":"Write","tool_input":{"file_path":"test.md"},"tool_response":{"success":true},"session_id":"test"}' |
.claude/hooks/my-hook.sh

PreToolUse (Bash)

PreToolUse(Bash工具)

echo '{"tool_name":"Bash","tool_input":{"command":"ls"},"session_id":"test"}' |
.claude/hooks/my-hook.sh
echo '{"tool_name":"Bash","tool_input":{"command":"ls"},"session_id":"test"}' |
.claude/hooks/my-hook.sh

SessionStart

SessionStart

echo '{"hook_event_name":"SessionStart","source":"startup","session_id":"test"}' |
.claude/hooks/session-start.sh
echo '{"hook_event_name":"SessionStart","source":"startup","session_id":"test"}' |
.claude/hooks/session-start.sh

SessionEnd

SessionEnd

echo '{"hook_event_name":"SessionEnd","reason":"clear","session_id":"test"}' |
.claude/hooks/session-end.sh
echo '{"hook_event_name":"SessionEnd","reason":"clear","session_id":"test"}' |
.claude/hooks/session-end.sh

UserPromptSubmit

UserPromptSubmit

echo '{"prompt":"test prompt","session_id":"test"}' |
.claude/hooks/prompt-submit.sh
undefined
echo '{"prompt":"test prompt","session_id":"test"}' |
.claude/hooks/prompt-submit.sh
undefined

Rebuild After TypeScript Edits

TypeScript修改后重新打包

bash
cd .claude/hooks
npx esbuild src/my-hook.ts \
  --bundle --platform=node --format=esm \
  --outfile=dist/my-hook.mjs

bash
cd .claude/hooks
npx esbuild src/my-hook.ts \
  --bundle --platform=node --format=esm \
  --outfile=dist/my-hook.mjs

Common Patterns

常见模式

Block Dangerous Files (PreToolUse)

拦截危险文件操作(PreToolUse)

python
#!/usr/bin/env python3
import json, sys

data = json.load(sys.stdin)
path = data.get('tool_input', {}).get('file_path', '')

BLOCKED = ['.env', 'secrets.json', '.git/']
if any(b in path for b in BLOCKED):
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": f"Blocked: {path} is protected"
        }
    }))
else:
    print('{}')
python
#!/usr/bin/env python3
import json, sys

data = json.load(sys.stdin)
path = data.get('tool_input', {}).get('file_path', '')

BLOCKED = ['.env', 'secrets.json', '.git/']
if any(b in path for b in BLOCKED):
    print(json.dumps({
        "hookSpecificOutput": {
            "hookEventName": "PreToolUse",
            "permissionDecision": "deny",
            "permissionDecisionReason": f"已拦截:{path}为受保护文件"
        }
    }))
else:
    print('{}')

Auto-Format Files (PostToolUse)

自动格式化文件(PostToolUse)

bash
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ "$FILE" == *.ts ]] || [[ "$FILE" == *.tsx ]]; then
  npx prettier --write "$FILE" 2>/dev/null
fi

echo '{}'
bash
#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ "$FILE" == *.ts ]] || [[ "$FILE" == *.tsx ]]; then
  npx prettier --write "$FILE" 2>/dev/null
fi

echo '{}'

Inject Git Context (UserPromptSubmit)

注入Git上下文(UserPromptSubmit)

bash
#!/bin/bash
echo "Git status:"
git status --short 2>/dev/null || echo "(not a git repo)"
echo ""
echo "Recent commits:"
git log --oneline -5 2>/dev/null || echo "(no commits)"
bash
#!/bin/bash
echo "Git状态:"
git status --short 2>/dev/null || echo "(当前不是Git仓库)"
echo ""
echo "最近提交:"
git log --oneline -5 2>/dev/null || echo "(无提交记录)"

Force Test Verification (Stop)

强制测试验证(Stop)

python
#!/usr/bin/env python3
import json, sys, subprocess

data = json.load(sys.stdin)
python
#!/usr/bin/env python3
import json, sys, subprocess

data = json.load(sys.stdin)

Prevent infinite loops

防止无限循环

if data.get('stop_hook_active'): print('{}') sys.exit(0)
if data.get('stop_hook_active'): print('{}') sys.exit(0)

Check if tests pass

检查测试是否通过

result = subprocess.run(['npm', 'test'], capture_output=True) if result.returncode != 0: print(json.dumps({ "decision": "block", "reason": "Tests are failing. Please fix before stopping." })) else: print('{}')

---
result = subprocess.run(['npm', 'test'], capture_output=True) if result.returncode != 0: print(json.dumps({ "decision": "block", "reason": "测试未通过,请修复后再停止。" })) else: print('{}')

---

Debugging Checklist

调试检查清单

  • Hook registered in settings.json?
  • Shell script has
    +x
    permission?
  • Bundle rebuilt after TS changes?
  • Using
    tool_response
    not
    tool_result
    ?
  • Output is valid JSON (or plain text)?
  • Checking
    stop_hook_active
    in Stop hooks?
  • Using
    $CLAUDE_PROJECT_DIR
    for paths?

  • 是否已在settings.json中注册Hook?
  • Shell脚本是否有
    +x
    执行权限?
  • TypeScript修改后是否重新打包?
  • 是否使用
    tool_response
    而非
    tool_result
  • 输出是否为合法JSON(或纯文本)?
  • Stop hook中是否检查
    stop_hook_active
  • 是否使用
    $CLAUDE_PROJECT_DIR
    来构建路径?

Key Learnings from Past Sessions

过往会话总结的关键经验

  1. Field names matter -
    tool_response
    not
    tool_result
  2. Output format -
    decision: "block"
    +
    reason
    for blocking
  3. Exit code 2 - stderr goes to Claude/user, stdout IGNORED
  4. Rebuild bundles - TypeScript source edits don't auto-apply
  5. Test manually -
    echo '{}' | ./hook.sh
    before relying on it
  6. Check outputs first -
    ls .claude/cache/
    before editing code
  7. Detached spawn hides errors - add logging to debug
  1. 字段名至关重要 - 使用
    tool_response
    而非
    tool_result
  2. 输出格式 - 拦截操作需设置
    decision: "block"
    并搭配
    reason
  3. 退出码2 - 标准错误会展示给Claude/用户,标准输出会被忽略
  4. 重新打包 - TypeScript源码修改后不会自动生效,需重新打包
  5. 手动测试 - 在依赖Hook前先执行
    echo '{}' | ./hook.sh
    测试
  6. 先检查输出 - 修改代码前先查看
    .claude/cache/
    中的输出
  7. 后台进程会隐藏错误 - 添加日志便于调试

See Also

相关链接

  • /debug-hooks
    - Systematic debugging workflow
  • .claude/rules/hooks.md
    - Hook development rules
  • /debug-hooks
    - 系统化调试流程
  • .claude/rules/hooks.md
    - Hook开发规则