<!-- title: 造可观测自主代理三件套 -->

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

## 概述

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

## 前置条件

- Claude Code CLI（建议 2.x 及以上，本文字段名以 2026 年版本为准）
- 一台 macOS / Linux 终端（Windows 用 Git Bash 或 WSL）
- 会写基础 shell 脚本（`jq`、`curl`、`git`）
- 已用过 `settings.json` 配权限或钩子（不熟可先看官方 hooks-guide）
- 一个真实项目目录（带 `.git`，用于状态栏显示分支与统计改行）

## 为什么需要"三件套"

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

- **状态栏**解决"代理现在怎么样"——上下文占用、累计成本、改了多少行、用的哪个模型，一秒看清。
- **输出风格**解决"代理每次说话都不一样"——同一套任务，输出结构、口吻、详略完全可控，便于下游脚本解析。
- **会话钩子**解决"代理跑着跑着就忘了/就停了/就没人管了"——开工注入清单、压缩前抢救关键记忆、停下时强制自检收尾、结束时落盘通知。

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

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

### 原理

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

配置位置：`~/.claude/settings.json`（用户级）或项目 `.claude/settings.json`（项目级）。

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

### stdin 喂进来的 JSON 长什么样

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

```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`：

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

赋予可执行权限并测试：

```bash
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 声明名字与描述，正文就是给模型的行为指令。

```markdown
---
name: audit-report
description: 输出结构化的代码审计报告，便于脚本解析与归档
---

你是一名严谨的代码审计员。每次输出必须遵循：

1. 先用一句话总结本次审计范围。
2. 用 Markdown 二级标题分节：## 🔴 高危 / ## 🟡 中危 / ## 🟢 提示。
3. 每条发现格式为：`- [文件:行号] 问题描述 → 建议修复`。
4. 结尾给出 `## 📊 统计` 小节，用列表写明高/中/低各多少条。
5. 不要寒暄、不要复述任务，直接出报告。
```

### 三种使用方式

**交互式切换**：会话内输入 `/output-style`，从列表里选；或 `/output-style audit-report` 直接指定。

**命令行指定**：批处理里直接挂上风格，最稳：

```bash
claude -p --output-style audit-report "审计 src/ 目录下所有未捕获异常"
```

**从当前对话提炼**：输入 `/output-style new`，Claude 会把你当前这段"理想输出"反推成一个可复用风格文件，存到 `~/.claude/output-styles/`。

内置风格通常有 `default`（默认）、`Explanatory`（讲解型，多解释）、`Learning`（教学型，留思考题）。自定义风格优先级与内置平级，按名字调用即可。

### 实操：为审计代理定一个"机器友好"风格

把上面那段 frontmatter 存为 `~/.claude/output-styles/audit-report.md`。然后跑：

```bash
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）

```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"

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

```jsonc
// 让代理"别停，继续干"——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 的提示文案，适合桌面通知：

```bash
#!/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——开工有交代

```bash
#!/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 加"硬约束"

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

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

自主代理长跑必然触发压缩。这一步是"别让代理压缩之后忘了已经找到的 8 个高危"。

```bash
#!/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——没写完报告不准停（自主续跑的核心）

```bash
#!/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——收尾落盘

```bash
#!/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. 跑起来

```bash
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 流程图：夜审代理的生命周期

```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 要一致，别一个要求"简洁"一个要求"详尽"。

## 延伸阅读

- 官方 Hooks 参考与快速上手（事件全集、JSON schema、async/HTTP/prompt 钩子）
- 官方 Statusline 文档（stdin JSON 字段表、padding、安全建议）
- 官方 Output Styles 文档（frontmatter、`/output-style new` 用法）
- Anthropic Cookbook：长时自主代理（long-running agent）的成本与上下文管理
- 社区实践：用 Stop 钩子做"自愈闭环"与多步任务编排

## 训练营适用性评估

- **适合类型**：DevOps 自动化营、AI 工程化进阶营、安全审计/代码质量营。不适合纯零基础营（建议先过 Round 1 入门与基础 hook 教程）。
- **教学时长**：约 2.5–3 小时。状态栏 45 分钟、输出风格 30 分钟、会话钩子 75 分钟、综合实战 60 分钟。
- **动手强度**：高。每个件套都有可独立运行的脚本，最后合成一个能 `nohup` 挂起的真实代理。
- **考核建议**：以"代理能否在无人值守下稳定产出结构化 `audit.md` 且 Stop 钩子不死循环"作为通关标准。

## 参考来源

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

> [!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学通信知识星球海报-扫码加入](https://wiki.500nian.com/images/zhixingqiu_qrcode.png)
