把 Claude Code 接进脚本和 CI:claude -p 的输出格式、权限和退出码
claude -p 让 Claude Code 不带界面运行,可以放进脚本和 CI。本站实测了管道、JSON 输出、按结构返回、dontAsk 加允许列表、轮数上限和 --bare。要留意:操作被拒绝时退出码仍是 0;--bare 把一次最简单调用的估算花费从 0.14 美元降到 0.008 美元。附验证过的检查脚本。
本站已实测,测试日期:2026-10-12。
本页目录(14 节)
这篇教程解决一个问题:想让 Claude Code 在没人盯着的地方干活,比如一个脚本、一个 git 钩子、一条 CI 流水线,该怎么调用它,怎么拿到结果,怎么保证它不乱动东西。
入口是 -p(也可以写 --print)。加上它,Claude Code 不打开界面,跑完把结果打到标准输出就退出,用起来和别的命令行工具一样。
要接进自动化,光会调用不够,还得弄清三件事:结果怎么让程序读,没人点确认的时候它能做什么,怎么判断这一次跑成了没有。下面逐项实测。
| 实测环境 | |
|---|---|
| 日期 | 2026-10-12 |
| 版本 | Claude Code 2.1.296 |
| 系统 | Ubuntu 24.04 |
| 模型 | Opus 5.5 |
| 连接方式 | 通过酷站AI 中转站,不是直连官方 |
| 示例仓库 | Express 的源码,v5.3.0,分支上有上一篇重构教程留下的两个提交 |
最基本的用法
claude -p "这个仓库是做什么的?只用一句话回答。"
这是 Express.js 框架本身的源码仓库(v5.x),它是一个基于 Node.js 的轻量级 Web 框架,
把路由、请求体解析、静态文件服务等许多小型 pillarjs/jshttp 包组合在一起。
用时 8 秒,退出码 0。它和交互式会话加载的东西一样:CLAUDE.md、skill、hooks、MCP 服务器都在。
用管道把数据送进去
-p 会读标准输入,所以可以把别的命令的输出直接接给它:
git diff e55e6c9..HEAD -- lib/express.js | claude -p "这段 diff 除了把 var 换成 const 或 let 之外,还有没有别的改动?有就逐条列出来,没有就只回答两个字:没有。"
没有。
数据走管道进去,它就不需要读文件、跑命令的权限。官方文档说明,管道输入的上限是 10MB,超过会报错退出,更大的内容要先写进文件,在提示词里给路径。
我们踩到的一个坑:管道前面的命令失败了
第一次跑的时候,我们把 diff 的范围写错了,git diff 报错、没有任何输出。claude -p 照样跑了,而且没有因为拿不到 diff 就停下:它自己去跑 git 命令,比对了最近的两个提交,给了一个看上去很完整的回答。
fatal: bad revision 'demo...HEAD'
有一处,其余都是单纯把 `var` 换成 `const` 或 `let`(我比对的是 `e55e6c9..HEAD` 这两个
refactor 提交,工作区本身没有改动):
……
这次它找对了,但在脚本里这是个隐患:你以为它审的是你送进去的那段内容,实际上它审的是自己找来的东西。两个办法防住它:
- 脚本开头写
set -o pipefail,并在调用之前检查要送进去的内容是不是空的。 - 用下面讲的权限设置,让它没有自己去找的能力。
让程序能读结果:--output-format json
默认输出是纯文本。加上 --output-format json,得到一个 JSON 对象:
claude -p "只回复两个字:收到" --output-format json | jq '{type, subtype, is_error, result, num_turns, duration_ms, total_cost_usd, session_id}'
{
"type": "result",
"subtype": "success",
"is_error": false,
"result": "收到",
"num_turns": 1,
"duration_ms": 4372,
"total_cost_usd": 0.141118,
"session_id": "ac241e15-c131-4720-bf97-4ee40384625c"
}
脚本里常用的几个字段:
| 字段 | 用处 |
|---|---|
result |
回答的文本。jq -r '.result' 取出来 |
is_error、subtype |
这一次有没有出错,出的是哪一类 |
session_id |
之后接着问要用 |
total_cost_usd |
这一次的花费估算 |
num_turns |
用了几轮 |
permission_denials |
有哪些操作被拒绝了,下面会用到 |
usage、modelUsage |
token 用量,按模型分 |
total_cost_usd 是 Claude Code 在本地按官方价格估的,官方文档写明它可能和实际账单不同。通过中转站用的话,实际扣多少以中转站的控制台为准。
注意这个数:只让它回两个字,估算是 0.14 美元。usage 里能看到原因,这一次写入缓存的输入有 28,206 个 token,全是它自己的系统提示词、工具说明和项目的 CLAUDE.md。这笔固定开销可以降下来,见后面 --bare 一节。
按你定的结构返回:--json-schema
让它回答“OK 或 FAIL”再用字符串去匹配,是不牢靠的。更稳的办法是给一个 JSON Schema,它会返回符合这个结构的数据,放在 structured_output 字段里:
claude -p "列出 lib/utils.js 里用 exports. 导出的所有名字。" \
--output-format json \
--json-schema '{"type":"object","properties":{"exports":{"type":"array","items":{"type":"string"}},"count":{"type":"integer"}},"required":["exports","count"]}' \
| jq '.structured_output'
{
"exports": [
"methods",
"etag",
"wetag",
"normalizeType",
"normalizeTypes",
"compileETag",
"compileQueryParser",
"compileTrust",
"setCharset"
],
"count": 9
}
我们用 grep 对了一遍,9 个名字一个不差。这一次它用了 3 轮(要先读文件),估算 0.15 美元。
Schema 写得不合法时,官方文档说它会直接报错退出,不会悄悄返回一段普通文本。
接着上一次问
每次 claude -p 都是一个新会话。要接着问,记下 session_id,下一次用 --resume:
sid=$(claude -p "列出 lib/utils.js 导出的所有名字。" --output-format json | jq -r '.session_id')
claude -p --resume "$sid" "这些名字里,哪一个是用来处理 Content-Type 的 charset 的?只回答名字。" --output-format json \
| jq '{result, total_cost_usd}'
{
"result": "setCharset",
"total_cost_usd": 0.2984764
}
第二次没有重新读文件,直接答了。还有一点要知道:接着问的那一次,total_cost_usd 报的是整段对话的累计(上一次 0.154,这一次加到 0.298),不是这一次单独的花费。按次记账的脚本要自己做减法。
只想接最近的一次,可以用 --continue。-p 跑出来的会话不出现在交互式的会话列表里,见会话和上下文。
没人点确认的时候,它能做什么
这是接进自动化最要紧的一节。
交互式会话里,没把握的操作会弹确认框。-p 没有人可问,所以要事先定好规矩。先看不定规矩时是什么样:流式输出的第一个事件里写着这次运行的权限模式,我们这里是
{"model":"claude-opus-5-5","permissionMode":"auto", ...}
也就是说,什么都不指定时,它在 auto 模式下跑:改文件、跑命令都不问,由分类器把关。官方文档也提醒了这一点,并建议明确传你想要的模式。前面那个“自己去跑 git 找 diff”的坑,根子就在这里。
dontAsk:没有事先允许的,一律拒绝
claude -p "在 lib/view.js 的第一行前面加一行注释 // touched,然后告诉我加上了没有。" \
--permission-mode dontAsk --output-format json
文件没有被改。返回的 JSON 里(节选):
{
"is_error": false,
"subtype": "success",
"result": "没加上。`lib/view.js` 没有改动。\n\n我用 Edit 工具在第一行前面插入 `// touched` 时,权限被拒绝了。当前 Claude Code 运行在 don't ask 模式,不允许编辑文件。我没有改用 `sed` 之类的命令绕过去写文件,因为那样等于避开了这次拒绝。……",
"permission_denials": [
{ "tool_name": "Edit", "tool_input": { "file_path": "/home/ccdemo/demo/d3-headless/lib/view.js", ... } }
]
}
这里有一个必须记住的结果:事没办成,但退出码是 0,is_error 是 false。 对 Claude Code 来说,这一次运行本身是正常结束的,它如实汇报了“没加上”。所以脚本不能只看退出码,要么检查 permission_denials 是不是空的,要么用 --json-schema 让它返回一个明确的成败字段。
dontAsk 下仍然能做的事,按官方文档:读工作目录里的文件、跑只读的那一组命令,以及你用允许列表放行的操作。
加一份允许列表
--allowedTools 写上这次任务要用的工具。我们让它迁移一个文件,只放行读、改、跑 ESLint 和跑测试:
claude -p "把 lib/view.js 里的 var 迁到 const 和 let,对外行为不能变。改完用 npx eslint 检查这个文件,再用 npx mocha --require test/support/env test/app.render.js test/res.render.js 跑测试。不要提交。最后只回答 OK 或 FAIL,后面跟一句原因。" \
--permission-mode dontAsk \
--allowedTools "Read" "Edit" "Bash(npx eslint *)" "Bash(npx mocha *)" \
--output-format json
65 秒后返回,17 轮,估算 0.43 美元:
{
"is_error": false,
"result": "OK:`lib/view.js` 里的 `var` 已全部改成 `const`/`let`(只有重新赋值的变量用 `let`),`npx eslint` 没有报错,两个渲染测试文件 42 个用例全部通过,没有提交。",
"permission_denials": [
{ "tool_name": "Bash", "tool_input": { "command": "sed -i -E -e '64s/var /let /;105s/var /let /;…" } }
]
}
permission_denials 里有一条:它中途想用 sed 批量替换,这条命令不在允许列表里,被拒绝了,它随后改用 Edit 工具逐处修改,把事做完了。我们自己核对了结果:diff 是 26 行增、26 行删,文件里没有 var 了,ESLint 通过,42 个用例通过。
允许列表的写法是权限规则的语法。两点容易写错,来自官方文档:
Bash(npx mocha *)末尾的*表示前缀匹配,放行所有以npx mocha开头的命令。*前面的空格不能省。Bash(git diff *)只匹配git diff加参数;写成Bash(git diff*),git diff-index也会被放行。
文档里还有一个参数 --permission-prompts none,用在有“权限宿主”的场合(Agent SDK 应用,或者用 --permission-prompt-tool 指定了一个工具来回答确认)。它让 Claude Code 不去等宿主,凡是需要问的直接拒绝。普通的 -p 调用本来就没有宿主。这个参数我们没有试。
给它设上限
| 参数 | 作用 | 实测 |
|---|---|---|
--max-turns <N> |
最多跑几轮 | 试了,见下 |
--max-budget-usd <金额> |
估算花费到这个数就停 | 没有试 |
--max-turns 1 配一个要读文件的任务,一轮做不完:
claude -p "读 lib/request.js,告诉我里面定义了多少个 getter。" --max-turns 1 --output-format json
{
"is_error": true,
"subtype": "error_max_turns",
"result": null,
"terminal_reason": "max_turns"
}
这一次退出码是 1。
关于 --max-budget-usd,官方文档有两点提醒:它比对的是本地的估算,不是账单;实际花费可能略超过上限,要留余量。
退出码到底说明什么
把上面几次放在一起:
| 情况 | 退出码 | JSON 里 |
|---|---|---|
| 正常完成 | 0 | is_error: false,subtype: "success" |
| 操作被权限拒绝,它如实汇报“没做成” | 0 | is_error: false,permission_denials 不为空 |
达到 --max-turns |
1 | is_error: true,subtype: "error_max_turns" |
| 参数写错 | 1 | 没有 JSON,错误信息打到标准错误 |
参数写错时是这样:
error: option '--output-format <format>' argument 'xml' is invalid. Allowed choices are text, json, stream-json.
结论:退出码只能告诉你“这次运行有没有正常结束”,不能告诉你“你要它办的事办成了没有”。后者要从输出里判断。
--bare:不加载项目和个人的配置
加上 --bare,Claude Code 跳过自动发现:hooks、skill、自定义命令、子代理、插件、MCP 服务器、自动记忆、CLAUDE.md 都不加载,只留下跑命令、读文件、改文件三样工具。
同一句“只回复两个字:收到”,两种方式的对比:
不加 --bare |
加 --bare |
|
|---|---|---|
| 写入缓存的输入 | 28,206 token | 1,608 token |
| 估算花费 | 0.141 美元 | 0.008 美元 |
差了十几倍。紧接着再跑一次 --bare,那 1,608 个 token 走的是缓存读取,估算只有 0.0004 美元。
代价是它不认识你的项目了。我们问了同一个问题,并且不许它读文件:
$ claude --bare -p "这个项目的提交说明要求用什么前缀?不要读任何文件,不知道就回答不知道。"
不知道。我没有读这个项目的任何文件,也没有关于它提交规范的信息,所以说不出要求用什么前缀。
$ claude -p "这个项目的提交说明要求用什么前缀?不要读任何文件,不知道就回答不知道。"
按 CLAUDE.md 里的工作约定,提交说明的前缀只能用这三个之一:
- `fix:`
- `docs:`
- `test:`
……
所以怎么选很清楚:任务需要项目的约定和工具时不加;只是把一段内容送进去让它判断、总结、提取,加上。需要的上下文可以用参数单独给:--append-system-prompt 加系统提示词,--settings 加设置,--mcp-config 加 MCP 服务器。
用 --bare 还有两个理由,来自官方文档:
- 每台机器上的结果一致。 同事个人目录里的 hook、项目里的
.mcp.json都不会被读到。 - 安全。 不加
--bare时,-p会运行项目.claude/settings.json里的 hooks、连接.mcp.json里的服务器,而且不弹信任确认,哪怕这个目录你从没信任过。在 CI 里跑别人提交的代码,这一点要当心。
文档还说,--bare 是脚本调用的推荐方式,以后会成为 -p 的默认行为。
通过中转站时的凭据
官方文档说,--bare 不读登录信息和系统钥匙串,要用环境变量 ANTHROPIC_API_KEY,或者在 --settings 里给。
我们的环境里,接口地址和凭据写在 ~/.claude/settings.json 的 env 里。实测 claude --bare -p 不加任何别的参数就能正常调用,说明这个版本在 bare 模式下仍然读了用户设置文件里的 env。这是这一个版本上看到的行为,文档没有保证。另外文档对 bare 模式写的变量是 ANTHROPIC_API_KEY,我们的凭据放在 ANTHROPIC_AUTH_TOKEN 里,也能用。
放进 CI 的话,稳妥的做法是在运行环境里明确给出接口地址和凭据,不依赖某个设置文件恰好被读到。两个变量怎么选见Claude Code 接入中转站。key 放在 CI 平台的密钥管理里,不要写进仓库。
流式输出
长任务想边跑边看,用 --output-format stream-json,每行一个 JSON 事件。最简单的一次运行只有三行:
1 system/init
1 assistant
1 result/success
system/init:这次运行的环境。模型、权限模式、工作目录、可用的工具、MCP 服务器、加载的插件和 skill 都在里面。CI 里可以用它检查该加载的东西加载了没有。assistant:模型的每条回复,包括工具调用。result:最后一行,内容和--output-format json的输出一样。
要逐字输出,再加上 --verbose --include-partial-messages,这个我们没有试。
一个完整的例子
把上面几样拼起来,做一个放在 CI 里的检查:一个分支声称“只做了某件事”,让它核对 diff 有没有超出范围。超出就让流水线失败。
#!/usr/bin/env bash
# 用法:check-scope.sh <基准提交> "<这次改动声称只做了什么>"
set -euo pipefail
base="$1"; claim="$2"
diff=$(git diff "$base"...HEAD)
if [ -z "$diff" ]; then echo "没有改动,跳过"; exit 0; fi
schema='{"type":"object","properties":{"ok":{"type":"boolean"},"issues":{"type":"array","items":{"type":"string"}}},"required":["ok","issues"]}'
out=$(printf '%s' "$diff" | claude --bare -p \
"标准输入是一段 git diff。这次改动声称:${claim}。逐个文件检查有没有超出这个范围的改动。ok 为 true 表示没有超出范围;有的话 ok 为 false,并在 issues 里逐条写出 文件:行号 和问题。" \
--permission-mode dontAsk \
--max-turns 3 \
--output-format json \
--json-schema "$schema")
if ! echo "$out" | jq -e '.is_error == false and .structured_output != null' > /dev/null; then
echo "检查没有跑成:$(echo "$out" | jq -r '.subtype')"; exit 2
fi
echo "花费估算:$(echo "$out" | jq -r '.total_cost_usd') 美元"
if echo "$out" | jq -e '.structured_output.ok' > /dev/null; then
echo "通过:改动没有超出范围"
else
echo "不通过:"
echo "$out" | jq -r '.structured_output.issues[]'
exit 1
fi
每一处写法都对应前面的一个结论:先检查 diff 是不是空的;内容走管道,不给它自己找的机会;--bare 省掉固定开销,也不受仓库里配置的影响;dontAsk 加上不给任何允许列表,它什么都改不了;--max-turns 封顶;成败看 structured_output.ok,不看退出码;“没跑成”和“不通过”用不同的退出码分开。
在重构教程留下的分支上跑:
$ ./check-scope.sh e55e6c9 "只把 var 换成 const 或 let,行为不变"
花费估算:0.0438072 美元
通过:改动没有超出范围
然后我们故意在一个提交里混进一行无关的改动,再跑:
$ ./check-scope.sh e55e6c9 "只把 var 换成 const 或 let,行为不变"
花费估算:0.037191 美元
不通过:
lib/express.js:38 — 这一行除了把 var 换成 const,还在 app.handle(req, res, next); 后面加了一句
app.locals.touched = true;。每次处理请求都会多写一个 app.locals 属性,行为变了,超出了范围。
退出码是 1,行号是对的。每次四分钱左右。
要说明它的局限:这是一个模型的判断,不是确定性的检查。它适合当一道额外的提醒,不能代替测试和人的审查。同一个 diff 多跑几次,结论有可能不同,我们每种情况只跑了一次。
这一趟花了多少
这篇前后一共 17 次调用,按各次的 total_cost_usd 相加,合计约 2 美元(有四次没有记下数字,按同类调用估的)。其中不加 --bare 的调用,哪怕只回两个字,每次也在 0.14 美元上下;加了 --bare 的检查脚本每次 0.04 美元。这些都是 Claude Code 按官方价格估算的数字,不是中转站实际扣的金额。
这次实测没有覆盖到的
--max-budget-usd、--permission-prompts none、--include-partial-messages、--append-system-prompt、--input-format stream-json没有试,照官方文档写的。- 没有真的放进一条 CI 流水线。 脚本是在服务器的终端里跑的。GitHub Actions 里的做法见下一篇。
- bare 模式读用户设置里的凭据,只在这一个版本上看到,文档没有这样写,不要依赖它。
- Python 和 TypeScript 的 Agent SDK 没有涉及。 这篇只讲命令行。
- 每种情况只跑了一次。 模型在被拒绝之后会不会换一种办法绕过去,这次两回都没有,但这不是保证。真正的保证来自权限设置本身。
- macOS 和 Windows 没有测。
下一步
- 系列的下一篇:用 GitHub Actions 让 Claude Code 审 PR、修 issue。
- 几种权限模式的完整说明:权限模式怎么选。
- 大批文件的迁移,用循环调
claude -p的思路:重构和迁移。 - 通过中转站用时哪些功能要留意:通过中转站用 Claude Code。