首页 › tutorials › 造上下文管家钩子:注入与抢救

造上下文管家钩子:自动注入与抢救记忆

概述

本教程聚焦 Claude Code 三类常被忽视的「上下文生命周期钩子」——SessionStart、UserPromptSubmit、PreCompact,带你从零搭建一个项目记忆管家:会话启动即注入状态、每条提问按关键词补充相关文档、压缩前把目标抢救到磁盘。读完即可让 Claude 在长会话和 -p 批处理中始终保持「记得自己在做什么」。适合已跑通首个 Claude Code 项目、想进阶自动化的开发者。

前置条件

一、为什么是这三类钩子

Claude Code 的 8 类钩子事件,社区教程九成都在讲 PreToolUse / PostToolUse(拦危险命令、自动格式化)。但真正决定 Claude「记不记得住事」的,是另外三类的生命周期钩子:

钩子事件 触发时机 最擅长的事
SessionStart 新会话、--resume 恢复、/clear 清屏、/compact 压缩后 启动即注入项目状态、上次笔记、git 摘要
UserPromptSubmit 用户按下回车、提问进入 Claude 之前 按关键词路由,给这条提问补充相关上下文
PreCompact /compact 手动或上下文将满自动压缩之前 把目标、待办、关键决策抢救到磁盘,防止压缩丢失

这三者的共同点是:它们的 stdout(exit 0)能被注入进 Claude 的上下文(PreCompact 例外,它的 stderr 只给用户看,本身不注入——它的活儿是写盘抢救)。这恰好补上了 CLAUDE.md 静态记忆做不到的两件事:动态、按需、跨压缩持续。

CLAUDE.md 是「永远在场」的全局记忆,但它是静态的、全文常驻、不分场景。而上下文管家钩子是「按需召唤」的:启动时给一份当前状态,提问时按内容补一两段相关文档,压缩前把要紧的东西先存盘。两者互补,不是替代。

⚠️ 重要区别:这三类钩子和 PreToolUse/PostToolUse 的 exit code 2 行为不同。对 SessionStart/UserPromptSubmit/PreCompact,exit code 2 的 stderr 只给用户看,不进上下文,也不阻断流程(UserPromptSubmit 的 exit 2 会阻断该提问并擦除)。这是后面调试时最常踩的坑。

二、钩子脚手架与三种验证方法

先把目录和测试方法备好,避免后面改一次就要重启会话。

目录结构:

your-project/
├── .claude/
│   ├── settings.json          # 注册三类钩子
│   ├── hooks/
│   │   ├── on-session-start.sh
│   │   ├── on-prompt-submit.sh
│   │   └── on-precompact.sh
│   ├── context/               # UserPromptSubmit 路由用的知识片段
│   │   ├── testing.md
│   │   ├── deploy.md
│   │   └── database.md
│   └── notes/                 # PreCompact 抢救出来的笔记(自动生成)
└── STATUS.md                  # 人机共维护的项目状态

STATUS.md 是个简单约定,内容示例:

# 当前目标
- 把 src/ 里的错误处理统一改成返回 Result 类型
- 修复 #142:登录偶发 500

# 已知约束
- 不得引入新依赖,预算已锁

三种验证方法(按从快到慢排列):

  1. 管道喂数据(最快,不依赖 Claude):直接构造一份 stdin JSON 喂给脚本,看 stdout。 bash echo '{"hook_event_name":"SessionStart","source":"startup","cwd":"'"$PWD"'","session_id":"t1","transcript_path":""}' \ | bash .claude/hooks/on-session-start.sh 能看到拼接好的上下文片段,就说明逻辑对了。

  2. /hooks 命令:在交互式会话里输入 /hooks,会列出当前生效的所有钩子及其事件,能确认配置有没有被正确加载。

  3. transcript 回看(Ctrl+R):钩子注入的上下文会出现在 transcript 里。按 Ctrl+R 进入 transcript 模式,能看到「这条提问注入了什么」「会话启动注入了什么」,是排查「为什么没生效」的终极手段。

关键心法:对 SessionStart 和 UserPromptSubmit,脚本的 stdout 会被原样注入 Claude 上下文。这意味着脚本里任何一句调试用的 echo "debug..." 都会污染上下文。调试一律走 stderr(echo ... >&2),它只给用户看,不进上下文。

三、SessionStart:启动即注入当前状态

会话一开(或 /clear、--resume、/compact 之后),Claude 的上下文是「干净」的。这正是注入项目状态的最佳时机。

.claude/hooks/on-session-start.sh:

#!/usr/bin/env bash
# SessionStart 钩子:会话启动/恢复/清屏/压缩后注入项目上下文
set -euo pipefail

input=$(cat)
source=$(printf '%s' "$input" | jq -r '.source // "startup"')
cwd=$(printf '%s' "$input" | jq -r '.cwd // "."')
cd "$cwd" 2>/dev/null || exit 0   # 进不去项目目录就静默退出

ctx=""
ctx+="## 🧠 项目记忆管家 · 会话上下文(来源: $source)\n\n"

# 1) 上次 PreCompact 抢救的笔记:取最近一份
latest=$(ls -t .claude/notes/*.md 2>/dev/null | head -1)
if [ -n "$latest" ]; then
  ctx+="### 🧷 上次压缩前抢救的笔记($latest)\n$(sed -n '1,30p' "$latest")\n\n"
fi

# 2) STATUS.md:人机共维护的当前目标
if [ -f STATUS.md ]; then
  ctx+="### 📋 STATUS.md\n$(sed -n '1,40p' STATUS.md)\n\n"
fi

# 3) Git 摘要:当前分支 + 未提交改动数
if command -v git >/dev/null 2>&1 && git rev-parse --git-dir >/dev/null 2>&1; then
  branch=$(git branch --show-current 2>/dev/null || echo "?")
  dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
  ahead=$(git rev-list --count '@{u}..HEAD' 2>/dev/null || echo "0")
  ctx+="### 🌿 Git:分支=$branch  未提交=$dirty  领先远端=$ahead\n\n"
fi

# stdout + exit 0 → 这段文本被注入上下文
printf '%b' "$ctx"
exit 0

逐行说明:

预期结果:会话一启动,Claude 就「知道」当前在哪个分支、有几个未提交改动、上次压缩前在忙什么、当前目标是什么。你不用每次开场都重复「我们在做一个登录修复」。

常见问题:

四、UserPromptSubmit:按关键词路由补充上下文

SessionStart 是「开场白」,UserPromptSubmit 才是「随叫随到」。它在提问进入 Claude 之前触发,收到 prompt 字段,可以分析提问内容并补充相关文档。

这解决一个真实痛点:项目文档全堆进 CLAUDE.md 会撑爆上下文;不堆又导致 Claude「不知道有这套规范」。UserPromptSubmit 让上下文按需进场。

.claude/hooks/on-prompt-submit.sh:

#!/usr/bin/env bash
# UserPromptSubmit 钩子:按提问关键词路由补充相关上下文片段
set -euo pipefail

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

[ -d .claude/context ] || exit 0   # 没有知识目录就不补

add=""
# 关键词 → 上下文片段的路由表
if printf '%s' "$prompt" | grep -qiE '测试|test|单测|tdd|覆盖率'; then
  add+="$(cat .claude/context/testing.md 2>/dev/null)\n\n"
fi
if printf '%s' "$prompt" | grep -qiE '部署|deploy|发布|上线|prod|回滚'; then
  add+="$(cat .claude/context/deploy.md 2>/dev/null)\n\n"
fi
if printf '%s' "$prompt" | grep -qiE '数据库|db|sql|表结构|schema|migration|索引'; then
  add+="$(cat .claude/context/database.md 2>/dev/null)\n\n"
fi

# 没命中任何关键词就静默退出,不污染上下文
[ -z "$(printf '%b' "$add" | tr -d '[:space:]')" ] && exit 0

# 结构化输出:additionalContext 会被附加到这条提问的上下文里
jq -nc --arg ctx "## 📎 相关上下文(按提问关键词自动补充)\n\n${add}" \
  '{hookSpecificOutput:{hookEventName:"UserPromptSubmit", additionalContext:$ctx}}'

知识片段示例 .claude/context/testing.md:

# 测试规范
- 单测用 Vitest,文件放 src/__tests__/,命名 *.test.ts
- 禁止 mock 数据库,用 testcontainers 起真实 PG
- 覆盖率门槛:核心模块 ≥ 90%,工具函数 ≥ 80%
- 提交前必须跑 `npm run test:cov`

两种输出方式的选择:

预期结果:你输入「给登录函数补单测」,钩子命中 测试|单测,自动把 testing.md 的规范塞进上下文。Claude 生成的测试会直接遵守「用 Vitest、不 mock 数据库、放 __tests__/」,而不是自作主张用 Jest。

进阶:语义路由而非关键词

关键词 grep 简单可靠,但「帮我把用户表加个索引」里没有「数据库」三个字却命中了 索引。要更准,可以调用一个本地嵌入模型或直接在脚本里 curl 一个轻量分类接口。但先用关键词跑通,80% 场景够用,别过早优化。

常见问题:

五、PreCompact:压缩前抢救关键信息

长会话里上下文迟早会满,Claude Code 自动 /compact,或你手动压缩。压缩会丢弃大量中间细节,目标和待办尤其容易丢。PreCompact 在压缩执行之前触发,是「抢救黄金窗口」。

.claude/hooks/on-precompact.sh:

#!/usr/bin/env bash
# PreCompact 钩子:压缩前把最近目标/待办抢救到 .claude/notes
set -euo pipefail

input=$(cat)
trigger=$(printf '%s' "$input" | jq -r '.trigger // "manual"')   # manual | auto
tpath=$(printf '%s' "$input" | jq -r '.transcript_path // ""')
sid=$(printf '%s' "$input" | jq -r '.session_id // "unknown"')

mkdir -p .claude/notes 2>/dev/null || true

# 从 transcript 抽取最近的若干条 user 消息作为「目标」抢救
rescued=""
if [ -n "$tpath" ] && [ -f "$tpath" ]; then
  rescued=$(jq -r 'select(.type=="user") | .message.content
                   | if type=="array" then (map(select(.type=="text").text)|join(" "))
                     else . end' "$tpath" 2>/dev/null \
           | grep -vE '^\s*$' | tail -n 25 || true)
fi

out=".claude/notes/session-${sid}.md"
{
  echo "# 压缩前抢救 · trigger=$trigger · $(date -u +%FT%TZ 2>/dev/null)"
  echo
  echo "## 最近用户目标(最近 25 条)"
  echo "${rescued:-(未抽取到用户消息)}"
} > "$out"

# PreCompact 的 stderr 只给用户看,不进上下文——适合做日志
echo "[记忆管家] trigger=$trigger 已抢救最近目标 → $out" >&2
exit 0

注意 PreCompact 的特殊性:

预期结果:每次压缩前,最近的目标和指令被存盘。即便会话被压缩多次、甚至重启,SessionStart 都能把上次的「我们在修 #142」捞回来。

常见问题:

六、注册钩子:settings.json

三类脚本就绪,统一注册到项目级 .claude/settings.json:

{
  "hooks": {
    "SessionStart": [
      { "hooks": [ { "type": "command", "command": "bash $CLAUDE_PROJECT_DIR/.claude/hooks/on-session-start.sh" } ] }
    ],
    "UserPromptSubmit": [
      { "hooks": [ { "type": "command", "command": "bash $CLAUDE_PROJECT_DIR/.claude/hooks/on-prompt-submit.sh" } ] }
    ],
    "PreCompact": [
      { "hooks": [ { "type": "command", "command": "bash $CLAUDE_PROJECT_DIR/.claude/hooks/on-precompact.sh" } ] }
    ]
  }
}

要点:

放好后 claude 重启会话,/hooks 应能看到三个事件。然后跑方法 1 的管道逐个验证,最后做端到端测试:输一句带「测试」的提问,Ctrl+R 看注入。

七、与 -p 无头模式联动:批量任务的记忆管道

-p(print/headless)模式适合 CI、批处理、脚本编排。但 -p 每次都是「冷启动」,上下文天然为空——这正是 SessionStart 钩子的用武之地。

给一个 -p 审查任务接上记忆管道:

claude -p "按 STATUS.md 的目标,审查 src/ 的错误处理是否统一返回 Result 类型" \
  --output-format json | jq -r '.result'

这条命令运行时:

  1. SessionStart 触发 → 注入 STATUS.md 目标 + git 摘要 + 上次抢救的笔记。Claude 一上来就知道「目标是统一 Result 类型」。
  2. 若审查涉及测试,UserPromptSubmit 命中关键词 → 补 testing.md 规范(注:-p 的 prompt 也会过 UserPromptSubmit)。
  3. 若上下文将满自动压缩 → PreCompact 抢救,下次 -p 还能读到。

进一步,把多个 -p 串成批量管道,每个任务都自带项目记忆:

# 批量审查最近 5 个改动文件,各自独立但共享记忆管家
for f in $(git diff --name-only HEAD~5); do
  claude -p "审查 $f 的错误处理与测试覆盖,违反 STATUS.md 约束请指出" \
    --output-format json | jq -r '.result' > "review-$(basename $f).md"
done

每个 -p 冷启动都被 SessionStart 喂了相同的项目上下文,输出一致、可复现。这比依赖会话历史稳定得多——记忆不在脑子里,在磁盘和钩子里。

组合心法:SessionStart 管「冷启动缺上下文」,UserPromptSubmit 管「提问缺相关文档」,PreCompact 管「长任务丢目标」。三者覆盖了 -p 批处理和长交互会话的全部记忆盲区。

实战案例:端到端跑通「项目记忆管家」

把上面的零件组装成一个可复现的最小演示。

步骤 1:建项目骨架。

mkdir mem-butler-demo && cd mem-butler-demo
git init -q
mkdir -p .claude/{hooks,context,notes}
touch STATUS.md

步骤 2:填 STATUS.md 和一个知识片段。

cat > STATUS.md <<'EOF'
# 目标
- 把 login() 改成返回 Result<User, AuthError>
# 约束
- 不引入新依赖
EOF

cat > .claude/context/testing.md <<'EOF'
# 测试规范
- 用 Vitest,文件 *.test.ts
- 禁止 mock 数据库,用 testcontainers
- 覆盖率核心模块 ≥ 90%
EOF

步骤 3:写入第三~五节的三份脚本,chmod +x .claude/hooks/*.sh(虽然用 bash 显式调用不强制,但加上无害)。

步骤 4:写入第六节的 settings.json。

步骤 5:单独验证三脚本(方法 1)。

echo '{"hook_event_name":"SessionStart","source":"startup","cwd":"'"$PWD"'","session_id":"d1","transcript_path":""}' \
  | bash .claude/hooks/on-session-start.sh
# 期望:输出含 STATUS.md 目标 + Git 摘要

echo '{"hook_event_name":"UserPromptSubmit","prompt":"给 login 补单测","cwd":"'"$PWD"'","session_id":"d1","transcript_path":""}' \
  | bash .claude/hooks/on-prompt-submit.sh
# 期望:输出 JSON,additionalContext 含 testing.md 规范

步骤 6:端到端。启动 claude,/hooks 确认三个事件,输入「给 login 补单测」,Ctrl+R 看注入。再 /compact,检查 .claude/notes/ 是否生成了抢救笔记。重启会话,确认 SessionStart 把抢救笔记读回。

步骤 7(批量):跑第七节的 -p 审查循环,观察每个任务都被喂了 STATUS.md。

跑通后,这套管家可直接复制到任何真实项目:改 STATUS.md、增减 .claude/context/*.md、调整 on-prompt-submit.sh 的关键词路由表即可。

工作流程图

flowchart TD
    A[会话启动 / 恢复 / 清屏 / 压缩后] -->|SessionStart 钩子| B[注入: STATUS + git + 上次抢救笔记]
    B --> C[Claude 主循环]
    U[用户或 -p 提交提问] -->|UserPromptSubmit 钩子| D[关键词路由: 补 testing/deploy/db 片段]
    D --> C
    C -->|上下文将满| E["手动 /compact 或自动压缩"]
    E -->|PreCompact 钩子| F["抢救目标/待办 → .claude/notes/"]
    F --> G[压缩执行: 丢弃中间细节]
    G --> C
    F -.磁盘中转.-> A
    C -->|任务完成| H[Stop]
    style B fill:#e8f5e9
    style D fill:#fff3e0
    style F fill:#fce4ec

闭环的核心是 PreCompact → 磁盘 → SessionStart 这条虚线:压缩丢的信息,靠磁盘中转跨压缩存活。

常见问题汇总

延伸阅读

训练营适用性评估

参考来源

  1. Claude Code Hooks 官方参考 — 8 类钩子事件的输入输出 schema、exit code 语义、matcher 规则的权威定义
  2. Automate actions with hooks 官方指南 — SessionStart/UserPromptSubmit/PreCompact 的配置示例与 stdout 注入上下文机制
  3. Claude Code Hooks — Input & Output Schemas (FrancisBourre gist) — 逐事件的 JSON 输入输出与 exit-code-2 行为矩阵,实现级参考
  4. Session and Context Hooks (DeepWiki) — 会话与上下文生命周期钩子的系统化讲解
  5. Claude Code Hooks Complete Guide (aiorg, 2026) — 20+ 可复制配置,含 SessionStart/PreCompact 的生产级用法

[!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学通信知识星球海报-扫码加入