<!-- title: 动态注入项目上下文：Hooks实战 -->

# 动态注入项目上下文：Hooks实战

## 概述

本教程聚焦 Claude Code 里最被低估的自动化能力之一：**用 Hooks 在每一次提问、每一次会话启动时，把项目的实时状态（Git 分支、最近提交、改动文件、测试结果、任务清单）自动注入到上下文里**。学完后，你不再需要手动粘贴 `git status`、不需要把动态信息写进臃肿的 `CLAUDE.md`，Claude 会"自带雷达"地读懂当前项目环境。适合每天重度使用 Claude Code、希望减少机械操作、追求工程效能的开发者。

## 前置条件

- 已安装较新版本的 Claude Code（Hooks 与 `hookSpecificOutput` 协议需较新版本支持）
- 能读懂 Bash 脚本，机器上有 `jq`（用于解析 Hook 的 stdin JSON）
- 手头有一个 Git 仓库用来练习
- 了解 `settings.json` 的三层位置（`~/.claude/settings.json` 用户级、`.claude/settings.json` 项目级、`.claude/settings.local.json` 个人本地级）

## 正文

### 步骤 1：先厘清"动态注入"和"静态记忆"的差别

很多人知道 `CLAUDE.md` 是 Claude Code 的长期记忆：启动时自动加载，每轮都在上下文里。它适合写**不变**的东西——项目约定、架构原则、命令清单。

但项目里大量信息是**会变的**：当前在哪个分支、最近三次提交是什么、工作区脏了几个文件、上一次测试有没有挂、`TODO.md` 里还剩几条没做。把这些塞进 `CLAUDE.md` 有两个硬伤：

1. **它不会自动更新**——你写进去的那一刻就过时了。
2. **它每轮都占 token**——即便这次提问根本用不上。

Hooks 提供了另一条路：**每次事件触发时，由脚本现算现注入**。这就是"动态注入"。它的两个核心通道：

- `SessionStart`：会话启动或 `--resume` 恢复时触发一次，适合注入"开机就要知道"的概览。
- `UserPromptSubmit`：**用户每次提交 prompt、Claude 处理之前**触发，适合注入"和这次提问相关"的增量状态。

注入机制很简单：Hook 脚本把要注入的文本放进 `hookSpecificOutput.additionalContext` 字段，或直接 `echo` 到 stdout（退出码 0 时，`UserPromptSubmit` 和 `SessionStart` 的 stdout 会被加进上下文）。Claude 在"看到"你的提问之前，会先看到这段补充上下文。

### 步骤 2：Hello World——用 SessionStart 注入开机上下文

先写一个最简单的开机注入脚本，确认管线打通。

**操作说明**：在项目根目录建 `.claude/hooks/boot.sh`：

```bash
#!/usr/bin/env bash
# .claude/hooks/boot.sh — SessionStart：开机注入项目概览
set -euo pipefail

cwd="$(jq -r '.cwd // "."' <<<"$(cat)")"
cd "$cwd" 2>/dev/null || exit 0

name="$(basename "$(pwd)")"
git rev-parse --is-inside-work-tree >/dev/null 2>&1 && {
  branch="$(git rev-parse --abbrev-ref HEAD)"
  echo "当前项目: ${name}（分支 ${branch}）。请优先遵守 CLAUDE.md 中的规范。"
} || echo "当前项目: ${name}（非 Git 仓库）。"
```

在 `.claude/settings.json` 里注册：

```json
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "",
        "hooks": [
          { "type": "command", "command": "bash $CLAUDE_PROJECT_DIR/.claude/hooks/boot.sh" }
        ]
      }
    ]
  }
}
```

**预期结果**：重新启动 Claude Code（或在当前会话 `/clear` 后），用 `Ctrl+R` 进入 transcript 模式，能看到脚本 `echo` 出的那一行已经被注入。之后你随便问一句"我在哪个分支"，Claude 不必再调用工具就能回答。

**常见问题**：

- *没看到注入*：八成是脚本报错被吞了。临时把 `set -euo pipefail` 注释掉，或在脚本最后加一行 `echo "DEBUG done" >&2`，stderr 会显示给用户。
- *路径不对*：Hook 命令里的工作目录默认是 cwd，但用 `$CLAUDE_PROJECT_DIR` 指向项目根更稳。
- *stdout 没进上下文*：确认退出码是 0；退出码 2 会变成"阻断"，stdout 不会被注入。

### 步骤 3：UserPromptSubmit 注入 Git 实时状态

开机注入是一次性的，真正好用的是"每次提问都刷新"。现在做一个 `UserPromptSubmit` Hook，把 Git 现状塞进去。

**操作说明**：建 `.claude/hooks/radar.sh`：

```bash
#!/usr/bin/env bash
# .claude/hooks/radar.sh — UserPromptSubmit：每次提问注入项目雷达
set -euo pipefail

input="$(cat)"
prompt="$(jq -r '.prompt // ""' <<<"$input")"
cwd="$(jq -r '.cwd // "."' <<<"$input")"
cd "$cwd" 2>/dev/null || exit 0

git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0

branch="$(git rev-parse --abbrev-ref HEAD)"
ab="$(git rev-list --left-right --count '@{u}...HEAD' 2>/dev/null \
      | awk '{print "behind "$1" / ahead "$2}' || echo '无上游')"
last="$(git log --oneline -3 2>/dev/null | sed 's/^/  - /' || true)"
status="$(git status --porcelain 2>/dev/null || true)"
mod="$(printf '%s\n' "$status" | grep -c '^.M' || true)"
unt="$(printf '%s\n' "$status" | grep -c '^??' || true)"

ctx=$(cat <<EOF
[项目雷达] 分支: ${branch} (${ab})
最近提交:
${last}
工作区: 已修改 ${mod} / 未跟踪 ${unt}
EOF
)

jq -nc --arg c "$ctx" \
  '{hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:$c}}'
```

把它注册到 `settings.json` 的 `UserPromptSubmit` 事件（结构和上面 `SessionStart` 一样，把事件名换掉即可）。

**预期结果**：你输入"帮我看看最近改了啥"，Claude 在收到这句话的**同时**就拿到了分支、最近三次提交、脏文件数。它可以直接基于这些回答，而省去先调一次 `Bash` 跑 `git status` 的往返。这在交互体验上是质变——AI"知道现场"。

**常见问题**：

- *JSON 转义出错*：永远用 `jq -nc --arg` 来拼 JSON，不要手写字符串拼接。提交信息里有引号、反斜杠也不会炸。
- *注入内容太长*：控制 `git log` 的条数、`status` 只统计数量而不列全文。注入是"按字收费"的，别贪。
- *Hook 卡住*：默认超时 60 秒，但 `@{u}` 在没网或没上游时会慢。上面的 `|| echo` 兜底了，仍建议在本地测试耗时。

### 步骤 4：按"提问意图"做条件注入

无脑注入所有信息会浪费 token。更聪明的做法是**先看 prompt 说了什么，再决定注入什么**。`radar.sh` 已经拿到了 `prompt`，加一段意图识别：

```bash
hint=""
case "$prompt" in
  *测试*|*test*|*跑测*)
    log=".last-test.log"
    [ -f "$log" ] && hint="最近测试日志存在，建议先 tail -50 $log 确认是否失败。";;
  *提交*|*commit*|*push*)
    hint="用户可能要提交，已修改 ${mod} 个文件。建议先 review diff 再提交。";;
  *重构*|*refactor*)
    hint="用户可能要重构。建议先列出改动影响面再动手。";;
esac
[ -n "$hint" ] && ctx="$ctx"$'\n'"[提示] $hint"
```

**预期结果**：当你说"跑一下测试"，上下文里就多出测试日志提示；说"准备提交"，就多出 review 提示。无关提问则不注入，token 零浪费。

**常见问题**：

- *意图判断太粗暴*：`case` 匹配中文/英文关键词够用；要更准可在脚本里调用一个小分类器或读 `transcript_path` 拿到更多上下文。但记住：Hook 要快，复杂的别放这里。
- *误触发*：例如 prompt 里提到"测试环境配置"被命中"测试"。可以通过加更具体的关键词（如"跑测试"/"run test"）降低误判。

### 步骤 5：注入任务清单与最近错误

把"动态信息"做厚一点：读取 `TODO.md` 的未完成项、读取最近一次测试/构建的错误。

```bash
# TODO 顶层未完成项（以 "- [ ]" 开头，取前 5 条）
if [ -f TODO.md ]; then
  todos="$(grep -m5 '^- \[ \]' TODO.md | sed 's/^/  /' || true)"
  [ -n "$todos" ] && ctx="$ctx"$'\n'"未完成 TODO:\n$todos"
fi

# 最近一次失败的测试痕迹
if [ -f .last-test.log ] && grep -qi 'fail\|error' .last-test.log; then
  fails="$(grep -iE 'fail|error' .last-test.log | tail -3 | sed 's/^/  /')"
  ctx="$ctx"$'\n'"[警告] 上次测试有失败:\n$fails"
fi
```

**预期结果**：Claude 一开始就知道还有哪些 TODO、上次测试挂在哪。你不用每次先交代背景。

**常见问题**：

- *TODO.md 格式不统一*：约定团队用 `- [ ]` / `- [x]` 的 checkbox 语法，脚本解析才稳。
- *`.last-test.log` 谁来写*：可以用 `PostToolUse` Hook 在 `Bash` 跑完测试后自动落盘，或直接在测试命令后 `| tee .last-test.log`。

### 步骤 6：与 `-p` 模式（headless）联动

这是动态注入最被忽视的价值：**`-p` 非交互模式同样会触发 `SessionStart` 和 `UserPromptSubmit` Hook**。意味着你的批量任务、CI 脚本里的 `claude -p "..."`，也能自动拿到项目雷达，而无需在 prompt 里手写一大段背景。

例如 CI 里：

```bash
claude -p "根据当前分支的改动，生成一段 PR 描述" \
  --output-format json | jq -r '.result'
```

因为 `radar.sh` 已经把分支、提交、脏文件数注入了，这句 prompt 不必再啰嗦背景，输出也更贴合实际改动。

**常见问题**：

- *CI 里 Hook 没生效*：CI 通常用全新 checkout，确认 `.claude/settings.json` 和 `.claude/hooks/` 都进了仓库，且 `jq`、`git` 在 CI 镜像里可用。
- *注入拖慢批量任务*：批量场景下每条都跑 Hook，务必保证脚本在 100ms 量级。去掉一切网络调用（如 `@{u}` 涉及远端比较）。

### 步骤 7：性能与成本控制（必读）

动态注入是把双刃剑，管不好反而更费 token、更慢。三条铁律：

1. **快**：Hook 默认超时 60s，但你该追求 <1s。不联网、不跑重计算；要联网的信息（远端 PR、issue）缓存到本地文件再读。
2. **少**：注入的是"会进入上下文"的文本，按字计费。宁可注入摘要（"已修改 3 个文件"）也不要贴全文（`git diff` 几千行）。
3. **幂等**：Hook 可能在一次会话里被触发多次，脚本别有副作用（不要在 Hook 里改文件、提交代码）。

配置层级也要分清：**团队共享的注入逻辑放 `.claude/settings.json`（进版本库）；个人偏好放 `.claude/settings.local.json`（gitignore）**，避免把个人雷达强加给同事。

## 实战案例：一个完整的"项目雷达"系统

把上面零散片段组装成一个可运行项目。目录结构：

```
your-repo/
├── .claude/
│   ├── settings.json
│   └── hooks/
│       ├── boot.sh        # SessionStart：开机概览
│       └── radar.sh       # UserPromptSubmit：实时雷达 + 意图注入
├── TODO.md
└── .last-test.log
```

`.claude/settings.json`（同时注册两个事件）：

```json
{
  "hooks": {
    "SessionStart": [
      { "matcher": "",
        "hooks": [ { "type": "command", "command": "bash $CLAUDE_PROJECT_DIR/.claude/hooks/boot.sh" } ] }
    ],
    "UserPromptSubmit": [
      { "matcher": "",
        "hooks": [ { "type": "command", "command": "bash $CLAUDE_PROJECT_DIR/.claude/hooks/radar.sh" } ] }
    ]
  }
}
```

`radar.sh` 完整版（合并意图识别 + TODO + 测试告警）：

```bash
#!/usr/bin/env bash
set -euo pipefail
input="$(cat)"
prompt="$(jq -r '.prompt // ""' <<<"$input")"
cwd="$(jq -r '.cwd // "."' <<<"$input")"
cd "$cwd" 2>/dev/null || exit 0
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0

branch="$(git rev-parse --abbrev-ref HEAD)"
ab="$(git rev-list --left-right --count '@{u}...HEAD' 2>/dev/null \
      | awk '{print "behind "$1" / ahead "$2}' || echo '无上游')"
last="$(git log --oneline -3 | sed 's/^/  - /' || true)"
status="$(git status --porcelain)"
mod="$(printf '%s\n' "$status" | grep -c '^.M' || true)"
unt="$(printf '%s\n' "$status" | grep -c '^??' || true)"

ctx="[项目雷达] 分支: ${branch} (${ab})
最近提交:
${last}
工作区: 已修改 ${mod} / 未跟踪 ${unt}"

case "$prompt" in
  *测试*|*test*|*跑测*)
    [ -f .last-test.log ] && \
      ctx="$ctx"$'\n'"[提示] 最近测试日志: .last-test.log";;
  *提交*|*commit*|*push*)
    ctx="$ctx"$'\n'"[提示] 可能要提交，先 review ${mod} 个文件的 diff";;
esac

[ -f TODO.md ] && {
  todos="$(grep -m5 '^- \[ \]' TODO.md | sed 's/^/  /' || true)"
  [ -n "$todos" ] && ctx="$ctx"$'\n'"未完成 TODO:\n$todos"
}

[ -f .last-test.log ] && grep -qi 'fail\|error' .last-test.log && {
  fails="$(grep -iE 'fail|error' .last-test.log | tail -3 | sed 's/^/  /')"
  ctx="$ctx"$'\n'"[警告] 上次测试失败:\n$fails"
}

jq -nc --arg c "$ctx" \
  '{hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:$c}}'
```

**演示运行**：设当前在 `feat/payment` 分支、有 2 个已改文件、`TODO.md` 里有 3 条未完成、上次测试挂了一条。你输入"帮我把这块收尾"，Claude 实际看到的上下文大致是：

```
[项目雷达] 分支: feat/payment (behind 0 / ahead 2)
最近提交:
  - a1b2c3d 接入支付回调
  - e4f5g6h 修复金额校验
  - 7h8i9j0 初始化支付模块
工作区: 已修改 2 / 未跟踪 1
未完成 TODO:
  - [ ] 支付失败重试
  - [ ] 对账脚本
  - [ ] 退款流程
```

Claude 因此能直接说"先把'支付失败重试'补上吧，它和你这次改动最相关"——而不是反问"你说的收尾指什么"。

## 工作流程图

```mermaid
flowchart TD
    A[用户输入 prompt] --> B[UserPromptSubmit Hook 触发]
    B --> C[radar.sh 读取 stdin JSON]
    C --> D[计算 Git 状态/TODO/测试/意图]
    D --> E[拼装 additionalContext]
    E --> F{退出码}
    F -->|0 + JSON| G[additionalContext 注入上下文]
    F -->|2| H[阻断 prompt，stderr 提示用户]
    G --> I[Claude 看到 prompt + 项目雷达]
    I --> J[生成更贴合现场的回复]
```

```mermaid
flowchart LR
    S1[SessionStart] -->|一次| O1[开机概览注入]
    S2[UserPromptSubmit] -->|每轮| O2[实时状态 + 意图注入]
    O1 --> CTX[共享上下文]
    O2 --> CTX
    CTX --> CC[Claude 处理]
```

## 进阶玩法

- **`PreToolUse` 当护栏**：和注入正交的另一条路是拦截。脚本返回 `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"禁止 rm -rf"}}`，可阻断危险命令；`"allow"` 则免去权限弹窗。
- **读 `transcript_path` 做历史感知**：注入脚本可以读会话 `.jsonl`，判断"这个话题已经聊过了"从而避免重复注入。
- **`-p` 批量 + 注入**：把雷达和 CI/批处理结合，每条 headless 任务都自带上下文，prompt 极短却信息完备。
- **缓存重计算**：把耗时的远端信息（如 open issue 列表）由独立的 cron 写到 `.claude/cache/*.json`，Hook 只读本地文件，保持亚秒级。

## 延伸阅读

- **Hooks reference**（官方事件、输入输出、退出码全集）：`code.claude.com/docs/en/hooks`
- **Automate actions with hooks**（官方入门与示例）：`code.claude.com/docs/en/hooks-guide`
- **Agent SDK hooks**（在 Agent SDK 里拦截工具调用）：`code.claude.com/docs/en/agent-sdk/hooks`
- **claude-code-hooks-mastery**（社区 Hook 仓库，含大量现成脚本）：`github.com/disler/claude-code-hooks-mastery`
- **Hooks 输入输出 Schema 速查**（实现级参考）：`gist.github.com/FrancisBourre/50dca37124ecc43eaf08328cdcccdb34`

## 训练营适用性评估

- **适用类型**：Claude Code 进阶训练营、研发工程效能训练营、AI 自动化实战营。尤其适合"让 AI 真正理解我的项目"这一主题的进阶模块。
- **预计教学时长**：2–2.5 小时（含动手搭建 radar.sh 并在自己的仓库跑通）。
- **学员水平**：中级。需熟悉 Bash 与 JSON，理解 Git 基本命令。
- **配套练习**：可让学员各自为自己的业务仓库定制意图关键词与注入字段，最后做一轮"注入前后对比"演示，体感最强。

## 参考来源

1. [Hooks reference — Claude Code Docs](https://code.claude.com/docs/en/hooks) — 官方 Hook 事件、JSON 输入输出格式、退出码语义的权威参考。
2. [Automate actions with hooks — Claude Code Docs](https://code.claude.com/docs/en/hooks-guide) — 官方入门指南，含 settings.json 配置与示例。
3. [Claude Code Hooks — Input & Output Schemas (Gist)](https://gist.github.com/FrancisBourre/50dca37124ecc43eaf08328cdcccdb34) — 8 个 Hook 事件的实现级输入输出 Schema 速查，含 additionalContext 与 permissionDecision 字段说明。
4. [disler/claude-code-hooks-mastery (GitHub)](https://github.com/disler/claude-code-hooks-mastery) — 社区维护的 Hook 实战仓库，可对照本文脚本扩展。
5. [Intercept and control agent behavior with hooks — Claude Code Docs](https://code.claude.com/docs/en/agent-sdk/hooks) — Agent SDK 中用 PreToolUse 等事件拦截与控制工具调用。

> [!tip] 51学通信知识星球
> **星主：爱卫生**（微信：gprshome201101）——公众号「51学通信」创办者，19 年通信行业运维与讲师经验，著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书（各大电商平台有售）。
> **星球定位**：深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域，主打运维岗位与信令分析。
> **当前规模**：5200 篇主题 / 990 个回答，198 元/年。
> **加入福利**：加入星球后请加爱卫生微信，拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题，另有群内分享。
> **AI 加持**：51学通信知识星球已作为 skill 接入 WorkBuddy，使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例，做总结、找文档和 PPT 等场景全覆盖。

![51学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
