首页 › tutorials › 造可观测自主代理三件套

Claude Code 进阶实战:用状态栏、输出风格与会话钩子造可观测自主代理

概述

当你把 Claude Code 从"一问一答"的助手推向"无人值守的自主代理"时,最先崩掉的不是能力,而是可观测性与行为一致性:它跑了多久、烧了多少钱、上下文还剩多少、压缩之后还记得关键结论吗、输出格式每次都不一样、跑完没人知道。本教程把 Claude Code 三套常被低估的机制——状态栏(statusLine)、输出风格(output styles)、会话生命周期钩子(SessionStart / UserPromptSubmit / PreCompact / Stop / SessionEnd / Notification)——组装成一个"可观测、可复现、会自己收尾"的自主代理。适合已经写过 Skill、跑过 -p 批处理、想让代理真正长时间自跑的开发者与运维同学。

前置条件

为什么需要"三件套"

很多教程把 hook 当成"拦危险命令的门禁"。这只是 hook 的冰山一角。Hook 的另一半价值在生命周期可观测与自主续跑:

三者合在一起,就是把一个"会写代码的聊天框"变成一个"有仪表盘、有作业规范、有交接流程"的值班工程师。

第一件套:状态栏 statusLine——给代理配一块仪表盘

原理

状态栏是 Claude Code 终端界面底部的一行,本质是你指定的任意 shell 脚本。Claude Code 在每次界面刷新时,把当前会话的 JSON 状态通过 stdin 喂给脚本,脚本 print 出来的第一行就是状态栏内容。

配置位置:~/.claude/settings.json(用户级)或项目 .claude/settings.json(项目级)。

{
  "statusLine": {
    "type": "command",
    "command": "~/.claude/statusline.sh",
    "padding": 0
  }
}

stdin 喂进来的 JSON 长什么样

这是写状态栏的"数据源",字段稳定可依赖:

{
  "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 外部服务或执行不可信命令——它等于一个每秒级触发的后台进程。

第二件套:输出风格 output styles——给代理定"作业规范"

原理

输出风格是一段写在 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 会话结束 落盘会话摘要、成本统计、清理临时文件

配置骨架(settings.json)

{
  "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 会被回喂给模型)。

钩子能回吐的"控制 JSON"

这是让代理自跑的核心机制:

// 让代理"别停,继续干"——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:落盘摘要 + 成本

1. SessionStart——开工有交代

#!/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

2. UserPromptSubmit——给 prompt 加"硬约束"

#!/usr/bin/env bash
# on-prompt.sh
cat <<'JSON'
{
  "hookSpecificOutput": {
    "hookEventName": "UserPromptSubmit",
    "additionalContext": "提醒:所有结论必须以 `路径:行号` 形式给出证据,否则视为无效发现。"
  }
}
JSON

3. PreCompact——压缩前抢救记忆

自主代理长跑必然触发压缩。这一步是"别让代理压缩之后忘了已经找到的 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

4. Stop——没写完报告不准停(自主续跑的核心)

#!/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 是把"自愈"做进钩子的关键:模型说"我做完了",钩子查一下产物,没产物就把它弹回去继续干,直到产物落地。

5. SessionEnd——收尾落盘

#!/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"

6. 跑起来

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 钩子会反复把代理顶回去,直到生成或耗尽重试。

Mermaid 流程图:夜审代理的生命周期

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

常见坑与对策

  1. 钩子脚本路径用 ~ 在某些 shell 下不展开:settings.json 里尽量写绝对路径或 $HOME。
  2. Stop 钩子 block 导致死循环:务必给"放行条件"(如产物文件存在),并在脚本里加最大重试兜底(读日志计数),否则代理会一直被顶回去烧钱。
  3. PreCompact 抢救的内容太多反而冲掉上下文:additionalContext 控制在几百字以内,只塞"关键结论清单"。
  4. 状态栏脚本卡顿:任何联网/重计算都不要放状态栏,状态栏只做本地 jq + git。
  5. 钩子输出非法 JSON:脚本里 echo 多了调试信息会污染 stdout。调试期把日志写 stderr 或文件,保持 stdout 干净。
  6. 输出风格在 -p 模式下被 prompt 覆盖:-p 的 prompt 优先级高,风格指令和 prompt 要一致,别一个要求"简洁"一个要求"详尽"。

延伸阅读

训练营适用性评估

参考来源

  1. Customize your status line — Claude Code Docs — 状态栏 stdin JSON 字段、statusLine 配置与安全建议的权威来源
  2. Hooks reference — Claude Code Docs — 全部生命周期事件、控制 JSON 与退出码语义的官方参考
  3. Claude Code Hooks Guide 2026 — SerenitiesAI — 覆盖十余种钩子事件的实战配方,含 Stop/PreCompact 续跑示例
  4. Hooks, Statuslines, and the Automation Layer Nobody Talks About — samanvya.dev — 把状态栏与会话钩子组合成自动化层的工程视角
  5. Claude Code Guide 2026: 25 Features — MarkTechPost — 状态栏、输出风格、钩子等特性的整体概览与示例

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