完整课件

5.2 headless 脚本化:claude -p 进流水线

课型:实战课 | 难度:⭐⭐⭐⭐ | 前置课:第 3 篇(建议先修 1.8 速查表①:高频命令、快捷键与 CLI 参数) | 预估学习时长:40-60 分钟 | 本课交付物:一个接进 package.json/CI 的 AI 审查脚本

到现在为止,你用 Claude Code 的方式都是「你说一句、它做一步、弹窗你点允许」——但 CI 流水线里没有人守着点「允许」,pre-commit 钩子也不会等你聊完天。headless(非交互)模式解决的就是这一个问题:给命令加一个 -p 参数,Claude Code 就从聊天窗口变成一个标准命令行工具——stdin 进、stdout 出,能被管道、脚本和 CI 像调用 grep 一样调用(官方 headless 文档)。做完这课,你会得到一个接进 package.json 的 AI 审查脚本:npm run lint:claude 一敲,Claude 替你检查整个 diff。

本课命令速查

命令 / 参数作用何时用
claude -p "提示词"非交互执行,结果打到 stdout(-p--print一切脚本化场景的起点
cat 文件 | claude -p "…"从 stdin 喂数据日志分析、diff 审查
--output-format json输出结构化 JSON(含结果、会话 ID、成本)脚本要解析结果时
--json-schema '<schema>'强制输出符合指定 JSON Schema要提取字段而非读文章时
--allowedTools "Bash,Edit"免弹窗放行指定工具需要 Claude 动手改东西时
--permission-mode acceptEdits放行全部文件写入批量修改类任务
--continue / --resume <id>接着上一次/指定会话继续多步流水线共享上下文

以上参数均出自 官方 CLI referenceheadless 文档

[!important] 关于 --bare 官方推荐 CI 脚本加 --bare,跳过 hooks/MCP/记忆加载,保证每台机器结果一致(headless 文档)。但 bare 模式跳过 OAuth、只认 ANTHROPIC_API_KEY——订阅登录的机器上加 --bare 会直接报 Not logged in(本课实测,见排错表)。本课在订阅机上练习,全程不加 --bare;等你接入 CI 并配好 API Key(详见 6.1 第一次 API 调用)再加回来。

一、-p:把 Claude 变成一条管道命令

交互会话里,Claude Code 的价值是「陪你干活」;加上 -p 后,它的价值变成「无人值守地干完一件事就退出」。这就是它能进流水线的原因:流水线要的不是对话,而是输入 → 处理 → 输出 → 退出码这个标准回路。原理不展开——-p 模式跑的是同一个 agent loop,详见 1.4 Claude Code 如何工作:agent loop、内置工具与权限确认。

下面动手。整个 Runbook 在一个一次性实验项目里进行,做完即删,不碰你的真实项目。

[!note] 预期输出怎么对 本课所有「预期输出」都是作者 2026-07-03 在 Ubuntu 24.04、Claude Code 2.1.199 上的实测原文。AI 生成的文字每次措辞会有差异,自检标准是格式一致、要点一致,不是逐字相同;带 # 注释标出的行才是硬性判据。

步骤 1:确认 Claude Code 版本

引导句:headless 模式的行为随版本演进(如 stdin 上限、后台任务处理都在 2.1.x 中调整过),先确认你的版本不低于本课实测版本,后面输出对不上时才有排查基准。

命令块:

# 查看 Claude Code 版本
claude --version

预期输出:

2.1.199 (Claude Code)
# 不低于 2.1 即可跟做本课

⚠️ 如果不符:

  • 若报 command not found → Claude Code 未安装或不在 PATH,回到 1.1 安装与首次登录(macOS Windows Linux WSL)。
  • 若版本低于 2.1 → 交互模式里跑 /update 升级后重试。

步骤 2:搭一个一次性实验项目

引导句:我们需要一个干净的 git 仓库来演示「AI 审查 diff」,并故意埋两处错别字当靶子。全部文件都是一次性的,最后一并删除。

命令块:

# Step 1: 建实验目录并初始化 git
mkdir ~/demo-lab && cd ~/demo-lab
git init -b main

# Step 2: 写入演示模块并提交为基线
cat > greet.js <<'EOF'
// 一个演示用的小模块:问候语生成器
function makeGreeting(name) {
  if (!name) {
    return 'Hello, stranger!';
  }
  return `Hello, ${name}!`;
}

module.exports = { makeGreeting };
EOF
git add -A && git commit -m "初始提交"

# Step 3: 制造一个埋了错别字的未提交改动(英文 Helo + 中文"问侯语")
sed -i "s/Hello, stranger/Helo, stranger/" greet.js
sed -i "s|// 一个演示用的小模块:问候语生成器|// 一个演示用的小模块:问侯语生成器|" greet.js

# Step 4: 伪造一份构建失败日志,留给管道实验用
cat > build-error.txt <<'EOF'
$ npm run build
> demo-lab@1.0.0 build
> node build.js
Error: Cannot find module '/tmp/demo-lab/build.js'
    at Module._resolveFilename (node:internal/modules/cjs/loader:1212:15)
EOF

预期输出:

# git commit 后应看到(哈希值每人不同):
[main (root-commit) 2b08b8f] 初始提交
 1 file changed, 9 insertions(+)
# git status 应显示 greet.js 已修改、build-error.txt 未跟踪

⚠️ 如果不符:

  • git init -b main 报不认识 -b → git 版本过旧,先 git initgit branch -m main

回滚:

# 实验做完后整体删除,不留任何痕迹
rm -rf ~/demo-lab

步骤 3:第一次非交互调用

引导句:先跑最小可用的一条 -p,确认无人值守回路是通的:命令发出、Claude 回答、进程退出,全程无弹窗。

命令块:

# 在实验目录里发起第一次非交互调用
cd ~/demo-lab
claude -p "用一句话回答:Claude Code 的非交互模式用哪个命令行参数开启?"

预期输出:

非交互模式用 `-p`(等同 `--print`)参数开启,例如 `claude -p "你的提示"`。
# 实测耗时 6.8 秒;要点判据:答案提到 -p / --print 且进程自动退出

⚠️ 如果不符:

  • 若报 Not logged in · Please run /login → 你多半加了 --bare,或本机从未登录过;见文末排错表第一条。
  • 若长时间无输出 → 网络问题,先在交互模式确认能正常对话,再回来重试。

步骤 4:像用 grep 一样用管道喂数据

引导句:-p 模式会读 stdin,这意味着任何命令的输出都能直接灌给 Claude——这是它接入现有流水线的关键接口(官方 headless 文档)。用刚才伪造的构建日志试一次。

命令块:

# 把构建失败日志管道喂给 Claude 做根因分析
cat build-error.txt | claude -p "用一句中文说明这份构建日志的根本原因"

预期输出:

根本原因:`package.json` 的 build 脚本要执行 `node build.js`,但项目里根本
不存在 `build.js` 这个入口文件,Node 找不到模块便直接报错退出。
# 要点判据:指出 build.js 不存在。实测耗时 7.6 秒

⚠️ 如果不符:

  • 若 Claude 答非所问 → 确认管道左侧确实有内容(cat build-error.txt 单独跑一遍)。
  • 若报输入过大错误 → stdin 有 10MB 上限,见排错表第三条。

二、--output-format json:让脚本读懂 Claude 的回答

管道解决了「怎么喂进去」,JSON 输出解决「怎么取出来」。纯文本适合人读;脚本要判断成败、取会话 ID、算成本,就需要结构化字段(官方 headless 文档)。

步骤 5:拿到结构化 JSON 并用 jq 提取

引导句:给调用加 --output-format json,输出会变成一个 JSON 对象;配合 jq 就能在脚本里精确取用任何字段。这一步顺带认识几个流水线里最常用的字段。

命令块:

# Step 1: 以 JSON 格式提问并落盘
claude -p "用一句话总结这个项目是干什么的" --output-format json > out.json

# Step 2: 提取正文
jq -r '.result' out.json

# Step 3: 提取流水线关心的元数据(成败/成本/耗时/会话)
jq '{total_cost_usd, duration_ms, num_turns, session_id, is_error}' out.json

预期输出:

# jq -r '.result' 输出(措辞会有差异,要点:识别出 makeGreeting 模块):
这个项目是一个演示用的极简 JavaScript 模块,导出一个 `makeGreeting` 函数,
根据传入的名字生成问候语(无名字时返回默认招呼语)。

# jq 元数据输出(数值每次不同,字段名是硬性判据):
{
  "total_cost_usd": 0.21014850000000002,
  "duration_ms": 9696,
  "num_turns": 2,
  "session_id": "8a493acb-9adc-4012-98fd-0a162182fefb",
  "is_error": false
}

⚠️ 如果不符:

  • jqcommand not found → 先装 jq(Ubuntu:sudo apt install jq)。
  • .result 为 null → 看 is_errorpermission_denials 字段定位原因。

[!tip] 成本就在字段里 total_cost_usd 让脚本无需查用量面板就能逐次记账(官方 headless 文档)。上面这次两轮调用实测 $0.21——把它写进 CI 日志,月底就知道这条流水线花了多少钱。

三、组装:一个真实可用的 AI 错别字 linter

前两个知识点各解决半件事,现在拼成完整交付物。思路来自官方 build-script 示例(官方 headless 文档):把 diff 管道喂给 Claude,让它只输出问题清单。用管道喂 diff 而不是让 Claude 自己跑 git 命令,好处是全程只读、无需任何工具授权。

stdin stdout git diff main claude -p 审查提示词 问题清单 / OK CI 判定或人工处理

图:AI linter 的数据流——diff 从 stdin 进,报告从 stdout 出,Claude 全程不需要写权限。

步骤 6:把审查固化成脚本

引导句:一次性命令验证了可行性,固化成脚本 + npm script 才能被团队和 CI 复用。这一步产出本课交付物。

命令块:

# Step 1: 写审查脚本
cat > lint-claude.sh <<'EOF'
#!/usr/bin/env bash
# AI 错别字检查:把当前分支相对 main 的 diff 交给 claude -p 审查
set -euo pipefail

git diff main | claude -p "你是错别字检查器。对这份 diff 里每处错别字或拼写错误,第一行输出 文件名:行号,第二行输出问题说明。除此之外不要输出任何内容。若没有发现问题,只输出 OK。"
EOF
chmod +x lint-claude.sh

# Step 2: 注册进 package.json
cat > package.json <<'EOF'
{
  "name": "demo-lab",
  "version": "1.0.0",
  "scripts": {
    "lint:claude": "./lint-claude.sh"
  }
}
EOF

预期输出:

# 无输出即成功;ls -l lint-claude.sh 应显示可执行位 x

⚠️ 如果不符:

  • 若后续执行报 Permission denied → 漏了 chmod +x,补上即可。

回滚:

# 本步只在实验目录新增两个文件,删除即回滚
rm -f ~/demo-lab/lint-claude.sh ~/demo-lab/package.json

步骤 7:验证步——端到端跑通交付物

引导句:最后一步验证全流程:npm script 调脚本、脚本喂 diff、Claude 抓出步骤 2 埋的两处错别字。这条命令通过,本课交付物即验收合格。

命令块:

# 端到端验证:AI linter 应抓出埋好的两处错别字
npm run lint:claude

预期输出:

> demo-lab@1.0.0 lint:claude
> ./lint-claude.sh

greet.js:4
"Helo" 拼写错误,应为 "Hello"

greet.js:6
"问侯语" 用字错误,应为 "问候语"

# 硬性判据:两处错别字(Helo / 问侯语)都被指出,exit code 为 0
# 实测耗时 11.7 秒

⚠️ 如果不符:

  • 若输出 OK → Claude 没看到 diff:确认你没有把步骤 2 的改动 commit 掉(git status 应显示 greet.js 已修改)。
  • 若只抓到一处 → 重跑一次;仍漏则把提示词中"错别字或拼写错误"改得更具体(如明示"含中文用字错误")。

验证通过后,按步骤 2 的回滚命令删掉 ~/demo-lab,实验不留痕迹。

变体玩法

示例 1:--json-schema 强制结构化提取

要的不是一段话而是能直接进程序的数据时,给调用加 JSON Schema 约束,结果出现在 structured_output 字段(官方 headless 文档):

# 从代码里提取函数名,强制返回字符串数组
claude -p "列出 greet.js 导出的函数名" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"functions":{"type":"array","items":{"type":"string"}}},"required":["functions"]}' \
  | jq '.structured_output'

实测输出:

{
  "functions": ["makeGreeting"]
}
关键点说明
出口字段结构化结果在 .structured_output,不在 .result
适用场景提取清单、打标签、出评分——下游是程序而不是人

示例 2:--allowedTools 放行写操作

默认的 -p 模式里,写类工具会被静默拒绝:进程正常结束、is_error 为 false,但文件一个字没变,被拦的调用记录在 permission_denials 字段里(本课实测)。要让 Claude 真动手,显式放行:

# 明确放行 Edit 工具,Claude 才能改文件
claude -p "在 README.md 末尾追加一行:由 AI 追加" --allowedTools "Edit"
关键点说明
只读命令例外git loggit status 等默认放行(实测无需授权即可执行,官方称 read-only command set
批量写场景--permission-mode acceptEdits 整体放行文件写入(官方 headless 文档
安全边界放行规则支持前缀匹配(如 Bash(git commit *)),规则语法详见 2.8 权限系统:少打断、不失控

练习

给你自己的真实项目写一个 claude -p 审查器——错别字、残留 console.log、commit message 规范,任选一个检查目标。要求:脚本接进 package.json(或 Makefile),并附上一次真实运行的输出。把脚本和输出归档进 claude-learning-lab/scripts/lint-claude/

小结

开场那个问题——CI 里没有人点「允许」——现在有了答案:-p 让 Claude 无人值守地跑完就退出,JSON 输出让脚本读懂结果,--allowedTools 按需放权。记住三个关键词:管道化(stdin/stdout)、可解析(--output-format json)、显式放权(--allowedTools)。想一想:你的 CI 流水线里,哪个环节最适合先塞进一个 claude -p?它需要放行哪些工具?下一课把这套思路搬上 GitHub:在 PR 评论区直接 @claude。

常见报错

Not logged in · Please run /login(命令秒退,exit 1)

  • 原因:加了 --bare。bare 模式跳过 OAuth 和钥匙串,认证只认 ANTHROPIC_API_KEY官方 headless 文档);订阅登录的机器必现(本课实测,0.8 秒即失败)。
  • 解法:本地订阅机去掉 --bare;CI 环境配置 ANTHROPIC_API_KEY 后再加回 --bare

claude -p 说「已完成修改」,文件却一点没变

  • 原因:写类工具(Edit/Write/Bash 写操作)默认被权限系统拦截,且不会报错——is_error 仍为 false(本课实测)。
  • 解法:查 JSON 输出的 permission_denials 字段确认被拦的工具,然后用 --allowedTools "Edit"--permission-mode acceptEdits 放行;只读任务改用管道喂数据,绕开授权问题。

管道灌大文件时报错退出

  • 原因:自 v2.1.128 起,stdin 输入上限 10MB,超限即报错并返回非零退出码(官方 headless 文档,未实测)。
  • 解法:把内容写入文件,在提示词里引用文件路径让 Claude 自己读。

关联

  • 5.1 worktree 并行与双会话模式(上一课)
  • 5.3 GitHub Actions:在 PR 里 @claude(下一课:这套脚本化思路上云)
  • 2.8 权限系统:少打断、不失控(--allowedTools 的规则语法出处)
  • 1.4 Claude Code 如何工作:agent loop、内置工具与权限确认(-p 背后的同一个 agent loop)
  • 官方文档:以编程方式运行 Claude Code(headless) · CLI 参数完整参考

On this page