Tutorial | 构建自定义 Hook¶
难度:⭐⭐ 时间:~1h 前置:基本 shell 脚本 产物:自定义 hook 自动执行
1. Hook 是什么¶
Hook = Claude Code 生命周期事件的自动响应
- 9 种事件
- 3 种类型:command / prompt / agent
- 配置文件 settings.json
位置:
- 用户:~/.claude/settings.json
- 项目:.claude/settings.json
- Plugin:plugin 内
2. 9 种 Hook 事件¶
| 事件 | 触发时机 |
|---|---|
PreToolUse |
工具调用前 |
PostToolUse |
工具调用后 |
SessionStart |
会话开始 |
SessionEnd |
会话结束 |
Notification |
通知触发 |
Stop |
Claude 停止生成 |
SubagentStop |
sub-agent 停止 |
UserPromptSubmit |
用户提交 |
PreCompact |
压缩前 |
9 种。
3. 3 种 Hook 类型¶
3.1 command¶
执行 shell 脚本。
3.2 prompt¶
{
"type": "prompt",
"prompt": "Decide if this command is safe to run. Respond with 'yes' or 'no'."
}
LLM 评估。
3.3 agent¶
Agent 循环。
4. Hook 配置¶
4.1 基本结构¶
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/check-bash.sh",
"timeout": 10
}
]
}
]
}
}
4 字段:matcher / hooks / type / command|prompt|agent。
4.2 matcher¶
3 种。
4.3 timeout¶
超时。
5. 5 个实战 Hook¶
5.1 Hook 1: Bash 危险命令拦截¶
#!/bin/bash
# ~/.claude/hooks/block-dangerous-bash.sh
set -e
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# 危险模式
if echo "$COMMAND" | grep -qE 'rm -rf /|mkfs|dd if='; then
echo "BLOCKED: Dangerous command" >&2
exit 2
fi
exit 0
配置:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/block-dangerous-bash.sh", "timeout": 5 }
]
}
]
}
}
5.2 Hook 2: Edit 后自动格式化¶
#!/bin/bash
# ~/.claude/hooks/format-on-edit.sh
set -e
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
case "$FILE" in
*.ts|*.tsx|*.js|*.jsx)
npx prettier --write "$FILE" 2>/dev/null
;;
*.py)
black "$FILE" 2>/dev/null
;;
*.go)
gofmt -w "$FILE" 2>/dev/null
;;
esac
exit 0
自动格式化。
5.3 Hook 3: 写文件时 lint¶
#!/bin/bash
# ~/.claude/hooks/lint-on-write.sh
set -e
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
case "$FILE" in
*.ts|*.tsx)
npx eslint "$FILE" 2>&1 | head -50 >&2
;;
esac
exit 0
lint 警告。
5.4 Hook 4: Session 启动通知¶
#!/bin/bash
# ~/.claude/hooks/notify-session-start.sh
echo "Session started at $(date)" >> ~/.claude/session.log
exit 0
会话日志。
5.5 Hook 5: Compact 前备份¶
#!/bin/bash
# ~/.claude/hooks/backup-before-compact.sh
set -e
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // ""')
PROJECT=$(echo "$INPUT" | jq -r '.cwd // ""')
if [ -n "$SESSION_ID" ]; then
cp ~/.claude/projects/$PROJECT/$SESSION_ID.jsonl \
~/.claude/backups/$SESSION_ID-$(date +%s).jsonl 2>/dev/null
fi
exit 0
备份。
6. Hook 输入输出¶
6.1 command hook 输入¶
{
"session_id": "...",
"transcript_path": "...",
"cwd": "...",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": { "command": "ls" }
}
JSON via stdin。
6.2 command hook 输出¶
3 种退出码。
6.3 prompt hook 输出¶
JSON via stdout。
6.4 agent hook 输出¶
同 prompt。
7. 4 步调试 Hook¶
7.1 测试 hook 命令¶
# 模拟输入
echo '{"tool_name": "Bash", "tool_input": {"command": "ls"}}' | \
~/.claude/hooks/block-dangerous-bash.sh
echo $? # 0 = 允许
直接测。
7.2 启用 debug¶
debug 模式。
7.3 看日志¶
log 过滤。
7.4 简化¶
简化。
8. 5 个最佳实践¶
- 可幂等 —— 重跑结果一致
- 快速失败 —— 5-30s timeout
- 明确退出码 —— 0 / 2 / 其他
- 不阻塞主流程 —— async 友好
- 路径校验 —— 防止 ../ 跳出
5 条。
9. 安全考虑¶
9.1 不要 sudo¶
sudo 危险。
9.2 验证输入¶
校验。
9.3 不要泄露¶
不泄露。
9.4 路径约束¶
路径白名单。
10. 3 步安装 Hook¶
10.1 写脚本¶
mkdir -p ~/.claude/hooks
cat > ~/.claude/hooks/my-hook.sh <<'EOF'
#!/bin/bash
# ...
EOF
chmod +x ~/.claude/hooks/my-hook.sh
3 步。
10.2 编辑 settings¶
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/format-on-edit.sh" }
]
}
]
}
}
2 步。
10.3 重启¶
Claude Code 重新加载 settings。
11. 完整实战:Linter Hook¶
#!/bin/bash
# ~/.claude/hooks/lint.sh
set -e
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
LANGUAGE=$(echo "$INPUT" | jq -r '.tool_input.language // ""')
# 检查文件存在
[ -f "$FILE" ] || exit 0
case "$FILE" in
*.ts|*.tsx)
npx eslint --no-warn-ignored "$FILE" 2>&1 | head -20 >&2
;;
*.py)
ruff check "$FILE" 2>&1 | head -20 >&2
;;
*.go)
go vet "$FILE" 2>&1 | head -20 >&2
;;
*.sh)
shellcheck "$FILE" 2>&1 | head -20 >&2
;;
esac
exit 0
完整。
12. 下一步¶
- 写第一个 hook
- 加到 settings.json
- 测