关于 hook-developer
供开发者创建和排查 Claude Code 钩子的参考技能,涵盖事件结构、阻止行为、注册、钩子类型、测试及常见实现模式。
主要功能
- 01钩子事件的输入与输出结构
- 02阻止操作和注入上下文的行为
- 03钩子注册与匹配器模式
- 04命令式和提示词式钩子
- 05测试与调试指南
- 06常见钩子实现模式
适用场景
- 01按正确的事件结构编写钩子
- 02判断钩子能否阻止操作
- 03在 `settings.json` 配置事件匹配器
- 04排查钩子输出或注册问题
MCP 相关
说明如何在 Claude Code 钩子中识别和匹配 MCP 工具,包括 MCP 服务工具名称的匹配模式。
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ parcadei/ continuous-claude-v3 --skill hook-developer安装后在 Claude Code 中输入/hook-developer确认已加载。
详细介绍
钩子开发者
Claude Code 钩子开发的完整参考。使用本文档编写具有正确输入/输出架构的钩子。
适用场景
- 创建新钩子
- 调试钩子输入/输出格式
- 了解可用字段
- 在 settings.json 中设置钩子注册
- 了解钩子可以阻止什么,或注入什么上下文
快速参考
| 钩子 | 触发时机 | 能否阻止? | 主要用途 |
|---|---|---|---|
| PreToolUse | 工具执行前 | 是 | 阻止/修改工具调用 |
| PostToolUse | 工具完成后 | 部分可以 | 对工具结果作出响应 |
| UserPromptSubmit | 用户发送提示词时 | 是 | 验证/注入上下文 |
| PermissionRequest | 显示权限对话框时 | 是 | 自动批准/拒绝 |
| SessionStart | 会话开始时 | 否 | 加载上下文、设置环境变量 |
| SessionEnd | 会话结束时 | 否 | 清理/保存状态 |
| Stop | Agent 完成时 | 是 | 强制继续 |
| SubagentStart | 子 Agent 启动时 | 否 | 模式协调 |
| SubagentStop | 子 Agent 完成时 | 是 | 强制继续 |
| PreCompact | 压缩前 | 否 | 保存状态 |
| Notification | 发送通知时 | 否 | 自定义提醒 |
钩子类型选项: type: "command"(bash)或 type: "prompt"(LLM 评估)
钩子输入/输出架构
PreToolUse
用途: 在工具执行前阻止或修改其执行。
输入:
{
"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):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "string",
"updatedInput": {}
},
"continue": true,
"stopReason": "string",
"systemMessage": "string",
"suppressOutput": true
}
退出码 2: 阻止工具执行,并将 stderr 显示给 Claude。
常见匹配器: Bash、Edit|Write、Read、Task、mcp__.*
PostToolUse
用途: 对工具执行结果作出响应,并向 Claude 提供反馈。
输入:
{
"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):
{
"decision": "block",
"reason": "string",
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": "string"
},
"continue": true,
"stopReason": "string",
"suppressOutput": true
}
阻止: "decision": "block" 配合 "reason" 会提示 Claude 处理该问题。
常见匹配器: Edit|Write、Bash
UserPromptSubmit
用途: 验证用户提示词,并在 Claude 处理之前注入上下文。
输入:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "UserPromptSubmit",
"prompt": "string"
}
输出(纯文本):
Any stdout text is added to context for Claude.
输出(JSON):
{
"decision": "block",
"reason": "string",
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "string"
}
}
阻止: "decision": "block" 会清除提示词,并且只向用户显示 "reason"(Claude 看不到)。
退出码 2: 阻止提示词,并且只向用户显示 stderr。
PermissionRequest
用途: 自动处理权限对话框的决定。
输入:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "PermissionRequest",
"tool_name": "string",
"tool_input": {}
}
输出:
{
"hookSpecificOutput": {
"hookEventName": "PermissionRequest",
"decision": {
"behavior": "allow|deny",
"updatedInput": {},
"message": "string",
"interrupt": false
}
}
}
SessionStart
用途: 初始化会话、加载上下文、设置环境变量。
输入:
{
"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):
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "string"
},
"suppressOutput": true
}
标准输出中的纯文本会作为上下文添加。
SessionEnd
用途: 清理、保存状态、记录会话。
输入:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "SessionEnd",
"reason": "clear|logout|prompt_input_exit|other"
}
输出: 无法影响会话(会话已经结束)。仅用于清理。
Stop
用途: 控制 Claude 何时停止,并强制其继续。
输入:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "Stop",
"stop_hook_active": false
}
重要: 检查 stop_hook_active: true,以防止无限循环!
输出:
{
"decision": "block",
"reason": "string"
}
阻止: "decision": "block" 会强制 Claude 继续,并将 "reason" 作为提示词。
SubagentStart
用途: 子 Agent(Task 工具)启动时运行。
输入:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "SubagentStart",
"agent_id": "string"
}
输出: 仅注入上下文(无法阻止)。
SubagentStop
用途: 控制子 Agent(Task 工具)何时停止。
输入:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "SubagentStop",
"stop_hook_active": false
}
输出: 与 Stop 相同。
PreCompact
用途: 在上下文压缩前保存状态。
输入:
{
"session_id": "string",
"transcript_path": "string",
"cwd": "string",
"permission_mode": "string",
"hook_event_name": "PreCompact",
"trigger": "manual|auto",
"custom_instructions": "string"
}
匹配器: manual、auto
输出:
{
"continue": true,
"systemMessage": "string"
}
Notification
用途: 自定义通知处理。
输入:
{
"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、*
输出:
{
"continue": true,
"suppressOutput": true,
"systemMessage": "string"
}
在 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" }]
}
]
}
}
钩子类型
命令钩子(类型:"command")
默认类型。执行 bash 命令或脚本。
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/my-hook.sh",
"timeout": 60
}
基于提示词的钩子(类型:"prompt")
使用大模型(Haiku)进行上下文感知决策。最适用于 Stop/SubagentStop。
{
"type": "prompt",
"prompt": "Evaluate if Claude should stop. Context: $ARGUMENTS. Check if all tasks are complete.",
"timeout": 30
}
响应架构:
{
"decision": "approve" | "block",
"reason": "Explanation",
"continue": false,
"stopReason": "Message to user",
"systemMessage": "Warning"
}
MCP 工具命名
MCP 工具使用模式 mcp__<server>__<tool>:
| 模式 | 匹配内容 |
|---|---|
mcp__memory__.* | 所有 memory 服务工具 |
mcp__.*__write.* | 所有 MCP 写入工具 |
mcp__github__.* | 所有 GitHub 工具 |
环境变量
所有钩子均可使用
| 变量 | 说明 |
|---|---|
CLAUDE_PROJECT_DIR | 项目根目录的绝对路径 |
CLAUDE_CODE_REMOTE | 远程/Web 环境为 "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>;
// ... 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);
测试钩子
手动测试命令
# 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":"test 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"Blocked: {path} is protected"
}
}))
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 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)"
强制验证测试(Stop)
#!/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)
# 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('{}')
调试检查清单
- 是否已在 settings.json 中注册钩子?
- Shell 脚本是否具有
+x权限? - 修改 TS 后是否重新构建了打包文件?
- 是否使用了
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/ - 分离式 spawn 会隐藏错误 - 添加日志以便调试
另请参阅
/debug-hooks- 系统化调试工作流.claude/rules/hooks.md- 钩子开发规则