名称: 钩子-开发者 描述: Claude Code钩子完整参考 - 输入/输出模式、注册、测试模式
钩子开发者
Claude Code钩子开发的完整参考。使用此参考编写具有正确输入/输出模式的钩子。
何时使用
- 创建新钩子
- 调试钩子输入/输出格式
- 理解可用字段
- 在settings.json中设置钩子注册
- 学习哪些钩子可以阻止或注入上下文
快速参考
| 钩子 | 触发时机 | 可以阻止? | 主要用途 |
|---|---|---|---|
| PreToolUse | 工具执行前 | 是 | 阻止/修改工具调用 |
| PostToolUse | 工具完成后 | 部分 | 响应工具结果 |
| UserPromptSubmit | 用户提交提示时 | 是 | 验证/注入上下文 |
| PermissionRequest | 权限对话框显示时 | 是 | 自动批准/拒绝 |
| SessionStart | 会话开始时 | 否 | 加载上下文、设置环境变量 |
| SessionEnd | 会话结束时 | 否 | 清理/保存状态 |
| Stop | 代理完成时 | 是 | 强制继续 |
| SubagentStart | 子代理生成时 | 否 | 模式协调 |
| SubagentStop | 子代理完成时 | 是 | 强制继续 |
| PreCompact | 压缩前 | 否 | 保存状态 |
| Notification | 通知发送时 | 否 | 自定义警报 |
钩子类型选项: type: "command" (bash) 或 type: "prompt" (LLM评估)
钩子输入/输出模式
PreToolUse
目的: 在工具执行前阻止或修改。
输入:
{
"session_id": "字符串",
"transcript_path": "字符串",
"cwd": "字符串",
"permission_mode": "default|plan|acceptEdits|bypassPermissions",
"hook_event_name": "PreToolUse",
"tool_name": "字符串",
"tool_input": {
"file_path": "字符串",
"command": "字符串"
},
"tool_use_id": "字符串"
}
输出 (JSON):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "字符串",
"updatedInput": {}
},
"continue": true,
"stopReason": "字符串",
"systemMessage": "字符串",
"suppressOutput": true
}
退出码 2: 阻止工具,stderr显示给Claude。
常见匹配器: Bash, Edit|Write, Read, Task, mcp__.*
PostToolUse
目的: 响应工具执行结果,向Claude提供反馈。
输入:
{
"session_id": "字符串",
"transcript_path": "字符串",
"cwd": "字符串",
"permission_mode": "字符串",
"hook_event_name": "PostToolUse",
"tool_name": "字符串",
"tool_input": {},
"tool_response": {
"filePath": "字符串",
"success": true,
"output": "字符串",
"exitCode": 0
},
"tool_use_id": "字符串"
}
关键: 响应字段是 tool_response,不是 tool_result。
输出 (JSON):
{
"decision": "block",
"reason": "字符串",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "字符串"
},
"continue": true,
"stopReason": "字符串",
"suppressOutput": true
}
阻止: "decision": "block" 带 "reason" 提示Claude解决问题。
常见匹配器: Edit|Write, Bash
UserPromptSubmit
目的: 验证用户提示,在Claude处理前注入上下文。
输入:
{
"session_id": "字符串",
"transcript_path": "字符串",
"cwd": "字符串",
"permission_mode": "字符串",
"hook_event_name": "UserPromptSubmit",
"prompt": "字符串"
}
输出 (纯文本):
任何stdout文本添加为Claude的上下文。
输出 (JSON):
{
"decision": "block",
"reason": "字符串",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "字符串"
}
}
阻止: "decision": "block" 擦除提示,仅向用户显示 "reason" (不给Claude)。
退出码 2: 阻止提示,仅向用户显示stderr。
PermissionRequest
目的: 自动化权限对话框决策。
输入:
{
"session_id": "字符串",
"transcript_path": "字符串",
"cwd": "字符串",
"permission_mode": "字符串",
"hook_event_name": "PermissionRequest",
"tool_name": "字符串",
"tool_input": {}
}
输出:
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow|deny",
"updatedInput": {},
"message": "字符串",
"interrupt": false
}
}
}
SessionStart
目的: 初始化会话,加载上下文,设置环境变量。
输入:
{
"session_id": "字符串",
"transcript_path": "字符串",
"cwd": "字符串",
"permission_mode": "字符串",
"hook_event_name": "SessionStart",
"source": "startup|resume|clear|compact"
}
环境变量: CLAUDE_ENV_FILE - 写入 export VAR=value 以持久化环境变量。
输出 (纯文本或JSON):
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "字符串"
},
"suppressOutput": true
}
纯文本stdout添加为上下文。
SessionEnd
目的: 清理、保存状态、记录会话。
输入:
{
"session_id": "字符串",
"transcript_path": "字符串",
"cwd": "字符串",
"permission_mode": "字符串",
"hook_event_name": "SessionEnd",
"reason": "clear|logout|prompt_input_exit|other"
}
输出: 无法影响会话 (已结束)。仅用于清理。
Stop
目的: 控制Claude停止时机,强制继续。
输入:
{
"session_id": "字符串",
"transcript_path": "字符串",
"cwd": "字符串",
"permission_mode": "字符串",
"hook_event_name": "Stop",
"stop_hook_active": false
}
关键: 检查 stop_hook_active: true 以防止无限循环!
输出:
{
"decision": "block",
"reason": "字符串"
}
阻止: "decision": "block" 强制Claude继续,以 "reason" 作为提示。
SubagentStart
目的: 当子代理 (Task工具) 生成时运行。
输入:
{
"session_id": "字符串",
"transcript_path": "字符串",
"cwd": "字符串",
"permission_mode": "字符串",
"hook_event_name": "SubagentStart",
"agent_id": "字符串"
}
输出: 仅上下文注入 (不能阻止)。
SubagentStop
目的: 控制子代理 (Task工具) 停止时机。
输入:
{
"session_id": "字符串",
"transcript_path": "字符串",
"cwd": "字符串",
"permission_mode": "字符串",
"hook_event_name": "SubagentStop",
"stop_hook_active": false
}
输出: 与Stop相同。
PreCompact
目的: 在上下文压缩前保存状态。
输入:
{
"session_id": "字符串",
"transcript_path": "字符串",
"cwd": "字符串",
"permission_mode": "字符串",
"hook_event_name": "PreCompact",
"trigger": "manual|auto",
"custom_instructions": "字符串"
}
匹配器: manual, auto
输出:
{
"continue": true,
"systemMessage": "字符串"
}
Notification
目的: 自定义通知处理。
输入:
{
"session_id": "字符串",
"transcript_path": "字符串",
"cwd": "字符串",
"permission_mode": "字符串",
"hook_event_name": "Notification",
"message": "字符串",
"notification_type": "permission_prompt|idle_prompt|auth_success|elicitation_dialog"
}
匹配器: permission_prompt, idle_prompt, auth_success, elicitation_dialog, *
输出:
{
"continue": true,
"suppressOutput": true,
"systemMessage": "字符串"
}
settings.json中的注册
标准结构
{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh",
"timeout": 60
}
]
}
]
}
}
匹配器模式
| 模式 | 匹配 |
|---|---|
Bash |
精确匹配Bash工具 |
Edit|Write |
匹配Edit或Write |
Read.* |
正则表达式: Read* |
mcp__.*__write.* |
MCP写工具 |
* |
所有工具 |
区分大小写: Bash ≠ bash
需要匹配器的事件
- PreToolUse - 是 (必需)
- PostToolUse - 是 (必需)
- PermissionRequest - 是 (必需)
- Notification - 是 (可选)
- SessionStart - 是 (
startup|resume|clear|compact) - PreCompact - 是 (
manual|auto)
无匹配器的事件
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [{ "type": "command", "command": "/path/to/hook.sh" }]
}
]
}
}
钩子类型
命令钩子 (type: “command”)
默认类型。执行bash命令或脚本。
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh",
"timeout": 60
}
基于提示的钩子 (type: “prompt”)
使用LLM (Haiku) 进行上下文感知决策。最适合Stop/SubagentStop。
{
"type": "prompt",
"prompt": "评估Claude是否应该停止。上下文: $ARGUMENTS。检查所有任务是否完成。",
"timeout": 30
}
响应模式:
{
"decision": "approve" | "block",
"reason": "解释",
"continue": false,
"stopReason": "给用户的消息",
"systemMessage": "警告"
}
MCP工具命名
MCP工具使用模式 mcp__<server>__<tool>:
| 模式 | 匹配 |
|---|---|
mcp__memory__.* |
所有memory服务器工具 |
mcp__.*__write.* |
所有MCP写工具 |
mcp__github__.* |
所有GitHub工具 |
环境变量
所有钩子可用
| 变量 | 描述 |
|---|---|
CLAUDE_PROJECT_DIR |
项目根目录的绝对路径 |
CLAUDE_CODE_REMOTE |
如果远程/网络则为"true",本地CLI则为空 |
仅SessionStart
| 变量 | 描述 |
|---|---|
CLAUDE_ENV_FILE |
写入 export VAR=value 行的路径 |
仅插件钩子
| 变量 | 描述 |
|---|---|
CLAUDE_PLUGIN_ROOT |
插件目录的绝对路径 |
退出码
| 退出码 | 行为 | stdout | stderr |
|---|---|---|---|
| 0 | 成功 | JSON处理 | 忽略 |
| 2 | 阻止错误 | 忽略 | 错误消息 |
| 其他 | 非阻止错误 | 忽略 | 详细模式 |
各钩子的退出码2
| 钩子 | 效果 |
|---|---|
| PreToolUse | 阻止工具,stderr给Claude |
| PostToolUse | stderr给Claude (工具已运行) |
| UserPromptSubmit | 阻止提示,仅stderr给用户 |
| Stop | 阻止停止,stderr给Claude |
Shell包装模式
#!/bin/bash
set -e
cd "$CLAUDE_PROJECT_DIR/.claude/hooks"
cat | npx tsx src/my-hook.ts
或用于打包:
#!/bin/bash
set -e
cd "$HOME/.claude/hooks"
cat | node dist/my-hook.mjs
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>;
// ... 其他字段按钩子类型
}
function readStdin(): string {
return readFileSync(0, 'utf-8');
}
async function main() {
const input: HookInput = JSON.parse(readStdin());
// 处理输入
const output = {
decision: 'block', // 或undefined以允许
reason: '阻止原因'
};
console.log(JSON.stringify(output));
}
main().catch(console.error);
测试钩子
手动测试命令
# PostToolUse (Write)
echo '{"tool_name":"Write","tool_input":{"file_path":"test.md"},"tool_response":{"success":true},"session_id":"test"}' | \
.claude/hooks/my-hook.sh
# PreToolUse (Bash)
echo '{"tool_name":"Bash","tool_input":{"command":"ls"},"session_id":"test"}' | \
.claude/hooks/my-hook.sh
# SessionStart
echo '{"hook_event_name":"SessionStart","source":"startup","session_id":"test"}' | \
.claude/hooks/session-start.sh
# SessionEnd
echo '{"hook_event_name":"SessionEnd","reason":"clear","session_id":"test"}' | \
.claude/hooks/session-end.sh
# UserPromptSubmit
echo '{"prompt":"测试提示","session_id":"test"}' | \
.claude/hooks/prompt-submit.sh
TypeScript编辑后重建
cd .claude/hooks
npx esbuild src/my-hook.ts \
--bundle --platform=node --format=esm \
--outfile=dist/my-hook.mjs
常见模式
阻止危险文件 (PreToolUse)
#!/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('{}')
自动格式化文件 (PostToolUse)
#!/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 '{}'
注入Git上下文 (UserPromptSubmit)
#!/bin/bash
echo "Git状态:"
git status --short 2>/dev/null || echo "(不是git仓库)"
echo ""
echo "最近提交:"
git log --oneline -5 2>/dev/null || echo "(无提交)"
强制测试验证 (Stop)
#!/usr/bin/env python3
import json, sys, subprocess
data = json.load(sys.stdin)
# 防止无限循环
if data.get('stop_hook_active'):
print('{}')
sys.exit(0)
# 检查测试是否通过
result = subprocess.run(['npm', 'test'], capture_output=True)
if result.returncode != 0:
print(json.dumps({
"decision": "block",
"reason": "测试失败。请在停止前修复。"
}))
else:
print('{}')
调试清单
- [ ] 钩子在settings.json中注册?
- [ ] Shell脚本有
+x权限? - [ ] TypeScript更改后重建打包?
- [ ] 使用
tool_response而不是tool_result? - [ ] 输出是有效的JSON (或纯文本)?
- [ ] Stop钩子中检查
stop_hook_active? - [ ] 使用
$CLAUDE_PROJECT_DIR作为路径?
过往会话的关键学习点
- 字段名称重要 -
tool_response而不是tool_result - 输出格式 -
decision: "block"+reason用于阻止 - 退出码2 - stderr给Claude/用户,stdout忽略
- 重建打包 - TypeScript源代码编辑不会自动应用
- 手动测试 - 依赖前先
echo '{}' | ./hook.sh - 先检查输出 - 编辑代码前先
ls .claude/cache/ - 分离生成隐藏错误 - 添加日志以调试
另见
/debug-hooks- 系统化调试工作流.claude/rules/hooks.md- 钩子开发规则