跳转至

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

{
  "type": "command",
  "command": "/path/to/script.sh",
  "timeout": 30
}

执行 shell 脚本

3.2 prompt

{
  "type": "prompt",
  "prompt": "Decide if this command is safe to run. Respond with 'yes' or 'no'."
}

LLM 评估

3.3 agent

{
  "type": "agent",
  "agent": "reviewer",
  "prompt": "Review this code change"
}

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

"matcher": "Bash"           // 单个
"matcher": "Bash|Edit|Write"  // 多个
"matcher": "*"                // 所有

3 种

4.3 timeout

"timeout": 30  // 秒

超时


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 输出

# exit 0  —— 允许
# exit 2  —— 阻止 + stderr message
# 其他非 0 —— 错误但不阻止

3 种退出码

6.3 prompt hook 输出

{
  "decision": "approve" | "block",
  "reason": "..."
}

JSON via stdout

6.4 agent hook 输出

{
  "decision": "approve" | "block",
  "reason": "..."
}

同 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

claude --debug hooks

debug 模式

7.3 看日志

tail -f ~/.claude/logs/<date>.jsonl | grep hook

log 过滤

7.4 简化

# 把 hook 简化到最小,确认问题

简化


8. 5 个最佳实践

  1. 可幂等 —— 重跑结果一致
  2. 快速失败 —— 5-30s timeout
  3. 明确退出码 —— 0 / 2 / 其他
  4. 不阻塞主流程 —— async 友好
  5. 路径校验 —— 防止 ../ 跳出

5 条


9. 安全考虑

9.1 不要 sudo

# ❌ 永远不要
sudo ~/.claude/hooks/foo.sh

sudo 危险

9.2 验证输入

# 验证 JSON
echo "$INPUT" | jq . > /dev/null || exit 1

校验

9.3 不要泄露

# ❌ 输出敏感信息
echo "$INPUT"  # 含 API key?

# ✅ 过滤
echo "$INPUT" | jq 'del(.tool_input.password)'

不泄露

9.4 路径约束

# 只在 ~/.claude/ 操作
case "$FILE" in
  ~/.claude/*) ;;
  *) exit 1 ;;
esac

路径白名单


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