<!-- title: 把-p模式织入Unix管道 -->

## 概述

本教程面向已经能在交互式 REPL 里用 Claude Code 完成单次任务、但想把"调用一次 Claude"变成"流水线上的一颗螺丝钉"的开发者。我们将把 `-p`（headless/print）模式当作一个标准的 Unix 过滤器来设计——它有 stdin、stdout、exit code，会吐出机器可读的事件流，能在断点处续跑。读完之后，你能够用 `jq`、`xargs`、`fswatch` 把 Claude Code 编进任何 Shell 管道里，并实时观测每一步的 token 与耗时。

## 前置条件

- 已安装 Claude Code（`claude --version` 可正常输出，建议 2.x 及以上）
- 已完成至少一次交互式会话，理解工具调用（tool use）的基本概念
- 终端里可用 `jq`（事件流解析必备）、`xargs`、以及文件监听工具 `entr` 或 `fswatch`（macOS）
- 一份真实的、半结构化的日志或数据文件作为练手素材（例如 Nginx access log、应用错误日志、CSV）

## 第一步：把 -p 当成 Unix 过滤器

交互式 REPL 是一个"对话"，而 `-p` 是一个"函数调用"。它的设计哲学完全是 Unix 式的：**从 stdin 读、向 stdout 写、用 exit code 报告成败**。掌握这一点，你就掌握了它和 `sed`、`awk`、`jq` 同级的可组合性。

最朴素的三种调用：

```bash
# 1. 提示词作为参数，纯文本输出
claude -p "用一句话解释什么是 PDU 会话"

# 2. 提示词从 stdin 读入（管道左侧喂入"数据"）
cat error.log | claude -p "总结这批日志的根因，只输出一句"

# 3. 文件内容重定向进 stdin，参数里只放"指令"
claude -p "为下面的函数补上类型注解，原样输出代码，不要解释" < utils.py
```

**预期结果**：三条命令都不会进入 REPL，执行完直接回到 Shell 提示符，答案打印在标准输出。

**核心心智模型**：把"指令"和"数据"分离。指令写在参数里（稳定、可复用），数据从 stdin 流入（每次不同）。这正是 Unix 过滤器的味道——`grep PATTERN` 里 PATTERN 是指令，文件流是数据。

**常见问题**：

| 现象 | 原因 | 解决 |
|---|---|---|
| 输出里夹了"好的，这是结果……"等寒暄 | 默认系统提示词允许对话语气 | 用 `--append-system-prompt` 强约束："只输出最终答案，不要寒暄" |
| 多行提示词被 Shell 拆成多个参数 | 没有正确引用 | 用单引号包裹，或改走 stdin |
| 想把答案写文件却连提示符也写进去了 | 直接 `claude -p ... > out.txt` 会把 stderr 一起混入 | 用 `2>/dev/null` 或 `--output-format` 拿干净输出 |

## 第二步：掌握三种输出格式与 stream-json 事件协议

`-p` 的灵魂在于 `--output-format`。它有三个取值，对应三个完全不同的使用场景：

```bash
# text —— 给人看，纯文本答案
claude -p --output-format text "列出三种排序算法"

# json —— 给脚本用，整个会话压成一个 JSON 对象（阻塞到结束才输出）
claude -p --output-format json "列出三种排序算法" | jq .

# stream-json —— 给实时观测用，逐行吐出事件流（NDJSON）
claude -p --output-format stream-json "列出三种排序算法" \
  | jq -c 'select(.type=="assistant")'
```

`stream-json` 是最值得深挖的一个。它按行输出（Newline-Delimited JSON），每一行是一个事件对象。理解这四种 `type`，你就能像读一段协议那样读懂 Claude 的整个思考过程：

```mermaid
sequenceDiagram
    participant U as 你的脚本(stdin)
    participant C as claude -p
    participant M as Claude 模型
    U->>C: 提示词 + 数据
    C->>U: {type:"system", subtype:"init", session_id, tools, model, permission_mode}
    loop 每一轮 agent turn
        C->>M: 历史 + 工具结果
        M-->>C: assistant 消息(content: text | tool_use)
        C->>U: {type:"assistant", message:{content:[...]}}
        alt 包含工具调用
            C->>C: 执行工具(读文件/跑命令)
            C->>U: {type:"user", message:{content:[{type:"tool_result",...}]}}
        end
    end
    C->>U: {type:"result", subtype:"success", total_cost_usd, num_turns, duration_ms, session_id}
    C-->>U: exit code 0
```

`system/init` 事件里藏着 `session_id`——**立刻把它存下来**，这是稍后续跑的钥匙。`result` 事件里有四个关键字段：`num_turns`（轮数）、`total_cost_usd`（花费）、`duration_ms`（耗时）、`is_error`（是否出错）。一行 `jq` 就能把它们抽出来做账：

```bash
claude -p --output-format stream-json "重构 utils.py" \
  | jq -c 'select(.type=="result") | {turns:.num_turns, cost:.total_cost_usd, ms:.duration_ms, sid:.session_id}'
```

**预期结果**：结束时打印一行形如 `{"turns":4,"cost":0.0123,"ms":8421,"sid":"abc123…"}` 的 JSON。

**常见问题**：

- **"我用 `jq` 解析 stream-json 总报错"**：`json` 格式是一次性输出整个对象，`stream-json` 是多行。对后者必须用 `jq -c` 按行处理，或用 `jq --slurp` 收成数组。两者别混用。
- **"我只想拿到最终答案的那段文本"**：不要去拼 `assistant` 事件，直接取 `result` 事件的 `.result` 字段——它就是最终给用户的纯文本答案，省去拼接多轮 text 的麻烦。
- **exit code 的含义**：`0` 成功；`1` 执行过程出错（对应 `result.subtype` 为 `error_during_execution`）；`2` 表示撞到 `--max-turns` 上限（`error_max_turns`）。脚本里用 `$?` 判断分支，比解析文本可靠得多。

## 第三步：会话状态机 —— resume / continue / fork

交互式会话天然有状态，但 `-p` 看似是"一次性"的——其实不然。Claude Code 把每一次 `-p` 调用都持久化为一条会话记录，配上三个标志位，它就变成了一个**可中途暂停、可分叉、可回放的状态机**：

```bash
# 拿到上一轮的 session_id（从 init 或 result 事件里取）
SID=$(claude -p --output-format stream-json "第一步：分析需求" \
        | jq -r 'select(.type=="result") | .session_id')

# continue —— 续接"当前目录下最近的一次"会话，无需记 id
claude --continue -p "第二步：基于刚才的分析出方案"

# resume —— 精确续接指定 session，适合并行多分支任务
claude --resume "$SID" -p "第三步：把方案拆成 TODO"

# fork —— 从某会话分叉出一个新分支，原会话不被污染
claude --fork "$SID" -p "换个思路，试试另一套方案"
```

这三者的差别很关键：

- `--continue`（`-c`）是"懒人续跑"：它隐式选中 cwd 里最近一次会话。单线任务用最省事，但**并发场景下会抢同一条最近会话**，导致串台。
- `--resume <id>` 是"精确续跑"：你显式指定 session_id，多任务并行时各自续各自的，互不干扰。**这是批处理与多分支工作流的正确选择**。
- `--fork <id>` 是"无损分叉"：它复制历史并另起一条会话，原会话保持原样。适合"在不破坏主线的前提下探索 Plan B"。

```mermaid
stateDiagram-v2
    [*] --> S1: claude -p "分析需求"
    S1 --> S2: --resume S1 "出方案"
    S2 --> S3: --resume S2 "拆 TODO"
    S2 --> B1: --fork S2 "方案B"
    B1 --> B2: --resume B1 "..."
    S3 --> [*]: --resume S3 "执行"
    note right of S1: session_id 是状态机的游标
```

**常见问题**：

- **"我 `--continue` 续跑，但 Claude 像失忆了"**：多半是 cwd 变了。会话记录是按工作目录绑定的，换目录就续不上。要么在同一目录跑，要么改用 `--resume <id>` 显式指定。
- **"`--fork` 和 `--resume` 区别记不住"**：一句话——`resume` 是"接着干"（同一条线前进），`fork` 是"另起一支"（原线原地不动）。担心弄脏主线就用 fork。
- **"批量任务里每个分支都要存 id，很烦"**：把 session_id 当作函数的返回值往下传。下文实战会演示一个 `sid` 文件约定，让管道节点之间靠文件传递游标。

## 第四步：资源边界 —— 给无人值守的 Agent 套上笼子

`-p` 跑在脚本里，意味着没有人在旁边盯。三个标志位构成了你的"护栏三件套"：

```bash
claude -p \
  --max-turns 8 \
  --permission-mode acceptEdits \
  --allowedTools "Read,Edit,Bash(npm run *)" \
  --append-system-prompt "改完必须跑 npm test，测试不过就回滚，不要狡辩。" \
  "修复 src/auth.ts 里的空指针，提交前自测"
```

- **`--max-turns N`**：硬上限。Agent 陷入死循环时，N 就是你的救命稻草。给探索性任务留大点（如 20），给确定性任务收窄（如 5）。撞顶会返回 exit code 2，脚本里据此判定"这次没跑完，需要人工介入"。
- **`--permission-mode`**：`default`（每次问，不适合无人值守）、`acceptEdits`（自动放行文件编辑，但危险命令仍拦截）、`plan`（只规划不动手）、`bypassPermissions`（全自动，**慎用，仅隔离环境**）。CI 里最常用 `acceptEdits`。
- **`--allowedTools` / `--disallowedTools`**：白/黑名单。`--allowedTools "Bash(npm run *),Read,Edit"` 把 Bash 限定到只能跑 `npm run`，是命令注入的第一道闸。

**预期结果**：上面这条命令会自动改文件、跑测试，若 8 轮内没收敛就以非零码退出，不会无限烧 token。

**常见问题**：

- **"`bypassPermissions` 真省心，我直接全局开"**：危险。它等于把 Shell 完全交给模型。永远只在容器/沙箱里开，生产机器上用 `acceptEdits` + `--allowedTools` 收口。
- **"max-turns 设多少合适"**：经验值——只读分析类 3–5，单文件修复 5–8，跨文件重构 15–20。先小后大，撞顶了再放宽，比一开始就放开省钱。
- **"我加了 `--append-system-prompt` 但模型不遵守"**：把它写成**可观测的动作**（"跑测试""回滚"），而非抽象原则（"要小心""保证质量"）。模型对动词比对形容词听话。

## 第五步：Unix 管道组合 —— jq / xargs / fswatch

单次 `-p` 是函数，组合起来才是工程。三个经典 Unix 模式把 Claude 变成管道公民：

**模式 A：流式解析——实时观测与过滤**

```bash
# 只看模型每一次要调用的工具，像 tail -f 一样盯着它干活
claude -p --output-format stream-json "梳理这个仓库的依赖" \
  | jq -r 'select(.type=="assistant") | .message.content[]? | select(.type=="tool_use") | "调用：" + .name + " " + (.input | tostring)'
```

**模式 B：扇出——一个结果喂给 N 个子任务**

```bash
# 先让 Claude 列出需要加测试的文件，再对每个文件并发派发子任务
claude -p --output-format text \
  --append-system-prompt "只输出文件名，每行一个，不要任何解释" \
  "src 目录下哪些模块最缺单元测试？" \
  | xargs -P4 -I{} claude -p --max-turns 6 \
      --permission-mode acceptEdits \
      "为 {} 补三个单元测试"
```

`-P4` 让 4 个子任务并发跑。每个 `-p` 都是独立的进程、独立的会话，天然隔离——这比手写线程池简单得多。

**模式 C：监听——文件一变就触发**

```bash
# 用 entr 监听：只要被测文件改动，就让 Claude 重跑测试并自动修
ls src/*.ts | entr -c sh -c '
  claude -p --output-format stream-json --max-turns 10 \
    --permission-mode acceptEdits \
    "跑 npm test，把第一个失败的用例修到通过" \
    | jq -c "select(.type==\"result\") | {ok:(.is_error|not), turns:.num_turns}"
'
```

macOS 上把 `entr` 换成 `fswatch -o src/ | xargs -I{} ...` 即可。

**常见问题**：

- **"`xargs -P4` 一开，token 账单飙升"**：并发不是免费午餐。给每个子任务配 `--max-turns` 上限，并在 `--append-system-prompt` 里要求"只改目标文件、别扩散"。
- **"`entr`/`fswatch` 死循环"**：模型改了文件→触发监听→又改→又触发。用 `entr -c` 清屏缓解观感，根本上要给模型加约束："如果测试已通过，直接报告 PASS，不要再改任何文件"。

## 第六步：可观测性 —— 把事件流变成实时仪表盘

`stream-json` 的真正威力是"白盒"。下面这段脚本用一个 `tee` 把事件流一分为二：一份喂给业务处理，一份喂给一个纯 Shell 写的实时仪表盘，边跑边打印轮数、累计 token、累计花费：

```bash
#!/usr/bin/env bash
# dashboard.sh —— 边跑 Claude 边看实时开销
FIFO=/tmp/cc_events_$$
mkfifo "$FIFO"
# 后台：把事件流分类统计，实时刷新
(
  turns=0; cost=0; itok=0; otok=0
  jq -c '.' < "$FIFO" | while read -r ev; do
    t=$(echo "$ev" | jq -r '.type')
    case $t in
      assistant) echo "$ev" | jq -e '.message.content[]? | select(.type=="tool_use")' >/dev/null \
                   && turns=$((turns+1));;
      result)    cost=$(echo "$ev"  | jq -r '.total_cost_usd');
                 itok=$(echo "$ev"  | jq -r '.usage.input_tokens');
                 otok=$(echo "$ev"  | jq -r '.usage.output_tokens');;
    esac
    printf '\rturns=%-3s cost=$%-7s in=%-7s out=%-7s' "$turns" "$cost" "$itok" "$otok"
  done
) &
DASH=$!
# 前台：真正执行任务，事件流 tee 给仪表盘
claude -p --output-format stream-json "$@" > "$FIFO"
kill $DASH 2>/dev/null; rm -f "$FIFO"
echo
```

跑起来：`./dashboard.sh "把 src 下所有 console.log 删掉"`，你会看到一行不断刷新的指标条。把这段思路接上 `curl` 推到 Prometheus/Grafana，就是一套现成的 Agent 观测管线。

**常见问题**：

- **"tee 之后 stdout 被污染"**：业务输出走 `--output-format json` 落到文件，仪表盘只读事件流，两者用命名管道（FIFO）解耦，互不串台。
- **"想知道每次工具调具体在干嘛"**：把 `assistant` 事件里 `.message.content[].type=="tool_use"` 的 `.input` 也统计进去，就能画出"工具调用热度图"。

## 实战案例：半结构化日志 → 结构化告警的自愈管道

把前面六步串成一个真实项目。目标：监听一份不断追加的 `app.log`，每当出现新的 ERROR，让 Claude 把它的半结构化文本解析成 JSON、判定严重程度、对 P0 自动尝试修复并 PR，全程可观测。

**目录约定**：`.sid/` 存每个告警分支的会话游标；`alerts/` 存结构化结果；`dash.log` 存实时指标。

```bash
#!/usr/bin/env bash
# selfheal.sh —— 日志自愈管道主控
set -euo pipefail
mkdir -p .sid alerts

follow_errors() {
  # 模式 C：监听日志新增行，过滤出 ERROR，按指纹去重后触发处理
  tail -n0 -F app.log \
    | grep --line-buffered 'ERROR' \
    | while IFS= read -r line; do
        FID=$(echo -n "$line" | shasum | cut -c1-8)
        [ -f ".sid/$FID.done" ] && continue        # 同一问题只处理一次
        handle "$FID" "$line" &
      done
}

handle() {
  local fid="$1" line="$2"
  # 步骤1：解析 + 定级（只读，快，max-turns 收窄）
  local parsed sid
  parsed=$(echo "$line" | claude -p --output-format json --max-turns 4 \
    --append-system-prompt "只输出 JSON：{severity:P0|P1|P2, component, msg, suggested_fix}" \
    "把这条日志解析成结构化字段")
  sid=$(echo "$parsed" | jq -r '.session_id')
  echo "$sid" > ".sid/$fid"
  echo "$parsed" | jq -r '.result | fromjson' > "alerts/$fid.json"
  echo "[$(date +%T)] $fid 定级完成 sid=$sid"

  # 步骤2：仅 P0 才尝试自愈（acceptEdits + 白名单，max-turns 兜底）
  if [ "$(jq -r .severity "alerts/$fid.json")" = "P0" ]; then
    claude --resume "$sid" -p --max-turns 10 \
      --permission-mode acceptEdits \
      --allowedTools "Read,Edit,Bash(npm run *),Bash(git *)" \
      --output-format stream-json \
      --append-system-prompt "修完必须 npm test，绿了再 git commit，最后总结一行。" \
      "按 suggested_fix 修复并验证" \
      | tee -a dash.log | jq -c 'select(.type=="result") | {fid:"'"$fid"'", ok:(.is_error|not), turns:.num_turns, cost:.total_cost_usd}'
  fi
  touch ".sid/$fid.done"
}

follow_errors
```

整体架构与数据流：

```mermaid
flowchart LR
    LOG[(app.log)] -->|tail -F grep ERROR| DEDUP{指纹去重<br/>shasum}
    DEDUP -->|新问题| PARSE[claude -p --max-turns 4<br/>解析+定级]
    DEDUP -->|已处理| SKIP[跳过]
    PARSE -->|session_id| SID[(.sid/ 指纹→游标)]
    PARSE -->|JSON| ALERTS[(alerts/ 结构化)]
    PARSE -->|severity| GATE{P0?}
    GATE -->|是| HEAL[claude --resume sid<br/>--acceptEdits --max-turns 10<br/>修复+自测+提交]
    GATE -->|否| DONE([归档])
    HEAL -->|stream-json tee| DASH[dash.log<br/>实时轮数/花费/成败]
    HEAL --> SID2[复用同一游标<br/>保持上下文]
```

这个案例同时用上了：第二步的 `stream-json` 协议解析、第三步的 `--resume` 复用游标（定级和自愈共享上下文）、第四步的 `--max-turns` + `--permission-mode` + `--allowedTools` 三重护栏、第五步的 `tail`/`while`/`&` 并发管道、第六步的 `tee` 实时观测。每一段都能单独抽出来复用。

**验收**：手动 `echo "[ERROR] db connection refused" >> app.log`，几秒内应看到 `alerts/<fid>.json` 生成；若是 P0（取决于你的系统提示约定），会看到 `dash.log` 里滚出 `result` 事件和一行汇总。

**常见问题**：

- **"同一错误重复触发"**：`shasum` 指纹 + `.done` 标记做去重。指纹可改成"去掉时间戳再哈希"以聚合同类问题。
- **"自愈改崩了代码"**：`--allowedTools` 收紧到只允许特定子命令；要求"npm test 不过就 git checkout 回滚"写进 `--append-system-prompt`；CI 上再加一道独立的回归测试兜底。
- **"并发太多打爆额度"**：把 `handle ... &` 换成有界并发，例如 `xargs -P4`，或用一个简单的信号量（`sem`）限流。

## 延伸阅读

- **官方 Headless 文档**：`code.claude.com/docs/en/headless` —— `-p` 的权威语义、`stream-json` 事件字段的完整定义。
- **CLI Reference**：`code.claude.com/docs/en/cli-reference` —— 所有标志位（含 `--fork`、`--mcp-config`、`--input-format`）的一手说明。
- **`--input-format stream-json`**：把 `-p` 变成"喂整段对话、收整段对话"的双向协议，可用任何语言写驱动，完全绕开 SDK——适合做自定义 Agent 调度器。
- **与 Hooks 联动**：把本教程的 `PreToolUse`/`PostToolUse` 钩子接到 `stream-json` 的工具调用事件上，可实现"每次写文件前自动留快照"的细粒度审计。
- **CI/CD 集成**：在 GitHub Actions 里用 `claude -p --permission-mode acceptEdits` 做 PR 自动审查与修复，exit code 直接驱动 workflow 的 `if: failure()` 分支。

## 参考来源

1. [Run Claude Code programmatically — Claude Code Docs](https://code.claude.com/docs/en/headless) — `-p`/`--print` 头部模式、`--output-format`、`--input-format`、`--resume`/`--continue` 的官方语义与示例。
2. [CLI reference — Claude Code Docs](https://code.claude.com/docs/en/cli-reference) — 全部命令与标志位（`--fork`、`--max-turns`、`--permission-mode`、`--allowedTools`、`--append-system-prompt`、`--mcp-config`）的分类速查。
3. [Claude Code stream-json: the output format that changes everything](https://backgroundclaude.com/blog/stream-json) — `text`/`json`/`stream-json` 三种格式的对比与事件流解析实战。
4. [CLI Output & Headless Mode — DeepWiki](https://deepwiki.com/kdxsydq/ClaudeCode/14.2-cli-output-and-headless-mode) — `--print` 标志如何启用头部模式与结构化输出，含事件类型拆解。
5. [Headless Mode and CI/CD — Cheatsheet (SFEIR Institute)](https://institute.sfeir.com/en/claude-code/claude-code-headless-mode-and-ci-cd/cheatsheet/) — 头部模式在 CI/CD 管道中的速查命令清单。

---

> 把每一次 `-p` 调用都当作可组合、可观测、可续跑的工程原语——Claude Code 就从"一个聊天工具"变成"你 Shell 工具箱里最锋利的那把瑞士军刀"。

> [!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)
