Skip to main content

前言

在使用Claude Code进行日常开发工作时,我们常常面临一些挑战:Claude有时会执行一些需要谨慎对待的操作(如删除文件、修改生产配置),而内置的权限提示并不总能覆盖所有自定义场景;同时,我们也希望在Claude完成某些操作后自动触发代码检查、测试运行、日志记录等流程。

Hooks正是为了解决这类问题而设计的。它是Claude Code提供的一套事件驱动钩子机制,允许开发者在智能体工作流的关键节点注入自定义逻辑,将原本需要人工干预或事后检查的步骤变成自动化、可编程的流程。

什么是Hooks

Claude Code Hooks

设计目标

Hooks的核心目标是让开发者对Claude Code的行为拥有精细的控制权,主要解决以下几类问题:

  • 安全控制:在Claude执行危险命令前进行拦截,例如阻止rm -rf等破坏性操作
  • 权限自动化:根据规则自动批准或拒绝权限请求,减少频繁的人工确认
  • 质量保障:在Claude修改文件后自动运行代码风格检查、单元测试等
  • 可观测性:记录每次工具调用日志、跟踪会话行为,满足审计需求
  • 上下文注入:在会话启动或提示词提交时自动注入环境信息、项目上下文

工作原理

Hooks基于事件驱动模型。当Claude Code运行到特定生命周期节点时(如即将调用某工具、刚完成某操作、会话结束等),会触发对应的事件。如果开发者为该事件配置了HookClaude Code会将事件的JSON上下文数据传递给Hook处理程序。处理程序可以根据数据做出决策,通过退出码和JSON输出告知Claude Code是否允许该操作继续执行。

整个处理流程如下:

四种处理程序类型

Hooks支持四种不同类型的处理程序,适用于不同复杂度的场景:

类型说明适用场景
command执行Shell命令或脚本命令验证、文件检查、日志记录
httpHTTP端点发送POST请求调用外部服务、远程审批系统
prompt调用LLM进行单轮评估需要语义理解的内容审查
agent启动一个具有工具访问权限的子智能体需要读取文件、检查代码的复杂验证

Hook生命周期

Claude Code Hook生命周期

事件总览

HooksClaude Code会话的不同阶段触发。以下是完整的事件列表:

事件名称触发时机支持阻断
SessionStart会话启动或恢复时
InstructionsLoaded加载CLAUDE.md或规则文件时
UserPromptSubmit用户提交提示词、Claude处理前
PreToolUse工具调用执行前
PermissionRequest出现权限确认对话框前
PostToolUse工具调用成功完成后否(可提供反馈)
PostToolUseFailure工具调用失败后否(可提供反馈)
NotificationClaude Code发送通知时
SubagentStart子智能体启动时
SubagentStop子智能体完成时
StopClaude完成当前响应时
TeammateIdle团队协作中某成员即将空闲时
TaskCompleted任务被标记为完成时
ConfigChange会话中配置文件发生变化时
WorktreeCreate创建Git Worktree是(替换默认行为)
WorktreeRemove移除Git Worktree
PreCompact执行上下文压缩前
SessionEnd会话结束时

生命周期顺序

Agentic Loop(智能体循环)中,Hook事件的触发顺序如下:

配置方式

配置文件位置

Hook可以定义在不同的配置文件中,生效范围各不相同:

配置文件生效范围可提交到代码仓库
~/.claude/settings.json所有项目否(仅本机)
.claude/settings.json单个项目
.claude/settings.local.json单个项目否(已.gitignore
企业托管策略配置组织全局是(管理员控制)
Plugin hooks/hooks.json插件启用时是(随插件分发)

配置结构

Hook配置采用三层嵌套结构:

{
"hooks": {
"<EventName>": [
{
"matcher": "<regex pattern>",
"hooks": [
{
"type": "command",
"command": "your-script.sh"
}
]
}
]
}
}
  • 第一层:选择要响应的事件,如PreToolUseStop
  • 第二层:配置匹配器(matcher),过滤何时触发
  • 第三层:定义一个或多个处理程序(Hook Handler

匹配器规则

matcher字段是一个正则表达式,用于过滤事件触发条件。不同事件类型匹配的字段不同:

事件匹配字段示例值
PreToolUsePostToolUsePermissionRequest工具名称BashEdit|Writemcp__.*
SessionStart启动方式startupresumeclearcompact
SessionEnd结束原因clearlogoutother
Notification通知类型permission_promptidle_prompt
SubagentStartSubagentStop智能体类型BashExplorePlan
ConfigChange配置来源user_settingsproject_settings

省略matcher或设置为"*"时,对所有事件触发。UserPromptSubmitStop等事件不支持matcher,每次都会触发。

处理程序公共字段

字段必填说明
type"command""http""prompt""agent"
timeout超时秒数,command默认600秒,prompt默认30秒,agent默认60
statusMessage执行时显示的自定义状态提示

command类型额外字段:

字段必填说明
command要执行的Shell命令
async设为true时后台运行,不阻塞Claude

Hook输入与输出

公共输入字段

所有Hook事件都会收到以下JSON数据(通过标准输入stdin传入):

字段说明
session_id当前会话标识符
transcript_path会话记录文件路径
cwd当前工作目录
permission_mode当前权限模式(defaultplanacceptEdits等)
hook_event_name触发的事件名称

退出码语义

Shell命令的退出码决定了Claude Code的后续行为:

退出码含义
0成功,解析stdout中的JSON输出
2阻断性错误:stderr内容反馈给Claude,触发阻断行为
其他非零非阻断错误:仅在verbose模式显示stderr,继续执行

JSON输出字段

退出码0时,可在stdout输出JSON进行精细控制:

字段默认值说明
continuetrue设为false时强制停止Claude处理
stopReasoncontinuefalse时展示给用户的消息
suppressOutputfalse设为true时隐藏verbose模式的输出
systemMessage展示给用户的警告消息

各事件决策控制

不同事件支持不同的决策控制方式:

事件决策格式可用值
PreToolUsehookSpecificOutput.permissionDecisionallow/deny/ask
PermissionRequesthookSpecificOutput.decision.behaviorallow/deny
UserPromptSubmitPostToolUseStop顶层decision"block"
WorktreeCreatestdout输出路径worktree的绝对路径

使用示例

阻止危险Shell命令

PreToolUse中拦截包含rm -rf的命令,防止Claude意外删除文件。

.claude/hooks/block-dangerous.sh中创建脚本:

#!/bin/bash
# 从 stdin 读取 JSON 输入,检查命令是否危险
COMMAND=$(cat | jq -r '.tool_input.command // empty')

if echo "$COMMAND" | grep -qE 'rm\s+-rf|rm\s+--no-preserve-root'; then
echo "Blocked: destructive command detected: $COMMAND" >&2
exit 2 # 退出码2触发阻断,stderr内容反馈给Claude
fi

exit 0 # 允许执行

.claude/settings.json中配置:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-dangerous.sh"
}
]
}
]
}
}

文件修改后自动执行代码检查

每次Claude写入或编辑文件后,自动运行代码风格检查,并将结果反馈给Claude

.claude/hooks/lint-check.sh中创建脚本:

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

# 只检查 TypeScript/JavaScript 文件
if [[ "$FILE_PATH" != *.ts && "$FILE_PATH" != *.js ]]; then
exit 0
fi

# 运行 ESLint
RESULT=$(npx eslint "$FILE_PATH" 2>&1)
EXIT_CODE=$?

if [ $EXIT_CODE -ne 0 ]; then
# decision: "block" 让 Claude 收到反馈并修正代码
echo "{\"decision\": \"block\", \"reason\": \"ESLint check failed:\\n$RESULT\"}"
exit 0
fi

exit 0

配置:

{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/lint-check.sh"
}
]
}
]
}
}

会话启动时注入环境上下文

在每次新会话启动时,自动将当前Git分支、最近提交、未提交变更等信息注入Claude的上下文中。

.claude/hooks/session-context.sh中创建脚本:

#!/bin/bash
BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown")
LAST_COMMIT=$(git log -1 --pretty=format:"%h %s" 2>/dev/null || echo "none")
DIRTY=$(git status --short 2>/dev/null | head -5)

# 通过 hookSpecificOutput 注入 Claude 可见的上下文
cat <<EOF
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "Current branch: $BRANCH\nLast commit: $LAST_COMMIT\nUncommitted changes:\n$DIRTY"
}
}
EOF
exit 0

配置(仅对新会话启动触发):

{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/session-context.sh"
}
]
}
]
}
}

自动评估任务是否真正完成

使用Prompt Hook,在Claude完成响应前,让LLM评估任务是否真正完成:

{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "You are evaluating whether Claude should stop working. Context: $ARGUMENTS\n\nCheck: 1) Are all user-requested tasks complete? 2) Are there any unresolved errors? 3) Is follow-up work needed?\n\nRespond with JSON: {\"ok\": true} to allow stopping, or {\"ok\": false, \"reason\": \"explanation\"} to continue.",
"timeout": 30
}
]
}
]
}
}

异步运行测试套件

文件修改后,在后台异步运行测试,不阻塞Claude继续工作,测试完成后将结果反馈到下一次对话轮次。

.claude/hooks/run-tests-async.sh中创建脚本:

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

# 仅对源文件运行测试
if [[ "$FILE_PATH" != *.go ]]; then
exit 0
fi

RESULT=$(go test ./... 2>&1)
EXIT_CODE=$?

if [ $EXIT_CODE -eq 0 ]; then
echo "{\"systemMessage\": \"All tests passed after editing $FILE_PATH\"}"
else
echo "{\"systemMessage\": \"Tests FAILED after editing $FILE_PATH:\\n$RESULT\"}"
fi

配置时设置async: true

{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/run-tests-async.sh",
"async": true,
"timeout": 300
}
]
}
]
}
}

审计所有配置变更

记录会话中发生的所有配置文件变更,用于安全审计:

{
"hooks": {
"ConfigChange": [
{
"hooks": [
{
"type": "command",
"command": "bash -c 'echo \"[$(date)] Config changed: $(cat | jq -r .source) - $(cat | jq -r .file_path // empty)\" >> ~/claude-audit.log'"
}
]
}
]
}
}

管理与调试

使用/hooks菜单

Claude Code中输入/hooks可打开交互式Hook管理界面,无需手动编辑JSON配置文件即可查看、添加和删除Hook。菜单中每个Hook会标注来源:

  • [User]:来自~/.claude/settings.json
  • [Project]:来自.claude/settings.json
  • [Local]:来自.claude/settings.local.json
  • [Plugin]:来自插件,只读

调试方法

使用--debug参数启动Claude Code可查看详细的Hook执行日志:

claude --debug

输出示例:

[DEBUG] Executing hooks for PostToolUse:Write
[DEBUG] Found 1 hook matchers in settings
[DEBUG] Matched 1 hooks for query "Write"
[DEBUG] Executing hook command: .claude/hooks/lint-check.sh with timeout 600000ms
[DEBUG] Hook command completed with status 0: <stdout output>

在交互模式下,按Ctrl+O可切换verbose模式,实时查看Hook的执行进度。

禁用Hooks

临时禁用所有Hook而不删除配置,可在settings.json中添加:

{
"disableAllHooks": true
}

注意:通过托管策略配置的企业Hook不受此设置影响,只有在托管策略层级设置disableAllHooks才能禁用。

安全注意事项

Hook命令以当前用户的完整权限运行,编写时需遵守以下安全实践:

  • 始终验证和清理输入:不要盲目信任stdin中的JSON数据
  • 必须引用Shell变量:使用"$VAR"而非$VAR,防止路径和命令注入
  • 阻止路径穿越:检查文件路径中是否存在..
  • 使用绝对路径:脚本引用使用完整路径,项目根目录用"$CLAUDE_PROJECT_DIR"
  • 避免处理敏感文件:跳过.env.git/、密钥文件等

此外,Claude Code在启动时会对Hook配置拍摄快照,会话中途对配置文件的外部修改不会立即生效——需要通过/hooks菜单审核后才会应用,以防止恶意Hook注入。

参考资料