<!-- title: -p 锁 JSON 造笔记索引管道 -->

## 概述

本教程教你把 Claude Code 从"交互式聊天框"变成"可组合的命令行数据工具"：用 `-p` 模式程序化调用、用 `--json-schema` 把输出锁死成结构化数据、用 `jq` 把 Claude 串进 Unix 管道，最后挂一个成本账本 hook 让批量调用可观测。适合已经会用 Claude Code 交互模式、想把它接进脚本与数据流水线的开发者、运维人和数据工程师。

## 前置条件

- 已安装 Claude Code（`claude --version` 可输出版本，建议 v2.1.128 以上）
- 命令行里装好 `jq`（`jq --version`），本文大量依赖它解析 JSON
- 一台能跑 `bash` 的环境（macOS / Linux / WSL）
- 认证就绪：交互式已 `/login`；若用 `--bare` 跑 CI，则需 `ANTHROPIC_API_KEY` 环境变量
- 一批想被索引的 Markdown 笔记（几十篇即可）

## 一、为什么要把 Claude Code 当 Unix 工具

Unix 哲学的核心是"每个程序做好一件事，通过文本流串联"。`grep`、`awk`、`jq` 都是这个传统的产物。Claude Code 的 `-p`（print）模式让它也具备了成为管道一环的能力：**读 stdin、写 stdout、退出码规范、输出可结构化**。这意味着你不必把 Claude 当成一个需要手动伺候的对话框，而可以让它和 `git`、`jq`、`xargs`、`curl` 并肩工作。

举个直觉：你想给 100 篇笔记各生成一份"标题 + 摘要 + 标签"的结构化卡片，传统做法是开 100 次对话框复制粘贴。换成管道思维，就是"喂文件 → Claude 提取 → jq 落库 → 聚合成索引"，整条链一行 `for` 循环就能跑完，而且每一步都可单独替换、可重放、可监控。

## 二、第一步：跑通最简 `-p` 调用

`-p`（或 `--print`）让 Claude Code 走非交互模式：吃一条 prompt，吐一段结果，然后退出。所有交互模式下的 CLI 选项在 `-p` 里都可用。

```bash
# 最简单的调用
claude -p "What does the auth module do?"

# 通过 stdin 喂数据（推荐，避免转义地狱）
cat notes/2026-07-01-plan.md | claude -p "用一句话总结这篇笔记"
```

**预期结果**：终端直接打印总结文本，进程退出。`-p` 模式会读取 stdin，所以 `cat 文件 | claude -p 提问` 是最稳的喂料姿势。

**关于 `--bare` 的取舍**：默认情况下，`claude -p` 会加载交互式会话的全部上下文——`~/.claude` 里的 hooks、skills、plugins、MCP 服务器、CLAUDE.md。这在脚本里反而是隐患：同事机器上配的某个 hook 可能悄悄改变结果。加 `--bare` 会跳过所有自动发现，只认你显式传入的 flag：

```bash
# 可复现、零惊喜：CI 与脚本的首选
claude --bare -p "Summarize this file" --allowedTools "Read" < notes/x.md
```

官方明确：**`--bare` 是脚本与 SDK 调用的推荐模式，未来会成为 `-p` 的默认值**。但 `--bare` 会跳过 hooks——所以本文的"成本账本 hook"那一步，需要你在交互或非 bare 模式下才能生效。这个权衡后面会再提到。

> ⚠️ 注意：从 v2.1.128 起，`-p` 的 stdin 输入上限是 10MB。超过会以非零退出码报错。大文件请写盘后在 prompt 里引用路径，而不是 `cat` 进去。

**常见问题**：

- *报 OAuth/keychain 错误？* `--bare` 会跳过 OAuth 和 keychain，认证必须来自 `ANTHROPIC_API_KEY` 或 `--settings` 里的 `apiKeyHelper`。
- *想限制工具权限？* 用 `--allowedTools "Read"` 只放行读文件，或 `--permission-mode dontAsk` 走只读基线。

## 三、第二步：用 `--output-format json` 拿结构化结果

默认 `-p` 吐纯文本，适合人看，不适合程序解析。加 `--output-format json` 后，你会拿到一个结构化对象：

```bash
claude -p "Summarize this project" --output-format json
```

输出大致长这样：

```json
{
  "type": "result",
  "subtype": "success",
  "result": "这是一个用 FastAPI 写的待办后端……",
  "session_id": "abc123de-4567-…",
  "total_cost_usd": 0.0123,
  "duration_ms": 4380,
  "is_error": false
}
```

这里有几个关键字段：`result` 是文本结果，`session_id` 可用于 `--resume` 续跑，`total_cost_usd` 是本次调用的花费，`duration_ms` 是耗时。**`total_cost_usd` 是批量调用做成本核算的金矿**——脚本里不必再去翻用量后台。

配合 `jq` 提取你想要的字段：

```bash
# 只要文本结果
claude -p "Summarize this project" --output-format json | jq -r '.result'

# 只要 session id，留给后续 --resume
sid=$(claude -p "Start a review" --output-format json | jq -r '.session_id')
claude -p "Continue that review" --resume "$sid"
```

**预期结果**：`jq -r '.result'` 干净地剥掉外壳，只剩纯文本；`$sid` 变量里是一个会话 ID，可用于多轮续跑。

**常见问题**：

- *输出里混了进度信息？* 非 `--verbose` 模式下，`json` 格式只在结尾输出一个对象，进度走 stderr，不会污染 stdout，管道是干净的。
- *想看模型流式吐字？* 那是下一个格式的活。

## 四、第三步：用 `--json-schema` 锁死输出契约（核心）

让大模型自由发挥写 JSON 是灾难：它会多塞字段、漏字段、把数组写成对象、把布尔写成 `"true"`。`--json-schema` 直接把输出绑死到一个 JSON Schema 定义上，结果落在响应的 `structured_output` 字段里。

```bash
claude -p "Extract the main function names from auth.py" \
  --output-format json \
  --json-schema '{
    "type": "object",
    "properties": {
      "functions": { "type": "array", "items": { "type": "string" } }
    },
    "required": ["functions"]
  }'
```

响应里你会看到：

```json
{
  "result": "…",
  "structured_output": { "functions": ["login", "verify_token", "refresh"] },
  "session_id": "…",
  "total_cost_usd": 0.008
}
```

`jq` 取结构化部分：

```bash
claude -p "Extract function names from auth.py" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
  | jq '.structured_output.functions'
```

**预期结果**：无论模型多想啰嗦，`structured_output` 永远严格符合你定义的 schema——数组就是数组，必填字段一定在。这是把"AI 输出"变成"可信数据"的关键一步，也是本文与普通批量调用最大的区别。

> 💡 把 schema 存成文件更易维护：`--json-schema "$(cat schemas/note-card.json)"`，schema 进版本控制，团队共享同一份契约。

**常见问题**：

- *schema 太复杂模型不遵守？* Schema 应尽量扁平，嵌套不超过两层；在 prompt 里同时用自然语言重述字段含义，双保险。
- *`structured_output` 字段不存在？* 确认同时传了 `--output-format json` 和 `--json-schema`，二者缺一不可。

## 五、第四步：把 Claude 串进 Unix 管道

有了结构化输出，Claude 就能和经典 Unix 工具平等协作。下面这条链：`git diff` 喂给 Claude 当 typo 检查器，结果再交给 `jq` 提取——Claude 只是管道中的一节。

```bash
# 把当前 diff 丢给 Claude 当拼写检查器
git diff main | claude -p \
  "you are a typo linter. for each typo in this diff, report filename:line on one line and the issue on the next. return nothing else."

# 接进管道：Claude 出结构化结果 → jq 过滤 → wc 数量
cat notes/plan.md | claude -p "提取这篇笔记涉及的 所有 技术名词，去重" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"terms":{"type":"array","items":{"type":"string"}}},"required":["terms"]}' \
  | jq '.structured_output.terms | length'
```

**预期结果**：第一条命令在终端逐条列出 diff 里的疑似拼写错误；第二条命令输出一个整数——笔记里涉及的技术名词数量。

这种"小而专"的调用比"一个大 prompt 干所有事"更稳：每节管道职责单一，坏了容易定位，换模型/换工具只动一节。

**常见问题**：

- *管道里 Claude 卡住等待权限？* 加 `--allowedTools` 或 `--permission-mode`，脚本里不能有人工确认环节。
- *特殊字符把 prompt 搞乱？* 能用 stdin 就别把内容塞进 prompt 字符串；prompt 字符串只放指令，数据走 stdin。

## 六、第五步：批量遍历目录聚合成 `index.json`

把前几步拼起来：遍历 `notes/*.md`，每篇调一次 Claude 提取结构化卡片，最后聚合成一个站点索引。先用单篇验证：

```bash
process_one() {
  local f="$1"
  cat "$f" | claude -p "你是笔记编目员。读取这篇笔记，提取标题、一句话摘要、3-5个标签、涉及的核心技术。纯文本，不要编造。" \
    --output-format json \
    --bare \
    --allowedTools "Read" \
    --json-schema "$(cat schemas/note-card.json)" \
    | jq -c --arg src "$f" '.structured_output + {source: $src}'
}
```

对应的 `schemas/note-card.json`：

```json
{
  "type": "object",
  "properties": {
    "title": { "type": "string" },
    "summary": { "type": "string" },
    "tags": { "type": "array", "items": { "type": "string" } },
    "tech": { "type": "array", "items": { "type": "string" } }
  },
  "required": ["title", "summary", "tags", "tech"]
}
```

`jq -c` 把每篇压成一行 JSON（JSONL），`--arg src` 把文件名塞进每条记录。批量遍历并聚合成数组：

```bash
# 串行版：稳，慢
for f in notes/*.md; do process_one "$f"; done > cards.jsonl

# 聚合成一个 index.json
jq -s '{generated_at: (now | todate), count: length, notes: .}' cards.jsonl > index.json
```

`jq -s`（slurp）把多行 JSONL 收成一个数组，`now | todate` 给索引盖上时间戳。

**并发加速**：笔记多了，串行会很慢。用 `xargs -P` 并发（`-P 4` 表示同时 4 个）：

```bash
find notes -name '*.md' -print0 \
  | xargs -0 -n1 -P4 -I{} bash -c 'process_one "$1"' _ {} \
  >> cards.jsonl
```

**预期结果**：`cards.jsonl` 里每行一篇笔记的结构化卡片；`index.json` 是一个带时间戳和总数的完整索引，可直接喂给静态站点生成器做搜索。

**常见问题**：

- *并发时输出交错错乱？* 每条都是完整的单行 JSON（`jq -c`），不会跨行交错；但若并发写同一文件，务必用 `>>` 追加且每条单行。
- *`--bare` 让 hook 不生效？* 对。要触发下文的成本账本 hook，去掉 `--bare`（牺牲可复现性换可观测性），或在 CI 里用 `--bare`、本地调试用非 bare。

## 七、第六步：给批量调用挂一个成本账本 hook

批量跑了几百篇笔记，你肯定想知道花了多少钱、哪篇最贵。`-p` 的 JSON 输出里有 `total_cost_usd`，但那是给程序读的；如果你想**自动、持续地**记账，就用 hook。

Claude Code 的 hooks 配在 `settings.json` 里，按事件分组。我们用 `Stop` 事件——每次 Claude 停止响应时触发，hook 通过 stdin 收到一个 JSON，里面带 `transcript_path` 指向本次会话的完整记录（JSONL）。我们从中提取最后一轮的 token 用量，写进账本。

`.claude/settings.json`：

```json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "bash .claude/cost-ledger.sh"
          }
        ]
      }
    ]
  }
}
```

`.claude/cost-ledger.sh`：

```bash
#!/usr/bin/env bash
# 从 stdin 读 hook input，提取 transcript，算 token 成本，追加到账本
input=$(cat)
transcript=$(printf '%s' "$input" | jq -r '.transcript_path')
session_id=$(printf '%s' "$input" | jq -r '.session_id')

# 从 transcript 取最后一条带 usage 的 assistant 消息
usage=$(jq -s 'map(select(.message.usage // empty)) | last | .message.usage' "$transcript" 2>/dev/null)

[ -z "$usage" ] || [ "$usage" = "null" ] && exit 0

# 简化定价（USD / 1M tokens），按需替换为你当前模型的真实价目
in=$(printf '%s' "$usage"   | jq -r '.input_tokens // 0')
out=$(printf '%s' "$usage"  | jq -r '.output_tokens // 0')
cin=$(printf '%s' "$usage"  | jq -r '.cache_read_input_tokens // 0')

cost=$(jq -n --argjson i "$in" --argjson o "$out" --argjson c "$cin" \
  '($i*3 + $o*15 + $c*0.3)/1000000')

# 追加一行 JSONL 到账本
jq -c -n \
  --arg ts "$(date -u +%FT%TZ)" \
  --arg sid "$session_id" \
  --argjson cost "$cost" \
  --argjson in "$in" --argjson out "$out" \
  '{ts:$ts, session_id:$sid, input_tokens:$in, output_tokens:$out, cost_usd:$cost}' \
  >> .claude/cost-ledger.jsonl

exit 0   # exit 0 放行；exit 2 会拦截并显示 stderr
```

跑几篇笔记后，`.claude/cost-ledger.jsonl` 里会留下一条条记录：

```json
{"ts":"2026-07-06T08:21:03Z","session_id":"abc…","input_tokens":1820,"output_tokens":240,"cost_usd":0.0088}
```

一键看总账：

```bash
jq -s '{runs: length, total_cost: (map(.cost_usd)|add), total_in: (map(.input_tokens)|add)}' .claude/cost-ledger.jsonl
```

> 📌 hook 退出码约定：`0` 成功放行；`2` 阻断并把 stderr 展示给 Claude（可用于"输出不合规则强制重试"）；其他非零码是非阻塞错误（记日志但不打断）。

**常见问题**：

- *`--bare` 下 hook 不触发？* 对，bare 模式跳过 hooks。要记账就别在那一轮用 bare；或在脚本里直接读 `-p` 输出的 `total_cost_usd` 字段自己记账（见下文实战脚本的双轨设计）。
- *transcript 路径找不到？* hook 的 stdin JSON 里 `transcript_path` 是绝对路径，确认进程对它有读权限。

## 八、实战案例：Markdown 笔记索引管道（完整）

把以上拼成一个可运行的工程。目录结构：

```
notes-indexer/
├── notes/                  # 你的 Markdown 笔记
│   ├── 2026-07-01-plan.md
│   └── 2026-07-02-rag.md
├── schemas/
│   └── note-card.json
├── .claude/
│   ├── settings.json       # Stop hook 注册
│   ├── cost-ledger.sh
│   └── cost-ledger.jsonl   # 自动生成
└── extract-notes.sh        # 主脚本
```

`extract-notes.sh`（双轨：既写卡片，又就地记账，不依赖 hook）：

```bash
#!/usr/bin/env bash
set -euo pipefail

SCHEMA="schemas/note-card.json"
OUT="cards.jsonl"
: > "$OUT"   # 清空

process_one() {
  local f="$1"
  local resp
  resp=$(cat "$f" | claude -p "你是笔记编目员。提取标题、一句话摘要、3-5个标签、核心技术。忠于原文，不要编造。" \
            --output-format json \
            --allowedTools "Read" \
            --json-schema "$(cat "$SCHEMA")")

  # 取结构化结果 + 来源文件 + 本次成本，合成一行
  jq -c --arg src "$f" '
    .structured_output
    + {source: $src, cost_usd: .total_cost_usd, session_id: .session_id}
  ' <<<"$resp"
}

# 并发处理，结果汇总
export -f process_one
export SCHEMA
find notes -name '*.md' -print0 \
  | xargs -0 -n1 -P4 -I{} bash -c 'process_one "$1"' _ {} >> "$OUT" || true

# 聚合成索引
jq -s '{generated_at: (now|todate), count: length,
        total_cost: (map(.cost_usd//0)|add), notes: .}' "$OUT" > index.json

echo "✓ 已生成 index.json（$(jq '.count' index.json) 篇，约 \$$(jq '.total_cost' index.json)）"
```

运行：

```bash
chmod +x extract-notes.sh
./extract-notes.sh
```

得到的 `index.json` 片段：

```json
{
  "generated_at": "2026-07-06T08:31:00Z",
  "count": 2,
  "total_cost": 0.0176,
  "notes": [
    {
      "title": "七月技术规划",
      "summary": "梳理本月要在后端落地的 RAG 与可观测性改造。",
      "tags": ["规划", "RAG", "监控"],
      "tech": ["FastAPI", "Postgres", "OpenTelemetry"],
      "source": "notes/2026-07-01-plan.md",
      "cost_usd": 0.0088
    }
  ]
}
```

这一份 `index.json` 可以直接喂给任意静态站点生成器做全文搜索、标签云、技术雷达。而 `.claude/cost-ledger.jsonl` 在你交互式调试时默默记下每一笔开销。两条可观测路径互补：脚本内读 `total_cost_usd`（bare 也能用），hook 读 transcript（非 bare 才触发）。

## 九、工作流程图

```mermaid
flowchart LR
    A[notes/*.md] -->|cat 管道| B[claude -p]
    S[schemas/note-card.json] -->|--json-schema| B
    B -->|--output-format json| C[结构化响应]
    C -->|jq .structured_output| D[cards.jsonl]
    C -->|.total_cost_usd| E[成本字段]
    D -->|jq -s 聚合| F[index.json]
    E --> F
    B -.会话结束.-> H[Stop hook]
    T[transcript.jsonl] --> H
    H -->|算 token| L[cost-ledger.jsonl]
    style B fill:#fef3c7,stroke:#b45309
    style F fill:#dcfce7,stroke:#15803d
    style L fill:#dbeafe,stroke:#1d4ed8
```

## 十、常见问题与排错

- **`-p` 一直卡着不出结果？** 大概率在等工具权限确认。脚本里务必加 `--allowedTools` 或 `--permission-mode`，headless 不能有人工介入。
- **`structured_output` 字段为空或缺失？** 检查是否同时传了 `--output-format json` 与 `--json-schema`；schema 过于复杂时模型可能放弃，先压扁再试。
- **批量调用烧钱太快？** 三招：① 用 `--include-partial-messages` 不可控时，改 `--output-format json` 一次性返回；② 给 prompt 加"摘要不超过 80 字"硬约束控输出长度；③ 用 `cache_read_input_tokens`，重复的 system prompt 走缓存便宜十倍。
- **想看 Claude 边想边吐？** `claude -p "..." --output-format stream-json --verbose --include-partial-messages | jq -rj 'select(.type=="stream_event" and .event.delta.type?=="text_delta") | .event.delta.text'`，逐 token 流式打印。
- **API 限流怎么办？** stream 里会发 `system/api_retry` 事件（含 `attempt`、`retry_delay_ms`、`error` 类别），脚本可据此显示重试进度或自定义退避。
- **会话被后台进程挂住？** 从 v2.1.182 起，后台子代理最多等 10 分钟（`CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS` 可调，设 `0` 则无限等）。dev server 这类永不退出的后台 shell，会在结果返回后约 5 秒被回收。

## 十一、延伸阅读

- **官方 CLI 参考**：`code.claude.com/docs/en/cli-reference`——所有 flag 的权威清单，`--append-system-prompt`、`--resume`、`--permission-mode` 都在此。
- **Hooks 指南**：`code.claude.com/docs/en/hooks-guide`——从 PreToolUse、PostToolUse、UserPromptSubmit 到 Stop、SessionStart 的快速上手与示例。
- **Agent SDK**：`code.claude.com/docs/en/agent-sdk/overview`——当 CLI 不够用，需要用 Python/TypeScript 做结构化输出回调、工具审批回调时，升级到 SDK。
- **GitHub Actions 集成**：`code.claude.com/docs/en/claude-code-github-actions`——把本文的管道搬进 CI，PR 自动出索引。
- **Prompt caching**：在 system prompt 上开缓存，批量调用的成本能再降一个数量级，与本文的成本账本 hook 配合食用最佳。

## 十二、训练营适用性评估

- **适用类型**：中高级开发者训练营、DevOps/SRE 自动化训练营、AI 工程实战营。不适合纯零基础——需要对 shell 管道、`jq`、JSON Schema 有基本概念。
- **教学时长**：建议 90–120 分钟。其中 30 分钟讲透 `-p` + `--json-schema`（核心难点是让学员理解"结构化输出契约"），40 分钟动手跑通批量管道，20 分钟做成本 hook 与可观测性，剩余时间做扩展练习（换 schema、接静态站点）。
- **练习梯度**：① 单篇提取 → ② 批量串行 → ③ 并发 `xargs -P` → ④ 加成本 hook → ⑤ 把 `index.json` 接进一个现成搜索页。每一步都有可验证的产出（一个 JSON 文件），适合做闯关式训练。
- **评估方式**：以 `index.json` 的 schema 合规率、`cost-ledger.jsonl` 的总成本、并发下的正确性作为评分维度，比"有没有跑通"更能检验对管道与契约的理解。

## 参考来源

1. [Run Claude Code programmatically — Claude Code Docs](https://code.claude.com/docs/en/headless) — 官方 `-p`/`--bare`/`--output-format`/`--json-schema` 权威文档，本文命令与字段均出自此处
2. [Hooks reference — Claude Code Docs](https://code.claude.com/docs/en/hooks) — hooks 事件、配置 schema、JSON 输入输出与退出码约定的官方参考
3. [Automate actions with hooks — Claude Code Docs](https://code.claude.com/docs/en/hooks-guide) — hooks 快速上手与示例，Stop/UserPromptSubmit 等
4. [Claude Code Headless Mode: The Definitive Guide](https://www.buildthisnow.com/blog/guide/development/claude-code-headless-mode) — `claude -p` 的 `jq` 解析、stdin 管道、实战模式第三方详解
5. [Headless Mode and CI/CD Tutorial — SFEIR Institute](https://institute.sfeir.com/en/claude-code/claude-code-headless-mode-and-ci-cd/tutorial/) — 把 headless 模式接入 CI/CD 流水线的实战教程

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