Claude Code Hooks 配置深度解析:把 Agent 工作流变成可治理的自动化系统

从事件生命周期、配置层级到安全治理,系统拆解 Claude Code Hooks 的工程化用法

Posted by iceyao on Wednesday, May 6, 2026

一、引言:Hooks 让 Claude Code 从“会调用工具”变成“可治理的工作流”

原文链接:Claude Code power user customization: How to configure hooks
来源:Claude Blog / Anthropic
发布时间:2025 年 12 月 11 日

Claude Code 的核心能力是让模型在代码仓库里调用工具:读文件、写文件、运行命令、搜索代码、启动子代理、根据反馈继续修改。但如果只有“模型决定何时调用工具”,团队很快会遇到三个现实问题:

  1. 重复动作太多:每次改完文件都要格式化、lint、记录变更,靠人提醒很容易漏。
  2. 安全边界太软:模型可能提出删除文件、访问敏感配置、运行高风险命令,单靠权限弹窗很难形成团队规范。
  3. 上下文注入太手工:每次会话都要告诉 Claude 当前分支、TODO、Sprint 背景、最近失败的测试,非常低效。

Hooks 的价值就在这里:它把 Claude Code 生命周期中的关键节点暴露出来,让开发者可以在这些节点上自动运行本地命令或让 Claude 执行一段检查 prompt。换句话说,Hooks 不是“给 Claude 增加一个工具”,而是给整个 Agent 工作流增加可编排、可拦截、可审计的控制面

先看一张总览图:

Claude Code Hooks 架构总览

图注:本文配图均为作者基于 Anthropic 原文机制整理的概念示意,并非官方架构图。

本文会围绕六个问题展开:

  • Hooks 到底是什么,它和工具调用、权限系统是什么关系?
  • 8 类 Hook 事件分别适合解决什么问题?
  • .claude/settings.json 应该怎样配置?
  • matcher、stdin JSON、exit code、结构化 JSON 响应如何协同工作?
  • 如何把 Hooks 用于格式化、权限治理、上下文注入和任务完成检查?
  • 团队落地时如何避免把 Hooks 变成新的安全风险?

二、核心概念:Hook 是 Claude Code 生命周期上的自动触发器

可以把 Claude Code 的一次工作流拆成三层:

层次 作用 典型例子
模型推理层 Claude 根据上下文决定下一步做什么 计划修改文件、决定运行测试
工具执行层 Claude Code 执行具体工具 ReadWriteEditBash、MCP 工具
Hook 控制层 在关键事件前后自动运行规则 写文件后格式化、执行命令前校验、会话开始注入上下文

Hook 是一段由用户配置的本地命令或 prompt。它通常具备这些特征:

  • 写在 Claude Code 的 settings JSON 文件里;
  • 挂载到某个事件,如 PreToolUsePostToolUseSessionStart
  • 可通过 matcher 过滤具体工具或命令;
  • 通过 stdin 接收事件相关 JSON;
  • 通过 stdout、stderr、exit code 或结构化 JSON 返回结果;
  • 在本地环境中执行,拥有当前用户权限。

一句话概括:工具是 Claude 想做的事,Hook 是你在 Claude 做事前后加上的规则、自动化和护栏。

2.1 三类典型收益

原文把 Hooks 的价值归纳为三类,工程上也基本可以按这三类来设计。

目标 典型 Hook 示例
自动化重复任务 PostToolUse Claude 写完文件后自动运行 Prettier、Black、gofmt
强制项目规则 PreToolUsePermissionRequest 阻止修改 .env、拒绝 git push --force、限制写入目录
动态注入上下文 SessionStartUserPromptSubmit 会话开始时注入 git 状态、TODO、Sprint 背景

这三类收益背后是同一个原则:不要只依赖模型“记得做正确的事”,而要把确定性规则沉到 Hook 层。

2.2 Hooks 与权限弹窗的关系

Claude Code 原本就有权限确认机制,例如运行某些 Bash 命令前会询问用户。PermissionRequest Hook 的意义不是绕过安全,而是把“每次手工点确认”改造成“可审计的策略判断”。

例如:

  • npm testgo test ./... 这类低风险测试命令可以自动批准;
  • rm -rfgit push --force、读取 secrets 的命令必须自动拒绝;
  • 介于两者之间的操作仍然交给用户确认。

这比单纯依赖弹窗更工程化,因为策略可以进入版本控制、代码评审和团队规范。


三、事件生命周期:8 类 Hook 分别卡在哪个位置

Claude Code Hooks 的事件不是随机设计的,而是覆盖了 Agent 工作流的几个关键阶段:会话开始、用户提交、工具执行前、权限请求、工具执行后、上下文压缩前、Claude 停止前、子代理停止前。

Claude Code Hook 事件生命周期

3.1 事件总览表

Hook 类型 触发时机 最适合做什么
SessionStart 新会话开始或恢复已有会话时 注入 git 状态、TODO、环境说明
UserPromptSubmit 用户提交 prompt 后、Claude 处理前 追加 Sprint 上下文、最近错误、请求校验
PreToolUse Claude 决定调用工具后、工具实际执行前 拦截危险操作、校验路径、修改工具输入
PermissionRequest 权限确认弹窗出现前 自动批准低风险命令、拒绝高风险命令
PostToolUse 工具成功执行后 自动格式化、lint、记录变更、通知
PreCompact 上下文压缩前 备份 transcript、保存关键决策
Stop Claude 完成响应、准备等待用户输入时 自检任务是否完成、要求继续处理遗漏项
SubagentStop 子代理完成任务时 校验子代理输出、触发后续处理

3.2 PreToolUse:在工具真正执行前加一道闸门

PreToolUse 是最像“安全网关”的事件。它发生在 Claude 已经决定使用某个工具之后,但工具还没有执行之前。

适合场景:

  • 阻止写入敏感文件;
  • 限制只能修改某些目录;
  • 拦截危险 Bash 命令;
  • 检查 MCP 工具参数是否符合团队规范。

配置骨架如下:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/validate-file-path.sh"
          }
        ]
      }
    ]
  }
}

这个 Hook 的直觉是:文件还没被改,命令还没被跑,此时阻止成本最低。

3.3 PermissionRequest:把重复授权变成策略判断

PermissionRequest 在 Claude Code 准备弹出权限确认框之前触发。它特别适合处理“每次都问很烦,但完全放开又不安全”的中间地带。

例如团队可以规定:

  • 自动批准 npm testnpm run lintgo test ./...
  • 自动拒绝包含 --force、访问 .env、修改生产配置的命令;
  • 其他命令继续走默认权限确认。
{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "Bash(npm test*)",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/approve-safe-test.sh"
          }
        ]
      }
    ]
  }
}

注意:自动批准不是为了省掉安全,而是为了让安全策略更精确。最危险的配置不是“严格拦截”,而是为了少点几次确认就把权限边界整体放宽。

3.4 PostToolUse:在变更发生后立即收尾

PostToolUse 在工具成功执行后触发,是最适合入门的 Hook。常见用法是写文件后自动格式化:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/format-touched-file.sh"
          }
        ]
      }
    ]
  }
}

为什么推荐从它开始?因为反馈非常直观:Claude 改了文件,Hook 立刻运行格式化器,diff 变干净,失败也容易定位。

3.5 SessionStartUserPromptSubmit:自动注入动态上下文

Claude Code 会话经常需要项目状态。例如当前分支、未提交变更、最近失败的测试、团队 TODO。如果每次都靠用户手工粘贴,既浪费时间,也容易遗漏。

SessionStart 适合注入“会话级背景”:

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/session-context.sh"
          }
        ]
      }
    ]
  }
}

UserPromptSubmit 适合注入“每轮 prompt 前都可能变化的信息”:

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "cat ./current-sprint-context.md 2>/dev/null || true"
          }
        ]
      }
    ]
  }
}

两者的关键差异是频率:SessionStart 更低频,UserPromptSubmit 每轮都会触发,因此后者必须控制输出长度,避免把上下文窗口塞满。

3.6 PreCompact:在上下文压缩前留档

长会话会触发 compaction。压缩能让会话继续运行,但也可能丢失细节。PreCompact 的价值是:在压缩前把关键状态保存下来。

适合保存:

  • 当前任务目标;
  • 已做决策;
  • 未解决问题;
  • transcript 路径;
  • 关键 diff 或测试结果摘要。
{
  "hooks": {
    "PreCompact": [
      {
        "matcher": "auto",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/backup-before-compact.sh"
          }
        ]
      }
    ]
  }
}

3.7 StopSubagentStop:把“完成”变成可检查状态

Stop 在 Claude 准备结束本轮响应时触发。它可以是 command,也可以是 prompt。一个很实用的模式是让 Claude 在停止前自检:

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Review the user's original request and the work completed in this turn. If all requirements are met, respond with complete. If work remains, respond with continue and list the missing items."
          }
        ]
      }
    ]
  }
}

SubagentStop 类似,但作用对象是子代理。它适合用于多代理工作流,例如让主代理在接收子代理结果前检查:子代理是否真的读了目标文件、是否给出了可验证证据、是否遗漏边界情况。


四、配置文件与优先级:团队规则、个人偏好、本地实验要分层

Hooks 配在 Claude Code 的 settings JSON 文件里。常见位置有三类:

Claude Code Hooks 配置层级

配置位置 适用范围 是否适合提交 推荐用途
.claude/settings.json 当前项目 团队共享规则、安全护栏、项目固定自动化
.claude/settings.local.json 当前项目的本地配置 个人实验、机器相关路径、临时 Hook
~/.claude/settings.json 当前用户所有项目 个人通用偏好、跨项目脚本

原文提到项目级设置优先于用户级设置,组织还可能通过企业托管策略做更高层级控制。工程实践上建议遵循一个简单原则:

团队必须一致的规则放项目级;个人效率偏好放用户级;不想进入版本控制的本机路径放 local。

4.1 最小可运行配置

一个 Hook 配置的基本结构如下:

{
  "hooks": {
    "事件名": [
      {
        "matcher": "匹配规则",
        "hooks": [
          {
            "type": "command",
            "command": "要执行的命令"
          }
        ]
      }
    ]
  }
}

例如,写文件或编辑文件后自动格式化:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/format-touched-file.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

字段解释:

字段 说明
hooks 顶层 Hooks 配置对象
PostToolUse 事件名,也可以是其他 7 类事件
matcher 只对工具相关事件生效,用于过滤工具或命令
type command 表示运行本地命令,prompt 表示让 Claude 执行检查提示
command 要执行的 shell 命令或脚本路径
prompt type: prompt 时使用的检查提示
timeout 单个 Hook 超时时间;默认超时为 60 秒

4.2 建议的项目目录结构

为了让 Hooks 可维护,不建议把复杂 shell 逻辑全部塞进 JSON 字符串。更推荐这样组织:

.claude/
  settings.json
  hooks/
    format-touched-file.sh
    validate-file-path.sh
    approve-safe-test.sh
    session-context.sh

这样做有三个好处:

  1. settings 文件只描述“什么时候触发什么脚本”;
  2. 脚本可以单独测试、审查和复用;
  3. shell quoting、日志、错误处理不会把 JSON 配置搅乱。

五、Matcher 语法:Hook 是否触发,先看匹配范围

matcher 只适用于三类工具相关事件:

  • PreToolUse
  • PostToolUse
  • PermissionRequest

它决定某个 Hook 是否应该被触发。

写法 含义 示例用途
"Write" 精确匹配 Write 工具 只拦截新文件写入
`“Write Edit”` 匹配多个工具
"*" 匹配所有工具 全量记录审计日志
"" 空字符串也可表示匹配全部 简单全量 Hook
"Bash(npm test*)" 匹配 Bash 命令参数 自动批准测试命令
"mcp__memory__.*" 匹配 MCP 工具 约束某类 MCP server 的调用

5.1 匹配工具名

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/format-touched-file.sh"
          }
        ]
      }
    ]
  }
}

这个配置只会在 Claude 使用 WriteEdit 后触发,不会因为 ReadGrepTask 等工具触发。

5.2 匹配 Bash 命令

{
  "hooks": {
    "PermissionRequest": [
      {
        "matcher": "Bash(npm test*)",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/approve-safe-test.sh"
          }
        ]
      }
    ]
  }
}

这个配置匹配以 npm test 开头的 Bash 命令。适合自动批准低风险测试命令,但不应该扩展成 Bash(*) 后无差别批准。

5.3 Matcher 的两个坑

第一,matcher 区分大小写。"bash" 不会匹配 "Bash"

第二,matcher 越宽,Hook 越容易频繁触发。全量 Hook 不是不能用,但最好只用于轻量审计或只读上下文注入;涉及写文件、网络请求、耗时检查的 Hook 应尽量收窄范围。


六、输入与输出协议:stdin JSON、exit code 和结构化响应

Hook 不是盲目运行脚本。Claude Code 会通过 stdin 把事件上下文传给 Hook,Hook 再通过退出码、stdout/stderr 或 JSON 告诉 Claude Code 下一步怎么处理。

6.1 Hook 会收到什么输入

一个工具相关 Hook 的输入大致类似这样:

{
  "session_id": "session_xxx",
  "transcript_path": "/Users/me/.claude/projects/demo/session.jsonl",
  "cwd": "/Users/me/project",
  "permission_mode": "default",
  "hook_event_name": "PreToolUse",
  "tool_name": "Write",
  "tool_input": {
    "file_path": "/Users/me/project/src/app.ts",
    "content": "..."
  }
}

常见字段可以分两类:

字段 说明
session_id 当前 Claude Code 会话 ID
transcript_path 会话 transcript 文件路径
cwd 当前工作目录
permission_mode 当前权限模式
hook_event_name 当前 Hook 事件名
tool_name 工具名称,工具相关事件才有
tool_input 工具输入参数,工具相关事件才有

脚本里应该把 stdin 当成不可信输入解析。即使这个 JSON 来自本地 Claude Code,也不要把其中字段直接拼接进 shell 命令。

6.2 Exit code 的语义

Exit code 语义 典型用途
0 成功 Hook 通过,stdout 被处理或加入上下文
2 阻塞错误 阻止当前操作,stderr 作为错误信息
其他 非阻塞错误 通常只在 verbose 模式显示,用于记录 Hook 自身失败

例如,阻止写入敏感文件可以这样写:

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')

if [ -z "$FILE_PATH" ]; then
  exit 0
fi

case "$FILE_PATH" in
  *.env|*.env.*|*secrets*|*.pem|*.key|*production*.yaml|*production*.yml)
    printf 'Blocked: sensitive file path is not allowed for Claude Code Hooks policy: %s\n' "$FILE_PATH" >&2
    exit 2
    ;;
  *)
    exit 0
    ;;
esac

这个脚本的关键点不是“识别所有危险文件”,而是展示 Hook 的阻断模型:发现违反策略时向 stderr 写清原因,并以 2 退出。

6.3 结构化 JSON 响应

除了 exit code,Hook 还可以输出结构化 JSON,表达更细的决策。以权限请求为例:

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
COMMAND=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty')

case "$COMMAND" in
  "npm test"|"npm test "*|"npm run test"|"npm run test "*)
    printf '{"decision":"approve","reason":"Project policy allows npm test commands."}\n'
    ;;
  *"--force"*|*".env"*|*"secrets"*)
    printf '{"decision":"deny","reason":"Command touches high-risk flags or sensitive files."}\n'
    ;;
  *)
    printf '{"decision":"allow","reason":"No automatic decision; continue with default permission flow."}\n'
    ;;
esac

这里有一个重要区别:

  • approve 更像“直接批准”;
  • deny 更像“直接拒绝”;
  • allow 可以理解为“不由 Hook 阻止,继续默认流程”。

实际可用字段和行为应以 Claude Code 当前版本文档为准,但工程思想很稳定:把策略结果显式化,而不是只靠脚本成功或失败。


七、配置步骤:从一个自动格式化 Hook 开始

如果团队第一次使用 Hooks,不建议一上来就做复杂权限系统。最稳妥的路径是:先用 PostToolUse 做一个低风险、反馈明确的自动格式化 Hook。

7.1 第一步:创建项目级 settings

在项目根目录创建 .claude/settings.json

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/format-touched-file.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

如果这是团队共享规则,可以提交到版本控制。如果只是个人实验,放在 .claude/settings.local.json 更合适。

7.2 第二步:编写格式化脚本

.claude/hooks/format-touched-file.sh

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')

if [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ]; then
  exit 0
fi

case "$FILE_PATH" in
  *.js|*.jsx|*.ts|*.tsx|*.json|*.css|*.md)
    npx prettier --write "$FILE_PATH"
    ;;
  *.py)
    python -m black "$FILE_PATH"
    ;;
  *.go)
    gofmt -w "$FILE_PATH"
    ;;
  *)
    exit 0
    ;;
esac

这个脚本有几个细节值得注意:

  • 文件路径从 stdin JSON 中读取;
  • 所有变量都用双引号包裹;
  • 只对明确支持的文件类型运行格式化器;
  • 不把 content 之类的大字段写入日志;
  • 不对未知文件类型做任何事。

7.3 第三步:在 /hooks 中审核配置

原文提到,Claude Code 对 Hook 配置文件的直接编辑有保护机制:需要在 /hooks 菜单中审核后才会生效。这一点很重要,因为 Hooks 本质上能执行任意本地命令。如果恶意代码可以静默写入 Hook 配置,就等于获得了隐蔽执行入口。

所以,团队流程建议是:

  1. Hook 配置进入代码评审;
  2. Hook 脚本必须可读、短小、职责单一;
  3. 本地启用前通过 /hooks 审核;
  4. 首次启用时打开 verbose 或脚本日志观察行为;
  5. 确认无误后再推广到团队。

7.4 第四步:逐步增加策略 Hook

自动格式化稳定后,再增加安全类 Hook:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/validate-file-path.sh"
          }
        ]
      }
    ],
    "PermissionRequest": [
      {
        "matcher": "Bash(npm test*)",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/approve-safe-test.sh"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/format-touched-file.sh",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

从这里开始,Hooks 就不只是“自动化小工具”,而是一套轻量治理框架了。


八、四个典型应用场景

8.1 场景一:写文件后自动格式化

这是最推荐的入门场景。它的链路很短:

  1. Claude 使用 WriteEdit
  2. PostToolUse 触发;
  3. Hook 从 stdin 读取目标文件路径;
  4. 根据扩展名运行对应格式化器;
  5. 格式化结果进入工作区 diff。

适用项目:

  • 前端项目:Prettier;
  • Python 项目:Black、Ruff;
  • Go 项目:gofmt;
  • Rust 项目:rustfmt。

实践建议:

  • 只格式化被 Claude 触碰的文件,不要全仓库格式化;
  • formatter 必须是项目已有工具,不要让 Hook 临时引入新依赖;
  • 格式化失败时不要阻断所有工作,先记录日志,再决定是否升级为强制策略。

8.2 场景二:阻止修改敏感文件

敏感文件保护适合放在 PreToolUse,因为此时文件还没被修改。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/block-sensitive-files.sh"
          }
        ]
      }
    ]
  }
}

策略脚本可以从简单黑名单开始:

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')

case "$FILE_PATH" in
  *.env|*.env.*|*credentials*|*secrets*|*.pem|*.key|*production*)
    printf 'Claude Code is not allowed to modify sensitive file: %s\n' "$FILE_PATH" >&2
    exit 2
    ;;
  *)
    exit 0
    ;;
esac

更成熟的团队可以把策略升级成白名单:只允许 Claude 修改 src/tests/docs/,禁止修改部署、证书、生产配置和 IaC 目录。

8.3 场景三:会话开始时注入项目状态

SessionStart 很适合解决“Claude 刚进入项目时不知道当前状态”的问题。

#!/usr/bin/env bash
set -euo pipefail

echo "## Current repository state"
echo

echo "### Git branch"
git branch --show-current 2>/dev/null || true

echo
echo "### Git status"
git status --short 2>/dev/null || true

echo
echo "### Project TODO"
if [ -f TODO.md ]; then
  sed -n '1,80p' TODO.md
else
  echo "No TODO.md found."
fi

这类 Hook 的目标不是把整个项目文档塞给 Claude,而是提供一段短而有用的启动上下文。建议限制输出长度,避免污染上下文窗口。

8.4 场景四:停止前检查任务是否真正完成

Stop 的 prompt Hook 可以把“完成标准”前置到 Claude 的停止动作之前。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "prompt",
            "prompt": "Before stopping, verify whether the user's request is fully satisfied. Check for unfinished todos, missing verification, and unresolved errors. If work remains, continue and explain the next concrete step."
          }
        ]
      }
    ]
  }
}

这个模式尤其适合长任务,例如:

  • 多文件重构;
  • 修复测试失败;
  • 生成文档并配套图表;
  • 多代理协作后汇总结果。

它不能替代真实测试,但能减少“Claude 过早宣布完成”的概率。


九、安全边界:Hooks 的能力越强,越需要工程化护栏

Hooks 最大的价值是可以运行本地命令;最大风险也正是可以运行本地命令。它们以当前用户权限执行,理论上可以读取、修改、删除用户有权限访问的文件,也可以访问环境变量和网络。

Claude Code Hooks 安全护栏

9.1 不要把 Hook 当成普通配置

普通配置写错,最多功能不生效。Hook 写错,可能导致:

  • 每次 Claude 写文件都运行错误脚本;
  • 敏感文件路径被写入日志;
  • 误删临时目录或构建产物;
  • 自动批准了本应人工确认的命令;
  • 把本地环境变量暴露给不该看到的流程。

因此,Hook 配置应该像 CI/CD 脚本、部署脚本一样被审查。

9.2 处理 stdin 时按“不可信输入”对待

反例是这样:

COMMAND=$(cat | jq -r '.tool_input.command')
eval "$COMMAND"

这等于把工具输入重新执行了一遍,风险极高。更安全的做法是:

  • 只读取必要字段;
  • 用白名单判断允许动作;
  • 永远引用变量;
  • 不使用 eval
  • 不把大段输入直接写入日志;
  • 对路径做敏感模式检查。

9.3 使用绝对路径或项目内固定路径

Hook 命令最好指向明确脚本,而不是依赖复杂 shell 解析:

{
  "type": "command",
  "command": ".claude/hooks/validate-file-path.sh"
}

如果是用户级 Hook,建议使用绝对路径,例如:

{
  "type": "command",
  "command": "/Users/me/.claude/hooks/global-audit-log.sh"
}

这样可以减少 PATH、当前目录、同名脚本带来的不确定性。

9.4 自动批准必须保守

适合自动批准的命令:

  • 只读命令,如 git statusgit diff
  • 测试命令,如 npm testgo test ./...
  • 明确范围内的 lint/format 命令。

不适合自动批准的命令:

  • 删除文件或目录;
  • 修改权限;
  • 访问 secrets;
  • 生产部署;
  • force push;
  • 修改全局系统配置;
  • 下载并执行远程脚本。

一个好策略是:自动批准低风险、自动拒绝高风险,中间地带继续询问用户。

9.5 团队共享 Hook 必须可解释

每个共享 Hook 至少应该回答四个问题:

  1. 触发时机是什么?
  2. 它读取了哪些输入?
  3. 它可能修改什么?
  4. 它失败时会阻塞还是只记录?

如果一个 Hook 无法用几句话解释清楚,就不应该直接进入团队共享配置。


十、调试与观测:看清 Hook 为什么触发、为什么阻止

Hook 一旦变多,最常见的问题不是“写不出来”,而是“不知道它为什么触发”。因此调试能力要从第一天就设计进去。

Claude Code Hooks 故障排查流程

10.1 使用 transcript 定位事件上下文

Claude Code 会维护 transcript,Hook 输入中包含 transcript_path。可以把路径记录下来,方便事后排查。

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/log-session.sh"
          }
        ]
      }
    ]
  }
}

示例脚本:

#!/usr/bin/env bash
set -euo pipefail

INPUT=$(cat)
TRANSCRIPT=$(printf '%s' "$INPUT" | jq -r '.transcript_path // empty')
CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // empty')

mkdir -p "$HOME/.claude/hook-logs"
printf '%s\t%s\t%s\n' "$(date -u +%FT%TZ)" "$CWD" "$TRANSCRIPT" >> "$HOME/.claude/hook-logs/sessions.tsv"

注意不要把完整 transcript 内容自动复制到共享位置,因为里面可能包含用户输入、代码片段和敏感上下文。

10.2 给 Hook 加轻量日志 wrapper

复杂 Hook 可以加统一日志 wrapper,记录事件名、工具名、决策结果和 exit code。

#!/usr/bin/env bash
set -euo pipefail

LOG_DIR="$HOME/.claude/hook-logs"
mkdir -p "$LOG_DIR"

INPUT=$(cat)
EVENT=$(printf '%s' "$INPUT" | jq -r '.hook_event_name // "n/a"')
TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // "n/a"')

printf '%s event=%s tool=%s start\n' "$(date -u +%FT%TZ)" "$EVENT" "$TOOL" >> "$LOG_DIR/hooks.log"

set +e
printf '%s' "$INPUT" | .claude/hooks/real-hook.sh
STATUS=$?
set -e

printf '%s event=%s tool=%s exit=%s\n' "$(date -u +%FT%TZ)" "$EVENT" "$TOOL" "$STATUS" >> "$LOG_DIR/hooks.log"
exit "$STATUS"

日志建议只记录摘要,不记录完整 tool_input.content,否则可能把源码或 secrets 写入日志。

10.3 常见故障定位表

现象 可能原因 排查方法
Hook 完全不触发 事件名写错、matcher 不匹配、配置未审核 检查 /hooks,确认 Write/Bash 大小写
Hook 触发太频繁 matcher 过宽 * 改成具体工具名
命令执行失败但 Claude 继续了 exit code 不是 2 明确阻断场景使用 exit 2
stdout 没进入上下文 事件类型不适合注入上下文 优先用 SessionStartUserPromptSubmit
自动批准没有生效 JSON 决策字段不对或 matcher 没匹配 打印决策摘要,检查 Bash 命令模式
Hook 很慢 脚本做了全仓库扫描或网络请求 缩小范围,设置 timeout,异步记录日志
敏感信息出现在日志 记录了完整 stdin 只记录事件名、工具名、路径摘要和决策

十一、最佳实践:把 Hooks 当成小型策略系统维护

11.1 从一个低风险 Hook 开始

推荐顺序:

  1. PostToolUse 自动格式化;
  2. SessionStart 注入短上下文;
  3. PreToolUse 阻止敏感文件;
  4. PermissionRequest 自动批准低风险命令;
  5. Stop 做任务完成检查。

不要第一天就配置十几个 Hook。Hooks 越多,交互越复杂,排查成本越高。

11.2 Hook 脚本保持短小、单一职责

一个 Hook 只做一件事。例如:

  • format-touched-file.sh 只格式化被触碰文件;
  • block-sensitive-files.sh 只处理敏感路径;
  • session-context.sh 只输出启动上下文;
  • approve-safe-test.sh 只做测试命令授权判断。

如果一个脚本同时做格式化、权限判断、日志上报和通知,很快会变成难以审查的黑盒。

11.3 优先白名单,谨慎黑名单

黑名单适合起步,例如阻止 .env*.pemproduction.yaml。但对高安全项目,白名单更可靠:

允许:src/**、tests/**、docs/**
拒绝:infra/**、deploy/**、secrets/**、.github/workflows/release.yml
人工确认:package manager lockfile、数据库 migration、CI 配置

白名单的好处是默认拒绝未知风险,缺点是维护成本更高。团队可以按项目敏感度选择。

11.4 控制输出长度

SessionStartUserPromptSubmit 的 stdout 可能进入 Claude 上下文。输出太长会带来两个问题:

  • 占用上下文窗口;
  • 增加模型处理成本和干扰。

建议:

  • git status --short 可以,完整 git diff 要谨慎;
  • TODO 只输出前几十行;
  • 错误日志只输出最近几条摘要;
  • 大文件内容不要通过 Hook 注入。

11.5 为 Hook 设置超时

原文提到 Hook 默认超时为 60 秒,也可以单独配置 timeout。实践上,绝大多数 Hook 都应该在几秒到几十秒内结束。

{
  "type": "command",
  "command": ".claude/hooks/format-touched-file.sh",
  "timeout": 30
}

如果 Hook 需要跑完整测试套件,通常不适合放在高频事件上。更好的做法是让 Claude 显式运行测试,或者把完整测试放到 CI。

11.6 团队规则进入代码评审

.claude/settings.json.claude/hooks/*.sh 应该像工程代码一样评审。评审重点包括:

  • 是否有 eval、未引用变量、命令拼接;
  • 是否读取或记录敏感内容;
  • matcher 是否过宽;
  • 自动批准策略是否保守;
  • 失败时是否给出清晰错误;
  • 是否有超时和日志。

十二、常见问题与解决方案

Q1:Hook 没有触发,第一步查什么?

先查三件事:

  1. 事件名是否正确,例如 PostToolUse 不是 PostTool
  2. matcher 是否匹配,尤其是 Bash 大小写;
  3. 配置是否已在 /hooks 中审核生效。

如果仍然不触发,把 matcher 暂时改成 * 做最小验证,确认事件能触发后再收窄范围。

Q2:Hook 应该写在项目级还是用户级?

团队共享规则写 .claude/settings.json;个人偏好写 ~/.claude/settings.json;本机路径和实验写 .claude/settings.local.json

判断标准很简单:如果你希望同事也执行同一条规则,就放项目级;如果只是你自己的效率工具,就不要提交到仓库。

Q3:PreToolUsePermissionRequest 都能拦截命令,怎么选?

如果你要在工具执行前校验工具输入,选 PreToolUse。如果你主要想处理权限弹窗前的批准或拒绝,选 PermissionRequest

一个典型组合是:

  • PreToolUse:阻止写敏感路径;
  • PermissionRequest:自动批准安全测试命令,拒绝危险 Bash 命令。

Q4:PostToolUse 自动格式化会不会和 Claude 的编辑冲突?

可能会改变 diff,但这通常是预期行为。建议只格式化被 Claude 触碰的文件,不要全仓库格式化。对于格式化失败,先记录日志,不要一开始就强制阻断。

Q5:如何避免 Hook 泄露敏感信息?

关键是控制输入和日志:

  • 不记录完整 stdin;
  • 不记录 tool_input.content
  • 不把 .env、token、credentials 输出到 stdout;
  • transcript 路径可以记录,但 transcript 内容不要自动复制到共享位置;
  • 日志目录使用用户本地目录,并设置合理权限。

Q6:多个 Hook 同时匹配会怎样?

原文提到多个匹配 Hook 会并行运行,相同命令会去重。这意味着不要依赖多个 Hook 的执行顺序。如果两个 Hook 有顺序依赖,最好合并到同一个脚本里显式编排。

Q7:Hook 里可以访问环境变量吗?

可以。原文提到的变量包括 CLAUDE_PROJECT_DIRCLAUDE_CODE_REMOTECLAUDE_ENV_FILE 等,也可以访问当前 shell 环境中的变量。但不要默认环境变量都存在,脚本里应提供兜底逻辑。

Q8:Hook 能修改工具输入吗?

原文提到结构化响应可用于更细粒度控制,包括更新输入一类能力。实际字段和支持范围应以当前 Claude Code 文档为准。工程上建议谨慎使用“修改输入”:它比“批准/拒绝/记录”更难排查,最好只用于非常明确的场景。

Q9:什么时候不应该用 Hook?

这些场景不适合 Hook:

  • 需要人类判断业务语义;
  • 需要长时间运行的完整测试;
  • 需要访问生产系统;
  • 会产生不可逆副作用;
  • 只是为了“看起来自动化”,但没有真实摩擦点。

Hook 的最佳使用方式是解决高频、确定、可验证的问题。


十三、一个可落地的团队 Playbook

如果要在团队里引入 Claude Code Hooks,可以按下面的节奏推进:

阶段一:个人试点

目标:验证 Hook 机制是否适合项目。

  • .claude/settings.local.json 中配置 PostToolUse
  • 只做被修改文件的格式化;
  • 记录 Hook 触发日志;
  • 观察一周,看是否影响正常开发。

阶段二:项目共享

目标:把低风险收益固化成团队规则。

  • 把稳定配置迁移到 .claude/settings.json
  • Hook 脚本进入 .claude/hooks/
  • 代码评审中检查 shell 安全和 matcher 范围;
  • README 或团队文档中说明如何通过 /hooks 审核。

阶段三:安全治理

目标:让 Claude Code 不越过项目红线。

  • 增加敏感文件保护;
  • 增加危险 Bash 命令拒绝策略;
  • 对自动批准命令做白名单;
  • 为策略决策记录轻量审计日志。

阶段四:上下文工程

目标:减少重复提示,让 Claude 更快进入项目状态。

  • SessionStart 注入 git 状态和 TODO 摘要;
  • UserPromptSubmit 注入短 Sprint 背景;
  • PreCompact 保存关键决策;
  • Stop 做完成度自检。

阶段五:持续改进

目标:把 Hook 当成工程资产维护。

  • 定期清理不再使用的 Hook;
  • 统计 Hook 触发频率和失败率;
  • 对误拦截、误批准做复盘;
  • 随 Claude Code 版本更新调整配置。

十四、总结:Hooks 是 Agent 工程化的控制面

Claude Code Hooks 的核心价值,不是让开发者少敲几条命令,而是把 Agent 工作流中的关键控制点显式化:

  • 在工具执行前,你可以拦截风险;
  • 在权限确认前,你可以自动化低风险决策;
  • 在工具执行后,你可以做格式化和记录;
  • 在会话开始和用户提交时,你可以注入动态上下文;
  • 在上下文压缩和任务停止前,你可以保存状态、检查完成度。

这让 Claude Code 从一个“能主动调用工具的模型界面”,进一步变成一个“可以被团队规则约束、被工程流程治理、被日志和策略观测的 Agent 系统”。

但也正因为 Hooks 能执行本地命令,它必须被严肃对待。最好的实践不是配置最多的 Hook,而是配置少量、清晰、可审查、能解决真实摩擦点的 Hook。

如果只记住一句话,可以是:把确定性规则交给 Hooks,把创造性判断留给 Claude,把最终责任留给人类工程流程。

「真诚赞赏,手留余香」

爱折腾的工程师

真诚赞赏,手留余香

使用微信扫描二维码完成支付