首页 › tutorials › 造会话中枢:生命周期钩子全装配

概述

本教程带你用 Claude Code 的生命周期钩子(Lifecycle Hooks)从零搭出一个"会话中枢":会话一开自动注入项目状态、每条提问自动带上当前焦点、长任务跑完桌面提醒、压缩前抢救关键上下文、子代理产出自动留痕、会话结束自动归档。学完你会掌握 SessionStart、UserPromptSubmit、Notification、PreCompact、SubagentStop、SessionEnd 六类钩子的接线方式与 stdin/stdout 的 JSON 握手协议。适合已经写过一两个 Skill 或 MCP、想把 Claude Code 改造成"无人值守 + 自动记忆"工作台的进阶玩家。

前置条件

本教程刻意避开已被反复讲过的 PreToolUse(命令护栏)和 Stop/PostToolUse(质量自愈)两条线,专注其余六类"会话级"钩子,补齐全景。

一、先搞懂:钩子事件全景

钩子本质是你在 settings.json 里登记的 shell 命令,Claude Code 在特定生命周期节点把它们唤醒,通过 stdin 喂进一段 JSON,再根据退出码与 stdout 的 JSON决定后续行为。所有事件共享同一套握手协议,区别只在于"什么时候触发"和"stdout 能影响什么"。

事件 触发时机 典型用途 本教程是否装配
SessionStart 会话开启/恢复/清屏后 注入项目状态、环境摘要 ✅
UserPromptSubmit 用户按下回车提交提问 注入动态上下文、留痕、拦截 ✅
Notification Claude 需要输入或等待确认 桌面提醒、响铃 ✅
PreCompact 上下文压缩前 抢救关键信息、回灌要点 ✅
SubagentStop 子代理(Task/Agent)结束 产出留痕、强制复核 ✅
SessionEnd 会话结束 归档、索引、清理 ✅
PreToolUse 工具执行前 拦截危险命令、改写参数 (已有护栏教程)
PostToolUse 工具执行后 自动格式化、自愈 (已有自愈教程)

关键握手规则记住三条:

  1. 退出码 0 = 正常。对 UserPromptSubmit/SessionStart/PreCompact 这类"可注入"事件,stdout(或结构化 JSON)会被加进上下文。
  2. 退出码 2 = 阻断错误,stderr 会被回喂给 Claude,让它知道"被打断了,原因是什么"。
  3. 其它非零 = 非阻断错误,stderr 只在 UI 里提示,不打断流程。
flowchart LR
    A[会话开启]:::start --> HS[SessionStart 注入项目状态]:::hook
    HS --> U[等待用户提问]
    U --> HU[UserPromptSubmit 注入焦点+留痕]:::hook
    HU --> C[Claude 思考/调工具]
    C -->|需输入/等待| HN[Notification 桌面提醒]:::hook
    C -->|上下文将满| HC[PreCompact 抢救要点]:::hook
    C -->|派子代理| HSU[SubagentStop 产出留痕]:::hook
    C -->|结束| HE[SessionEnd 归档索引]:::hook
    HE --> Z[(~/.claude/sessions)]:::store
    classDef start fill:#d4edda,stroke:#28a745;
    classDef hook fill:#cce5ff,stroke:#007bff;
    classDef store fill:#fff3cd,stroke:#ffc107;

二、搭建会话中枢骨架

先建一个集中存放钩子脚本和产出的目录,养成"脚本与数据分离"的习惯:

mkdir -p ~/.claude/hooks          # 钩子脚本
mkdir -p ~/.claude/sessions       # 钩子产出的快照、日志、归档

所有脚本共用同一个套路:cat 读 stdin → jq 取字段 → 干活 → 用 jq -n 拼结构化输出。先写一个取字段的小约定,后面六个脚本都按它来:

INPUT="$(cat)"
CWD="$(printf '%s' "$INPUT" | jq -r '.cwd // "."')"
SESSION_ID="$(printf '%s' "$INPUT" | jq -r '.session_id // "-"')"

注意:jq -r '.cwd // "."' 里的 // 是 jq 的"空值兜底"运算符,字段缺失时回退到默认值,避免脚本因为字段缺失而崩在 set -e 上。

三、SessionStart:启动即注入项目状态

新建 ~/.claude/hooks/on-session-start.sh:

#!/usr/bin/env bash
# SessionStart: feed Claude a one-shot project status on every session open
set -euo pipefail
INPUT="$(cat)"
CWD="$(printf '%s' "$INPUT" | jq -r '.cwd // "."')"
SRC="$(printf '%s' "$INPUT" | jq -r '.source // "startup"')"
PROJECT="$(basename "$CWD")"
mkdir -p "$HOME/.claude/sessions"
RECAP="$HOME/.claude/sessions/${PROJECT}.recap.md"

lines="[会话中枢·SessionStart source=$SRC]\n- 项目:$PROJECT\n"

if command -v git >/dev/null 2>&1 && git -C "$CWD" rev-parse >/dev/null 2>&1; then
  BRANCH="$(git -C "$CWD" branch --show-current 2>/dev/null || echo '-')"
  DIRTY="$(git -C "$CWD" status --porcelain 2>/dev/null | wc -l | tr -d ' ')"
  AHEAD="$(git -C "$CWD" rev-list --count '@{u}..HEAD' 2>/dev/null || echo 0)"
  lines+="- Git:分支 $BRANCH / 未提交 $DIRTY / 领先远端 $AHEAD\n"
fi

if [[ -f "$RECAP" ]]; then
  lines+="- 上次小结:\n$(sed 's/^/  /' "$RECAP" | head -20)\n"
fi

jq -n --arg ctx "$(printf '%b' "$lines")" \
  '{hookSpecificOutput:{hookEventName:"SessionStart", hookAdditionalContext:$ctx}}'

source 字段会告诉你这次会话是怎么来的:startup(全新启动)、resume(恢复历史会话)、clear(清屏)、compact(压缩后重建)。你可以据此决定要不要重新喂一大段上下文——压缩重建时其实不需要再灌一遍。

预期结果:会话一开,Claude 的上下文里就多了项目名、分支、脏文件数、上次小结,它第一句话往往就能直接接上"上次我们在改 X"。

四、UserPromptSubmit:每条提问自动带上焦点

UserPromptSubmit 是"高频注入点"——你每按一次回车它就跑一次。新建 on-prompt.sh:

#!/usr/bin/env bash
# UserPromptSubmit: log every prompt + inject FOCUS.md if present
set -euo pipefail
INPUT="$(cat)"
CWD="$(printf '%s' "$INPUT" | jq -r '.cwd // "."')"
PROMPT="$(printf '%s' "$INPUT" | jq -r '.prompt // ""')"
PROJECT="$(basename "$CWD")"
mkdir -p "$HOME/.claude/sessions"

# 1) leave a trail of every prompt
printf '[%s] %s\n' "$(date '+%F %T')" "$(printf '%s' "$PROMPT" | head -c 120)" \
  >> "$HOME/.claude/sessions/${PROJECT}.prompts.log"

# 2) inject focus file so the model never loses the thread
FOCUS="$CWD/.claude/FOCUS.md"
if [[ -f "$FOCUS" ]]; then
  ctx="[当前焦点·FOCUS.md]\n$(sed 's/^/  /' "$FOCUS" | head -30)\n"
  jq -n --arg c "$(printf '%b' "$ctx")" \
    '{hookSpecificOutput:{hookEventName:"UserPromptSubmit", hookAdditionalContext:$c}}'
fi

在项目里放一个 .claude/FOCUS.md,写上"本周目标:把 auth 模块拆成 service"之类。之后无论你问什么,Claude 都会先把这条焦点记在脑子里。

进阶玩法:UserPromptSubmit 也支持阻断。如果你检测到提问里粘了疑似密钥(如 AKIA 开头的 AWS Key),直接 echo "提问里疑似含密钥,请删去后再发" >&2; exit 2,Claude 会收到这条 stderr 并主动停下。这比 PreToolUse 拦命令更"前置"——在提问阶段就掐断。

五、Notification:长任务跑完桌面提醒

长任务(重构、跑测试、批量生成)最烦的就是盯着屏幕等。Notification 在 Claude 需要你输入或长时间等待时触发。新建 on-notify.sh:

#!/usr/bin/env bash
# Notification: ring the terminal bell + desktop toast
set -uo pipefail
INPUT="$(cat)"
MSG="$(printf '%s' "$INPUT" | jq -r '.message // "Claude Code 需要你")'"
printf '\a'                       # terminal bell
if command -v notify-send >/dev/null 2>&1; then
  notify-send "Claude Code" "$MSG" 2>/dev/null || true
elif command -v osascript >/dev/null 2>&1; then
  osascript -e "display notification \"$MSG\" with title \"Claude Code\"" 2>/dev/null || true
fi
exit 0

预期结果:你切去别的窗口干活,Claude 卡在"需要确认"或"任务完成"时,桌面右下角弹通知 + 终端响铃,回来一看就知道。

这里故意不写 set -e:通知是"锦上添花",绝不能因为 notify-send 不存在就让 Claude 报错中断。钩子的健壮性原则——副作用类钩子要"能跑就跑,不能跑就静默"。

六、PreCompact:压缩前抢救关键上下文

上下文快满时 Claude Code 会自动压缩(compact),但压缩有丢失细节的风险。PreCompact 给你最后一次机会把"绝对不能丢的东西"落盘并回灌。新建 on-precompact.sh:

#!/usr/bin/env bash
# PreCompact: snapshot recent prompts + focus before context is summarized
set -euo pipefail
INPUT="$(cat)"
CWD="$(printf '%s' "$INPUT" | jq -r '.cwd // "."')"
TP="$(printf '%s' "$INPUT" | jq -r '.transcript_path // ""')"
PROJECT="$(basename "$CWD")"
mkdir -p "$HOME/.claude/sessions"
DUMP="$HOME/.claude/sessions/${PROJECT}.compact.$(date '+%s').md"

{
  echo "# PreCompact 抢救快照"
  echo "- 时间:$(date '+%F %T')  触发:$(printf '%s' "$INPUT" | jq -r '.trigger // "auto"')"
  echo -e "\n## 最近用户提问(保意图)"
  if [[ -f "$TP" ]]; then
    jq -r 'select(.type=="user") | .message.content // empty' "$TP" 2>/dev/null | tail -40 || true
  fi
  echo -e "\n## 仍打开的焦点"
  [[ -f "$CWD/.claude/FOCUS.md" ]] && cat "$CWD/.claude/FOCUS.md" || echo "(无)"
} > "$DUMP"

# re-inject a reminder so the summarizer keeps unfinished items
jq -n --arg c "压缩前已把最近提问与焦点存档到 $DUMP,请务必在摘要中保留所有未完成事项。" \
  '{hookSpecificOutput:{hookEventName:"PreCompact", hookAdditionalContext:$c}}'

预期结果:每次压缩都会在 ~/.claude/sessions/ 留一个带时间戳的快照,同时给摘要器一句"别丢未完成事项"的提醒。事后翻快照就能还原"压缩那一刻我们在干嘛"。

七、SubagentStop:子代理产出自动留痕

主代理派出去的子代理(Task/Agent 工具)干完活就消失,产出混在大对话里很难复盘。SubagentStop 让你给每个子代理的结尾留个"小票"。新建 on-subagent-stop.sh:

#!/usr/bin/env bash
# SubagentStop: archive each subagent's tail output
set -uo pipefail
INPUT="$(cat)"
TP="$(printf '%s' "$INPUT" | jq -r '.transcript_path // ""')"
mkdir -p "$HOME/.claude/sessions/subagents"
LOG="$HOME/.claude/sessions/subagents/$(date '+%F_%H%M%S').tail.md"

{
  echo "## SubagentStop @ $(date '+%F %T')"
  if [[ -f "$TP" ]]; then
    jq -r 'select(.type=="assistant") | .message.content[]? | select(.type=="text") | .text' \
      "$TP" 2>/dev/null | tail -30 || true
  fi
} > "$LOG"

# uncomment to force the main agent to review subagent output before moving on:
# printf '{"decision":"block","reason":"请复核子代理产出(留痕:%s)"}\n' "$LOG"
exit 0

预期结果:每个子代理结束都会落一份尾部产出快照。需要更严格时,解开最后两行注释——decision:block 会让主代理"不急着收工",先复核子代理结果,相当于给并行任务挂了一道人工复核闸。

八、SessionEnd:会话结束自动归档

会话退出时(关终端、/exit、超时)触发,适合写索引、做清理。新建 on-session-end.sh:

#!/usr/bin/env bash
# SessionEnd: append one index line per session
set -uo pipefail
INPUT="$(cat)"
SID="$(printf '%s' "$INPUT" | jq -r '.session_id // "-"')"
CWD="$(printf '%s' "$INPUT" | jq -r '.cwd // "."')"
REASON="$(printf '%s' "$INPUT" | jq -r '.reason // "unknown"')"
PROJECT="$(basename "$CWD")"
mkdir -p "$HOME/.claude/sessions"
printf '[%s] project=%s session=%s reason=%s\n' \
  "$(date '+%F %T')" "$PROJECT" "$SID" "$REASON" \
  >> "$HOME/.claude/sessions/index.log"
exit 0

跑久了 index.log 就是一份"我跟哪些项目、哪些会话聊过"的流水账,配合 prompts.log 能完整复盘任何一天的交互。

九、一键装配 settings.json

把六个钩子登记到 ~/.claude/settings.json 的 hooks 段。注意:非工具类事件不写 matcher(或留空串),matcher 只对 PreToolUse/PostToolUse 有意义(用来匹配工具名)。

{
  "hooks": {
    "SessionStart": [
      { "hooks": [ { "type": "command", "command": "bash $HOME/.claude/hooks/on-session-start.sh" } ] }
    ],
    "UserPromptSubmit": [
      { "hooks": [ { "type": "command", "command": "bash $HOME/.claude/hooks/on-prompt.sh" } ] }
    ],
    "Notification": [
      { "hooks": [ { "type": "command", "command": "bash $HOME/.claude/hooks/on-notify.sh" } ] }
    ],
    "PreCompact": [
      { "hooks": [ { "type": "command", "command": "bash $HOME/.claude/hooks/on-precompact.sh" } ] }
    ],
    "SubagentStop": [
      { "hooks": [ { "type": "command", "command": "bash $HOME/.claude/hooks/on-subagent-stop.sh" } ] }
    ],
    "SessionEnd": [
      { "hooks": [ { "type": "command", "command": "bash $HOME/.claude/hooks/on-session-end.sh" } ] }
    ]
  }
}

项目级覆盖写在 <项目>/.claude/settings.json,本地不入库的写在 .claude/settings.local.json。三者按"企业策略 → 用户 → 项目 → 本地"的优先级合并。

十、脱机自测:不进 Claude 也能验钩子

钩子最怕"接好了但不响,又不知道哪错了"。写一个最小测试器 ~/.claude/hooks/test-hook.sh:

#!/usr/bin/env bash
# usage: ./test-hook.sh <script> <sample.json>
cat "$2" | bash "$1"; echo "——exit=$?"

再准备几份样本 JSON(模拟 Claude 真实喂进来的 stdin)。sample-session-start.json:

{
  "session_id": "test-001",
  "transcript_path": "",
  "cwd": "/home/gprshome/myproject",
  "hook_event_name": "SessionStart",
  "source": "startup"
}

sample-prompt.json:

{
  "session_id": "test-001",
  "transcript_path": "",
  "cwd": "/home/gprshome/myproject",
  "hook_event_name": "UserPromptSubmit",
  "prompt": "帮我把登录逻辑拆成 service"
}

sample-notify.json:

{ "session_id":"test-001", "cwd":"/home/gprshome/myproject",
  "hook_event_name":"Notification", "message":"Claude is waiting for your input" }

跑一遍:

chmod +x ~/.claude/hooks/*.sh
cd ~/.claude/hooks
./test-hook.sh on-session-start.sh sample-session-start.json
./test-hook.sh on-prompt.sh sample-prompt.json
./test-hook.sh on-notify.sh sample-notify.json    # 应听到响铃

预期结果:前两条会打印一段 {"hookSpecificOutput":{...}} JSON(这就是会被注入上下文的内容),第三条终端响铃且 exit=0。看到这些就说明握手正确,可以放心进真实会话。

实战案例:会话中枢完整接线

把上面所有零件串成一个可复现的小项目。目录结构:

~/.claude/
├── settings.json              # hooks 登记
└── hooks/
    ├── on-session-start.sh
    ├── on-prompt.sh
    ├── on-notify.sh
    ├── on-precompact.sh
    ├── on-subagent-stop.sh
    ├── on-session-end.sh
    └── test-hook.sh

在你的业务仓库里加一个 .claude/FOCUS.md:

# 当前焦点
- 目标:把 auth 模块拆成独立的 AuthService
- 红线:不要动 src/legacy/ 下的任何文件
- 验收:所有 auth 相关测试通过

然后启动会话:

cd ~/myproject
claude

体验闭环:

  1. 开场:Claude 上下文里已经有项目名、分支、脏文件数,第一句就能说"当前在 feat/auth 分支,有 5 个未提交改动"。
  2. 提问:你问"继续昨天的活",FOCUS.md 内容被自动带上,它直接奔"拆 AuthService"而去,且自觉绕开 src/legacy/。
  3. 等待:它跑完一轮要你确认,你正在看别的窗口——桌面通知 + 响铃把你叫回来。
  4. 派活:它派子代理并行查 3 个文件,每个结束都在 sessions/subagents/ 留了尾部产出。
  5. 压缩:聊了很久上下文将满,PreCompact 先把最近提问和焦点落盘,并提醒摘要器别丢未完成事项。
  6. 收工:/exit 退出,index.log 多了一行归档记录。

整套流程的端到端时序:

sequenceDiagram
    participant U as 用户
    participant CC as Claude Code
    participant H as 钩子脚本
    participant S as sessions 目录

    U->>CC: 启动会话
    CC->>H: SessionStart (stdin JSON)
    H->>CC: hookAdditionalContext 注入状态
    CC->>U: "在 feat/auth,5 个改动"

    U->>CC: 提问
    CC->>H: UserPromptSubmit
    H->>S: 写 prompts.log
    H->>CC: 注入 FOCUS.md
    CC->>H: SubagentStop (×N)
    H->>S: 留子代理产出

    Note over CC: 上下文将满
    CC->>H: PreCompact
    H->>S: 落快照 *.compact.*.md
    H->>CC: 回灌"别丢未完成事项"

    CC->>H: Notification (等输入)
    H->>U: 响铃 + 桌面通知

    U->>CC: /exit
    CC->>H: SessionEnd
    H->>S: 追加 index.log

常见坑与对策

现象 根因 解决
钩子死活不触发 事件名拼错(如 OnPromptSubmit) 严格用官方事件名,大小写敏感
注入的上下文没出现 stdout 不是合法 JSON 或没退出码 0 用 test-hook.sh 离线验证 jq -n 输出
脚本一跑就 set -e 退出 某字段缺失,jq 取到 null 后续命令失败 全程用 .x // "默认值" 兜底
副作用钩子(通知)偶发报错 用了 set -e 且依赖命令不存在 副作用钩子去掉 set -e,命令后加 \|\| true
改了 settings.json 没生效 改的是项目级但启动在别的目录 用 claude --debug 启动可看到钩子加载与执行轨迹
matcher 写了却没匹配 只对 PreToolUse/PostToolUse 有效 生命周期事件不要写 matcher
钩子卡住拖慢响应 脚本里有网络/长任务且同步阻塞 拆出后台进程 nohup ... &,或用 timeout 字段限时

调试金句:所有"注入型"钩子先离线喂样本 JSON 验 stdout,再进真实会话。这能拦下 90% 的"接好了但不响"。

延伸阅读

训练营适用性评估

参考来源

  1. Hooks reference - Claude Code Docs — 官方钩子事件、配置 schema、JSON 输入输出格式与退出码权威参考
  2. Automate actions with hooks - Claude Code Docs — 官方上手指南,含 matcher 匹配与多处理器写法
  3. The Complete Guide to Claude Code Hooks - timewell.jp — 逐事件机制深读,覆盖 PreToolUse/PostToolUse 等关键事件
  4. Claude Code Hooks: Complete Guide with 20+ Ready-to-Use Examples (2026) - aiorg.dev — 20+ 可复制配置,自动格式化、通知等生产级模式
  5. Claude Code Hooks — Input & Output Schemas (Reference) - GitHub Gist — 各事件 stdin/stdout 的实现级 schema 速查

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