当你把 Claude Code 从"一问一答"的助手推向"无人值守的自主代理"时,最先崩掉的不是能力,而是可观测性与行为一致性:它跑了多久、烧了多少钱、上下文还剩多少、压缩之后还记得关键结论吗、输出格式每次都不一样、跑完没人知道。本教程把 Claude Code 三套常被低估的机制——状态栏(statusLine)、输出风格(output styles)、会话生命周期钩子(SessionStart / UserPromptSubmit / PreCompact / Stop / SessionEnd / Notification)——组装成一个"可观测、可复现、会自己收尾"的自主代理。适合已经写过 Skill、跑过 -p 批处理、想让代理真正长时间自跑的开发者与运维同学。
jq、curl、git)settings.json 配权限或钩子(不熟可先看官方 hooks-guide).git,用于状态栏显示分支与统计改行)很多教程把 hook 当成"拦危险命令的门禁"。这只是 hook 的冰山一角。Hook 的另一半价值在生命周期可观测与自主续跑:
三者合在一起,就是把一个"会写代码的聊天框"变成一个"有仪表盘、有作业规范、有交接流程"的值班工程师。
状态栏是 Claude Code 终端界面底部的一行,本质是你指定的任意 shell 脚本。Claude Code 在每次界面刷新时,把当前会话的 JSON 状态通过 stdin 喂给脚本,脚本 print 出来的第一行就是状态栏内容。
配置位置:~/.claude/settings.json(用户级)或项目 .claude/settings.json(项目级)。
{
"statusLine": {
"type": "command",
"command": "~/.claude/statusline.sh",
"padding": 0
}
}
这是写状态栏的"数据源",字段稳定可依赖:
{
"hook_event_name": "Status",
"session_id": "abc123def456",
"transcript_path": "/home/you/.claude/projects/.../transcript.jsonl",
"cwd": "/home/you/work/my-project",
"model": { "id": "claude-sonnet-4-6", "display_name": "Sonnet" },
"workspace": {
"current_dir": "/home/you/work/my-project",
"project_dir": "/home/you/work/my-project"
},
"version": "2.0.42",
"output_style": { "name": "audit-report" },
"cost": {
"total_cost_usd": 0.1234,
"total_duration_ms": 184000,
"total_api_duration_ms": 9200,
"total_lines_added": 412,
"total_lines_removed": 57
},
"exceeds_200k_tokens": false
}
关键字段速查:model.display_name(用了哪个模型)、cost.total_cost_usd(已花多少美元)、cost.total_lines_added/removed(改了多少行)、exceeds_200k_tokens(上下文是否已破 20 万 token)、output_style.name(当前输出风格)。
新建 ~/.claude/statusline.sh:
#!/usr/bin/env bash
# 读取 stdin 的会话 JSON
input=$(cat)
# 用 jq 安全取值,缺失字段给默认值
model=$(echo "$input" | jq -r '.model.display_name // "?"')
cost=$(echo "$input" | jq -r '.cost.total_cost_usd // 0')
added=$(echo "$input" | jq -r '.cost.total_lines_added // 0')
removed=$(echo "$input" | jq -r '.cost.total_lines_removed // 0')
over=$(echo "$input" | jq -r '.exceeds_200k_tokens // false')
cwd=$(echo "$input" | jq -r '.workspace.current_dir')
# git 分支(不在仓库里就给兜底)
branch=$(git -C "$cwd" symbolic-ref --short HEAD 2>/dev/null || echo "no-git")
# 上下文警示色
ctx_flag="ok"
[ "$over" = "true" ] && ctx_flag="⚠️OVER200K"
# 组装输出(第一行即状态栏)
printf "🤖 %s | 💰\$%.4f | +%s/-%s | 🌿%s | 📊%s" \
"$model" "$cost" "$added" "$removed" "$branch" "$ctx_flag"
赋予可执行权限并测试:
chmod +x ~/.claude/statusline.sh
# 离线模拟一段输入,验证脚本不报错
echo '{"model":{"display_name":"Sonnet"},"cost":{"total_cost_usd":0.05,"total_lines_added":10,"total_lines_removed":2},"exceeds_200k_tokens":false,"workspace":{"current_dir":"'"$PWD"'"}}' | ~/.claude/statusline.sh
预期输出(一行):
🤖 Sonnet | 💰$0.0500 | +10/-2 | 🌿main | 📊ok
| 现象 | 原因 | 解决 |
|---|---|---|
| 状态栏空白 | 脚本没有可执行权限 | chmod +x ~/.claude/statusline.sh |
显示 ? 或 0 |
jq 未安装 |
brew install jq 或 apt install jq |
| 脚本报错把错误刷到界面 | stderr 内容被当作输出 |
脚本里统一 2>/dev/null |
| 卡顿明显 | 每次刷新都跑重命令(如联网查价) | 状态栏脚本务必纯本地、毫秒级,联网放到 Stop 钩子里 |
macOS 下 ~ 不展开 |
settings.json 里用了 ~/ |
用绝对路径或 $HOME/...,且脚本内部别依赖 ~ |
安全提示:状态栏脚本以你的用户权限运行,且每次刷新都跑。绝不要在状态栏里
curl外部服务或执行不可信命令——它等于一个每秒级触发的后台进程。
输出风格是一段写在 Markdown 文件里的系统提示,用来稳定控制代理的输出结构、详略与口吻。在无人值守场景下,它是"输出可被下游脚本解析"的关键——同样一个审计任务,没有输出风格可能给你散文体,有了就能稳定输出结构化小节。
文件位置:~/.claude/output-styles/*.md。每个文件用 YAML frontmatter 声明名字与描述,正文就是给模型的行为指令。
---
name: audit-report
description: 输出结构化的代码审计报告,便于脚本解析与归档
---
你是一名严谨的代码审计员。每次输出必须遵循:
1. 先用一句话总结本次审计范围。
2. 用 Markdown 二级标题分节:## 🔴 高危 / ## 🟡 中危 / ## 🟢 提示。
3. 每条发现格式为:`- [文件:行号] 问题描述 → 建议修复`。
4. 结尾给出 `## 📊 统计` 小节,用列表写明高/中/低各多少条。
5. 不要寒暄、不要复述任务,直接出报告。
交互式切换:会话内输入 /output-style,从列表里选;或 /output-style audit-report 直接指定。
命令行指定:批处理里直接挂上风格,最稳:
claude -p --output-style audit-report "审计 src/ 目录下所有未捕获异常"
从当前对话提炼:输入 /output-style new,Claude 会把你当前这段"理想输出"反推成一个可复用风格文件,存到 ~/.claude/output-styles/。
内置风格通常有 default(默认)、Explanatory(讲解型,多解释)、Learning(教学型,留思考题)。自定义风格优先级与内置平级,按名字调用即可。
把上面那段 frontmatter 存为 ~/.claude/output-styles/audit-report.md。然后跑:
claude -p --output-style audit-report "审计 ./src/parser.py 的错误处理"
预期:输出固定以"## 🔴 高危"等小节开头,结尾有"## 📊 统计"。由于结构稳定,你可以直接 grep '^## 🔴' 统计高危条数,或喂给下游脚本生成日报。
| 现象 | 解决 |
|---|---|
/output-style 列表里看不到自定义 |
文件必须放在 ~/.claude/output-styles/,frontmatter 的 name 不能有空格 |
| 风格"没生效",输出还是散文 | 风格是软约束;若任务本身要求散文,可把强结构要求写进 prompt 头部,双保险 |
| 想在不同项目用不同风格 | 项目级可在 .claude/ 放风格文件;或不同批处理任务 --output-style 传不同名字 |
| 输出风格与 CLAUDE.md 冲突 | 项目级 CLAUDE.md 是常驻上下文,输出风格是输出层;建议 CLAUDE.md 管"做什么",风格管"怎么写" |
前面那些"拦危险命令"的钩子多用 PreToolUse/PostToolUse。本节聚焦会话级事件,它们才是无人值守代理的脊梁:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
SessionStart |
新会话/--resume/claude 启动时 |
注入任务清单、加载项目上下文、打时间戳 |
UserPromptSubmit |
用户(或 -p 的 prompt)提交后、模型看到前 |
校验输入、追加附加上下文(如"务必引用 file:line") |
PreCompact |
上下文压缩之前 | 抢救关键记忆(已发现的问题清单、关键决策),注入回压缩后上下文 |
Stop |
模型本轮回答结束、即将把控制权交回 | 判定"真做完了吗",没做完就 block 让它继续 |
SubagentStop |
子代理结束时 | 收集子代理产出、汇总 |
Notification |
Claude 需要用户确认/输入(权限提示、长任务待命) | 桌面通知,无人值守时把"卡住"信号发出来 |
SessionEnd |
会话结束 | 落盘会话摘要、成本统计、清理临时文件 |
{
"hooks": {
"SessionStart": [
{ "hooks": [ { "type": "command", "command": "~/.claude/hooks/on-start.sh" } ] }
],
"UserPromptSubmit": [
{ "hooks": [ { "type": "command", "command": "~/.claude/hooks/on-prompt.sh" } ] }
],
"PreCompact": [
{ "hooks": [ { "type": "command", "command": "~/.claude/hooks/on-compact.sh" } ] }
],
"Stop": [
{ "hooks": [ { "type": "command", "command": "~/.claude/hooks/on-stop.sh" } ] }
],
"Notification": [
{ "hooks": [ { "type": "command", "command": "~/.claude/hooks/on-notify.sh" } ] }
],
"SessionEnd": [
{ "hooks": [ { "type": "command", "command": "~/.claude/hooks/on-end.sh" } ] }
]
}
}
每个钩子脚本通过 stdin 拿到 JSON,通过 stdout 输出 JSON 控制行为,退出码 2 表示阻塞型错误(stderr 会被回喂给模型)。
这是让代理自跑的核心机制:
// 让代理"别停,继续干"——Stop 钩子最常用
{ "decision": "block", "reason": "尚未生成 ./reports/audit.md,请继续完成。" }
// 给模型追加附加上下文——SessionStart / UserPromptSubmit / PreCompact 通用
{ "hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "本次任务清单见 ./AUDIT_CHECKLIST.md,每完成一项就在文件里勾选。"
}}
// 阻断当前 prompt(UserPromptSubmit),把 reason 回喂
{ "decision": "block", "reason": "请先在 prompt 里指明目标目录。" }
Notification 钩子拿到的是 Claude 的提示文案,适合桌面通知:
#!/usr/bin/env bash
# on-notify.sh —— 无人值守时把"等待确认"信号发出来
msg=$(jq -r '.message // "Claude 需要你介入"')
# macOS
command -v osascript >/dev/null && osascript -e "display notification \"$msg\" with title \"Claude Code\""
# Linux
command -v notify-send >/dev/null && notify-send "Claude Code" "$msg"
# 跨平台兜底:写到一个被监控的文件
echo "$(date '+%H:%M:%S') $msg" >> ~/.claude/logs/notify.log
目标:每晚让 Claude 审计当天 git diff 的改动,自动出一份结构化报告,跑完落盘 + 桌面通知,且保证报告文件真的生成才允许结束。
~/.claude/
├── settings.json # 挂状态栏 + 五个生命周期钩子
├── statusline.sh # 仪表盘
├── output-styles/audit-report.md # 输出风格
└── hooks/
├── on-start.sh # SessionStart:注入清单 + 打时间戳
├── on-prompt.sh # UserPromptSubmit:追加"引用 file:line"
├── on-compact.sh # PreCompact:抢救"已发现清单"
├── on-stop.sh # Stop:报告未生成则 block
├── on-notify.sh # Notification:桌面通知
└── on-end.sh # SessionEnd:落盘摘要 + 成本
#!/usr/bin/env bash
# on-start.sh
mkdir -p ~/.claude/logs ./reports
date '+%Y-%m-%d %H:%M:%S session-start' >> ~/.claude/logs/audit.log
cat <<'JSON'
{
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": "你是夜审代理。审计范围:今天相对 HEAD~1 的 git diff。完成后必须把结构化报告写入 ./reports/audit.md。每条发现引用 文件:行号。"
}
}
JSON
#!/usr/bin/env bash
# on-prompt.sh
cat <<'JSON'
{
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": "提醒:所有结论必须以 `路径:行号` 形式给出证据,否则视为无效发现。"
}
}
JSON
自主代理长跑必然触发压缩。这一步是"别让代理压缩之后忘了已经找到的 8 个高危"。
#!/usr/bin/env bash
# on-compact.sh
# 把已落盘报告里的统计摘要读回来,压缩后重新喂给模型
summary=""
if [ -f ./reports/audit.md ]; then
summary=$(grep -E '^## 📊' -A 6 ./reports/audit.md | tr '\n' ' ')
fi
cat <<JSON
{
"hookSpecificOutput": {
"hookEventName": "PreCompact",
"additionalContext": "压缩前抢救:当前已记录的审计统计为——${summary}。压缩后请基于此继续,不要重复已审项。"
}
}
JSON
#!/usr/bin/env bash
# on-stop.sh
if [ -f ./reports/audit.md ] && [ -s ./reports/audit.md ]; then
exit 0 # 报告已生成,放行结束
fi
cat <<'JSON'
{
"decision": "block",
"reason": "reports/audit.md 尚不存在或为空。请继续审计并按 audit-report 风格生成报告后写入该文件。"
}
JSON
这一行
decision: block是把"自愈"做进钩子的关键:模型说"我做完了",钩子查一下产物,没产物就把它弹回去继续干,直到产物落地。
#!/usr/bin/env bash
# on-end.sh
input=$(cat)
sid=$(echo "$input" | jq -r '.session_id')
# 把会话成本从 transcript 里扒出来太重,这里只记事件
date '+%Y-%m-%d %H:%M:%S session-end '"$sid" >> ~/.claude/logs/audit.log
command -v notify-send >/dev/null && notify-send "夜审完成" "报告见 ./reports/audit.md"
chmod +x ~/.claude/hooks/*.sh ~/.claude/statusline.sh
# 交互式:你会看到底部状态栏实时跳动,并感受钩子的"推背感"
claude --output-style audit-report
# 无人值守:挂上 --output-style,让钩子接管生命周期
nohup claude -p --output-style audit-report \
"审计今天相对 HEAD~1 的改动,生成 ./reports/audit.md" \
> ~/.claude/logs/run-$(date +%F).log 2>&1 &
跑完你应当得到:./reports/audit.md(结构化报告)、~/.claude/logs/audit.log(会话开关时间线)、桌面一条"夜审完成"通知。若报告没生成,Stop 钩子会反复把代理顶回去,直到生成或耗尽重试。
flowchart TD
A[启动 claude --output-style audit-report] --> B[SessionStart 钩子]
B --> B1[注入任务清单 + 打时间戳]
B1 --> C[模型开始审计]
C --> D{上下文是否接近上限?}
D -- 是 --> E[PreCompact 钩子]
E --> E1[抢救已记录统计]
E1 --> C
D -- 否 --> F[模型声称完成 → Stop 钩子]
F --> G{reports/audit.md 存在且非空?}
G -- 否 --> H[decision:block 弹回继续]
H --> C
G -- 是 --> I[放行结束 → SessionEnd 钩子]
I --> I1[落盘摘要 + 桌面通知]
J[需要用户确认] -.触发.-> K[Notification 钩子]
K --> K1[桌面/日志告警]
L[底部 statusLine 全程实时显示<br/>成本·上下文·改行·分支] -.观测.-> C
~ 在某些 shell 下不展开:settings.json 里尽量写绝对路径或 $HOME。block 导致死循环:务必给"放行条件"(如产物文件存在),并在脚本里加最大重试兜底(读日志计数),否则代理会一直被顶回去烧钱。additionalContext 控制在几百字以内,只塞"关键结论清单"。jq + git。echo 多了调试信息会污染 stdout。调试期把日志写 stderr 或文件,保持 stdout 干净。-p 模式下被 prompt 覆盖:-p 的 prompt 优先级高,风格指令和 prompt 要一致,别一个要求"简洁"一个要求"详尽"。/output-style new 用法)nohup 挂起的真实代理。audit.md 且 Stop 钩子不死循环"作为通关标准。statusLine 配置与安全建议的权威来源[!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 等场景全覆盖。
