首页 › tutorials › 动态注入项目上下文:Hooks实战

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

概述

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

前置条件

正文

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

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

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

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

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

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

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

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

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

#!/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 里注册:

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

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

常见问题:

步骤 3:UserPromptSubmit 注入 Git 实时状态

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

操作说明:建 .claude/hooks/radar.sh:

#!/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"知道现场"。

常见问题:

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

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

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 零浪费。

常见问题:

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

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

# 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、上次测试挂在哪。你不用每次先交代背景。

常见问题:

步骤 6:与 -p 模式(headless)联动

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

例如 CI 里:

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

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

常见问题:

步骤 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(同时注册两个事件):

{
  "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 + 测试告警):

#!/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 因此能直接说"先把'支付失败重试'补上吧,它和你这次改动最相关"——而不是反问"你说的收尾指什么"。

工作流程图

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[生成更贴合现场的回复]
flowchart LR
    S1[SessionStart] -->|一次| O1[开机概览注入]
    S2[UserPromptSubmit] -->|每轮| O2[实时状态 + 意图注入]
    O1 --> CTX[共享上下文]
    O2 --> CTX
    CTX --> CC[Claude 处理]

进阶玩法

延伸阅读

训练营适用性评估

参考来源

  1. Hooks reference — Claude Code Docs — 官方 Hook 事件、JSON 输入输出格式、退出码语义的权威参考。
  2. Automate actions with hooks — Claude Code Docs — 官方入门指南,含 settings.json 配置与示例。
  3. Claude Code Hooks — Input & Output Schemas (Gist) — 8 个 Hook 事件的实现级输入输出 Schema 速查,含 additionalContext 与 permissionDecision 字段说明。
  4. disler/claude-code-hooks-mastery (GitHub) — 社区维护的 Hook 实战仓库,可对照本文脚本扩展。
  5. Intercept and control agent behavior with hooks — Claude Code Docs — 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学通信知识星球海报-扫码加入