从源码看 Claude Code CLI 的执行模式和用法
这是 Claude Code 源码泄露后,结合源码整理的一手技术分析。不管你是 Claude Code 的深度用户,还是在做 Agent 相关的学习或工作,或许都能从中找到一些启发。本文是系列文章中的一篇,完整系列目录见文末「细说 Claude Code」。
本文内容脉络预览
这篇文章主要围着四个问题展开:
-
1. Claude Code CLI 为什么值得单独学 -
2. 它一共怎么运行,也就是 12 种执行模式怎么分 -
3. 脚本和自动化里,怎么控制边界,包括 --bare、权限和输出格式 -
4. 真进工程以后,哪些场景最常用
顺着这条线往下看,后面就不容易乱。
CLI 到底是什么
CLI 是 Command Line Interface,中文一般叫命令行界面。和更常见的 GUI 对着看,这个概念就很好理解了。
GUI 是图形界面,你看到的是按钮、菜单、弹窗和列表,靠鼠标去点;CLI 则是另一种方式,你面对的是终端和命令,输入一条命令,带上一组参数,程序执行后再把结果返回给你。
一句话说,GUI 是“把功能做成界面给人点”,CLI 是“把功能做成命令给人输”。像 git status、npm run build、docker ps、gh pr list,都属于 CLI。
CLI 的几个核心优点
CLI 这些年重新变热,不是因为怀旧,而是因为它在工程场景里确实很顺手。
它最核心的优点其实就四个:好串联,边界清楚,适合自动化,而且过程可追踪。上一个命令的输出可以直接接给下一个命令;输入、输出和参数通常也比较明确;脚本、定时任务、CI/CD 可以直接调用;出了问题还能顺着日志往回查。
所以 CLI 最有价值的地方,不只是让人操作工具更快,而是它也很适合让程序和 Agent 来调用工具。
为什么 Claude Code CLI 值得深究
先把一个很容易混淆的关系说清楚。
CLI 是通用概念,指的是命令行界面;Claude Code CLI 说的则不是一个独立产品,而是 Claude Code 在终端里的这套命令行形态,也就是你安装后直接执行的 claude 命令。
所以更准确的说法不是“Claude Code 搭配了 CLI”,而是:CLI 本来就是 Claude Code 的主要使用形态之一。
官方 overview 把 Claude Code 定义成一个可以运行在 terminal、IDE、desktop、browser 等多个 surface 上的产品,其中 terminal 那一栏写得很直接:“The full-featured CLI for working with Claude Code directly in your terminal.” 源码仓库说明里也写得很明确:“Claude Code is Anthropic’s CLI for interacting with Claude from the terminal.”
这也是为什么 CLI 值得单独拎出来讲。坐在终端前和 Claude Code 对话,只是它的一种用法;更重要的一种用法,是把它的命令行入口接进脚本和自动化流程里。
比如在 GitHub Actions 里自动 review PR,写一个 cron 脚本定时跑代码检查,嵌进发布流程做上线前扫描,或者写批处理脚本逐个文件分析代码。到了这一步,理解 Claude Code 的 CLI 执行模式和参数边界,就不是“多学几个命令”,而是理解这套能力怎么进入工程流程。
很多重度用户重新回到 CLI,往往也不是因为“命令行更酷”,而是因为它更容易接进真实工作流。大家真正在意的,基本也都是工程问题:能不能接进 GitHub Actions、shell 脚本、定时任务;能不能在终端里持续推进同一个任务;能不能把 worktree、resume、SDK、自动化调用串起来。
归根到底,CLI 的价值,不是多了一堆命令,而是它把 Claude Code 变成了一个能直接接进工程流程的入口。
1. 它不是“外面包了一层壳”,而是 Claude Code 的终端入口
从源码结构看,main.tsx 不是单纯解析一下参数就结束了,它本身就是整个启动编排中心:解析 Commander.js 参数、初始化配置、装载 commands、tools、skills、plugins,再决定是进入交互对话模式还是非交互模式。
而 commands.ts 里注册的也不只是聊天相关命令,还包括:
-
• install-github-app -
• skills -
• memory -
• mcp -
• teleport -
• desktop/mobile -
• remote-control
这说明 CLI 在 Claude Code 里不是“附属玩法”,而是最核心的终端入口之一。
如果再拆开一点看,可以把关系理解成这样:
-
• Claude Code是产品和运行时能力本身 -
• CLI、VS Code、JetBrains、Desktop、Web 是它暴露给用户的不同使用入口 -
• Claude Code CLI则是这些入口里最底层、也最完整的一种
2. 它也是终端、自动化和 SDK 之间的共同接口
这里拿 GitHub Actions 举个例子。Claude Code 一旦不是“人坐在终端前手动敲命令”,而是被工作流自动触发,底层调用的仍然还是同一套 CLI。
GitHub Actions 可以先简单理解成:GitHub 自带的自动化流水线系统。 你在仓库里写好规则后,它会在 PR、push、定时任务这些时机自动起一台 runner,按你写好的步骤执行命令。Claude Code 接进这个流程,本质上就是让一台机器去跑 claude。
比如你可以配一条规则:每次有人提交 PR,就自动执行一条命令,让 Claude Code 读取这次改动并给出 review 意见。你在终端里手动跑,可能是 gh pr diff | claude --bare -p "review this diff";放进 GitHub Actions 之后,本质上也还是这条命令,只不过从“你自己敲”变成了“GitHub 在 runner 上替你敲”。
官方文档其实说得很直接:在 GitHub Actions 里,claude_args 的意思基本就是你平时在终端里怎么写 claude 命令,这里就怎么把参数传进去。同时,官方也明确建议:只要是脚本调用,或者是 SDK 在后台调用,最好都用 --bare。而 Agent SDK 也不是另一套新东西,它复用的还是 Claude Code 原来的 tools、agent loop 和上下文管理。
把这三件事放在一起看,意思就很明确了:终端里手动运行、GitHub Actions 里自动运行、SDK 里程序化调用,底下复用的是同一套机制。
所以 CLI,不只是了解在终端里敲几条命令,而是在学 Claude Code 这套能力怎么被脚本、自动化系统和程序调用。这也是为什么 GitHub Actions、SDK、终端三条线,最后会在 -p、--output-format、--allowedTools、--resume、--bare 这些参数上汇合。
3. 有些能力放在 CLI 里,天然就比图形界面更顺手
比如下面这些场景,IDE 插件不是做不到,但 CLI 明显更顺手:
-
• 直接吃 stdin: git diff | claude -p ... -
• 把输出接给 jq、xargs、shell script -
• 在 CI 里做 review、汇总、批处理 -
• 用 --worktree在隔离分支里开新会话 -
• 用 --resume <session-id>把多轮自动化串成一条流水线 -
• 用 stream-json接成机器可消费的事件流
这些都不是“聊天更方便一点”的差别,而是从对话工具变成工程组件的差别。
4. 如果想看清楚 Claude Code 的边界,CLI 也是最直接的入口
在插件里,很多能力被 UI 包起来了,你只会感受到“它能做什么”;但在 CLI 里,你会被迫直接面对:
-
• 什么时候进入非交互模式 -
• 权限是怎么放开的 -
• Session 怎么恢复 -
• 自动发现和确定性之间怎么权衡 -
• JSON 输出怎么给程序消费
所以 CLI 单独拿出来讲,不是因为命令多,而是因为它最接近 Claude Code 本身的运行方式。
Claude Code 怎么运行:12 种执行模式
先把“12 种”这件事说清楚。
这里的 12,指的不是把所有参数、自动判定逻辑和底层行为全都算进去,而是只按你怎么启动 Claude Code 来数。按这个口径,刚好就是 12 种:6 种交互模式,6 种非交互模式。
后面会讲到的 --bare、stdout 是否是 TTY、stdin 管道输入,也都很重要,但它们影响的是这些模式怎么运行,不是额外再长出一种新模式。
简单说,交互模式用于持续协作,非交互模式用于脚本、CI 和程序调用。
交互模式(你坐在终端前对话)
|
|
|
|
|---|---|---|
|
|
claude |
|
|
|
claude "explain this" |
|
|
|
claude -c |
|
|
|
claude -r auth-refactor |
|
|
|
claude -w feature-auth |
|
|
|
claude --agent code-reviewer |
|
非交互模式(被脚本或 CI 调用)
|
|
|
|
|---|---|---|
|
|
claude -p "query" |
|
|
|
cat file | claude -p "query" |
|
|
|
claude -p --output-format json |
|
|
|
claude -p --output-format stream-json |
|
|
|
claude -p --json-schema '{...}' |
|
|
|
claude -p -c "followup" |
|
claude "query" 和 claude -p "query" 差一个 -p,效果完全不同。
claude "explain this" → 进入交互对话模式,处理完等你继续对话claude -p "explain this" → 输出结果后进程退出
前者是”帮我做这个,然后咱们接着聊”。后者是”帮我做完这个就行了”。
两个容易混的细节
main.tsx 里的分叉逻辑很清晰:
const isNonInteractive = hasPrintFlag || // 你显式加了 -p hasInitOnlyFlag || // --init-only hasSdkUrl || // SDK 模式 !process.stdout.isTTY // stdout 被重定向了
最后一条很实用:如果 stdout 不是终端(比如 claude "query" > output.txt),自动进入非交互模式。 你写脚本时不需要显式加 -p,只要输出被管道重定向就会自动切换。
另一个紧挨着的细节,是管道输入怎么处理。
stdin 管道输入处理
cat error.log | claude -p "解释这个错误"
处理流程大致是:
-
1. 检测 !process.stdin.isTTY(stdin 不是终端 = 有管道输入) -
2. 读取 stdin 全部内容(不是流式的,大文件注意内存) -
3. 把 stdin 内容和 -p后面的 prompt 合并成一个完整的用户消息 -
4. 发给模型
源码中还有个 peekForStdinData() 函数,会在 3 秒后发出告警——如果 stdin 在那个时间段内没有数据到来,说明可能是误操作(比如你打了 claude -p 但忘了管道输入,程序就挂在那等 stdin)。
脚本和自动化里,怎么把边界收清楚
前面讲的是“怎么运行”,下面这部分更关心“怎么把行为收住”。到了脚本、CI、SDK 这些场景,真正重要的无非三件事:运行环境尽量确定、权限边界尽量明确、输出结果尽量能被程序直接消费。
--bare 模式:确定性比便利性重要
--bare 在源码里只做了一行事:
process.env.CLAUDE_CODE_SIMPLE = '1'
但这一行关闭了整个自动发现体系:
|
|
|
|
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/skill-name 仍可解析 |
|
|
|
|
|
|
|
|
|
|
|
|
为什么 CI 必须用 –bare
因为需要确定性。
你写了一个 CI 脚本在 GitHub Actions 里跑。你的机器上有一个 PostToolUse hook 会自动跑 lint,同事小明的机器上没有。项目里有一个 .mcp.json 配置了一个 MCP server,但 CI runner 上这个 server 装没装不确定。
不加 --bare,同一个脚本在不同环境上可能:
-
• 因为 hook 的有无产生不同结果 -
• 因为 MCP server 连不上而报错 -
• 因为某个人的 ~/.claude/CLAUDE.md里有特殊指令而行为不一致
加了 --bare,一切都确定了——只有你显式传入的参数才生效。
Anthropic 官方也说了:
--bareis the recommended mode for scripted and SDK calls, and will become the default for-pin a future release.
这里还有个容易忽略的源码细节:main.tsx 的 --bare 帮助文案明确写了 “Skills still resolve via /skill-name”。也就是说,--bare 关掉的是自动发现和环境装配,不是把一切能力都彻底砍掉;你显式传入的上下文和显式点名的能力,仍然可以继续用。
踩坑案例
# ❌ 坑:bare 模式下 Claude 不知道你的项目用 pnpmclaude --bare -p "install and test" --allowedTools "Bash"# Claude 会跑 npm install,因为它没有 CLAUDE.md 告诉它用 pnpm# ✅ 修复:手动传入关键指令claude --bare -p "install and test" --allowedTools "Bash" \ --append-system-prompt "This project uses pnpm. Run pnpm install and pnpm test."
跳过了但可以手动补回
--bare 不是”什么都没有”,只是关闭自动发现。你可以通过 flag 手动加回需要的部分:
|
|
|
|---|---|
|
|
--append-system-prompt-file ./CLAUDE.md |
|
|
--system-prompt-file ./prompt.txt |
|
|
--settings ./settings.json |
|
|
--mcp-config ./mcp.json |
|
|
--agents '{"reviewer":{...}}' |
|
|
ANTHROPIC_API_KEY
|
权限控制:先分清三层
如果从使用上归类,权限控制其实就三层:
-
• 用 --permission-mode控制整体策略 -
• 用 --allowedTools/--disallowedTools控制工具边界 -
• 用 --dangerously-skip-permissions决定要不要彻底绕过权限系统
先把这三层分清,再看具体参数,就不容易乱。
几个关键权限参数
如果只看命令行参数,权限控制相关的核心开关其实就是下面这几组。
1. --dangerously-skip-permissions
这是最激进的开关,作用是直接跳过所有权限检查,不是“少问几次确认”。
源码也明确把它限制在很窄的范围内:不能在普通 root/sudo 环境直接用,通常只允许 Docker / sandbox 这类隔离、无外网环境。日常本机开发基本不该开;只有在受控容器里做全自动执行时,才有考虑价值。
2. --allow-dangerously-skip-permissions
这个参数容易和上面混。区别只有一句话:
-
• --dangerously-skip-permissions:一启动就彻底跳过权限检查 -
• --allow-dangerously-skip-permissions:先不开,但允许会话之后切到bypassPermissions
也就是前者是“现在就全放开”,后者是“允许之后再全放开”。
3. --permission-mode <mode>
这是权限模式的总开关。源码里定义的公开模式有 5 个:
-
• default:标准模式,危险操作时正常询问 -
• acceptEdits:自动接受文件编辑类操作 -
• plan:只规划,不真正执行工具 -
• dontAsk:不弹确认;凡是没被预先允许的操作,直接拒绝 -
• bypassPermissions:跳过所有权限检查
实际记法可以更简单:
-
• dontAsk是“不问你,但也不会乱来,没授权就拒绝” -
• bypassPermissions是“权限系统整体绕过”
所以日常开发通常用 default / acceptEdits,脚本里常用 dontAsk,只有完全沙箱化的环境才会碰 bypassPermissions。
4. --allowedTools 和 --disallowedTools
这两个是最实用的日常控制项:
-
• --allowedTools:只放行你点名允许的工具 -
• --disallowedTools:明确禁掉某些工具
它们本质上是工具级白名单 / 黑名单,通常比直接切激进权限模式更值得优先使用。常见用法:
-
• 只读审查: --allowedTools "Read,Grep,Glob" -
• 允许改代码但不允许乱跑命令:给 Read,Edit,不给Bash -
• 允许 Bash,但只允许某类命令:用更细的工具匹配规则去限制 Bash
平时如果只先记一组权限参数,通常就是这两个。
5. --permission-prompt-tool
这个参数不是放权,而是把权限确认这件事交给一个 MCP 工具处理。它只在 --print 下生效,适合 CI、平台集成、SDK 这类不方便人工盯着确认框的场景。
怎么选
-
• 想默认安全一点:用 --permission-mode default -
• 想不弹确认但也不胡来:用 --permission-mode dontAsk配合--allowedTools -
• 想让 Claude 自动改文件:用 acceptEdits或者直接放开特定编辑工具 -
• 想把某些工具彻底锁死:用 --disallowedTools -
• 想在受控沙箱里彻底跳过检查:才考虑 --dangerously-skip-permissions
要是只记一个原则,就是:优先用白名单和权限模式收边界,只有在高度受控环境里,才考虑彻底跳过权限检查。
怎么让输出结果被程序直接消费
前面是控制运行边界,接下来是控制输出形态。对脚本和平台来说,重点通常不是“Claude 回答得像不像人”,而是结果能不能稳定接走。
stream-json:把执行过程变成机器可读的事件流
stream-json 是给程序实时消费 Claude 执行过程用的输出格式。
和普通 json 输出相比,stream-json 不是等到最后一次性吐结果,而是把 Claude 执行过程里的每一步,都实时写成一行 JSON 往外发。
这东西不是主要给人看的,而是给程序接的。你如果只是自己在终端里看结果,通常用不上它;但如果你要做 SDK、网页前端、自动化平台,或者想实时看到 Claude 正在干什么,它就很重要。
可以把它理解成一条机器可读的事件流:会话开始、模型输出、工具调用、重试、最终结果,都会一条一条发出来。下面这些是最重要的几类事件:
|
|
|
|
|---|---|---|
system/init |
|
|
stream_event
text_delta |
|
|
assistant
|
|
|
user
tool_result |
|
|
system/api_retry |
|
|
system/status |
|
|
system/hook_* |
|
--verbose) |
result |
|
|
注意:text_delta 需要 --include-partial-messages 才会出现。不加这个 flag 你只能看到完整消息,没有逐 token 的流。
源码里有一个工程细节值得说:installStreamJsonStdoutGuard() 会把所有非 JSON 的 stdout 重定向到 stderr。为什么?因为第三方库的 console.log 会破坏你的 NDJSON 解析——一行 console.log("debug") 插在 JSON 流中间,你的 JSON parser 就挂了。这个守卫确保 stdout 里的每一行都是合法 JSON。
消费示例:
# 实时打字效果claude -p "Write a poem" --output-format stream-json \ --verbose --include-partial-messages | \ jq -rj 'select(.type == "stream_event" and .event.delta.type? == "text_delta") | .event.delta.text'# 只看工具调用(监控 Claude 在干什么)claude -p "fix the bug" --output-format stream-json --verbose | \ jq 'select(.type == "assistant") | .message.content[] | select(.type == "tool_use")'# 提取最终结果claude -p "analyze code" --output-format stream-json | \ jq 'select(.type == "result") | .result'
6 个更像真实工程的实战场景
前面讲的是运行方式、边界控制和输出形态。下面这些例子更接近“你为什么真的会去用 CLI”。
换个看法,这 6 个场景并不是 6 个孤立的小技巧,大致就是三类:
-
• 会话型:长期协作、worktree、resume -
• 自动化型:CI review、批量处理 -
• 结果消费型:结构化输出
场景 1:把 Claude 当成终端里的长期搭子,而不是一次性问答框
claude "先读一下这个仓库,告诉我认证模块在哪几层"claude -c "基于刚才的结论,给我一个最小改动的重构方案"
这个场景看起来简单,但很能说明 CLI 的价值。你在终端里开 Claude,不只是为了“问一个问题”,而是为了让它和当前工作目录、git 状态、shell 命令、已有会话自然连在一起。前一轮先摸清代码结构,后一轮继续沿着同一个 session 往下推进,这种体验比每次重新开一个聊天窗口更接近真实开发节奏。
如果你在命令行里本来就大量依赖 git diff、rg、测试命令、构建脚本,那 CLI 会比图形界面更自然,因为 Claude 就待在你原本工作的地方。
场景 2:用 worktree 隔离高风险修改或并行任务
claude -w feature-auth "修复登录流程里的 session 刷新问题"
--worktree 的意思,不是“换个目录启动”这么简单,而是让 Claude 直接在一个新的 git worktree 里工作。你可以把它理解成:从同一个仓库里再拉出一个独立工作区,带着自己的分支、自己的文件改动、自己的会话上下文。
这样做最实际的好处,就是把“让 Claude 放手去改”这件事和你当前正在写的代码隔开。
比如你主目录里还留着一半没写完的改动,这时候你想让 Claude 试着修一个登录 bug、跑一轮测试、顺手改几处相关代码。如果直接在当前目录里做,它很容易和你手头的未提交改动混在一起;等结果不理想,你连哪些是自己改的、哪些是 Claude 改的,都会分不清。
但如果改成 claude -w feature-auth ...,Claude 会在新的 worktree 里处理这件事。这样:
-
• 主分支工作目录不被污染 -
• 一个任务对应一个独立工作区,来回切换更清楚 -
• 你可以更放心地让 Claude 改代码、跑测试、生成提交 -
• 就算这次尝试效果一般,直接删掉这个 worktree 就能回到原状
这也是为什么 --worktree 特别适合两类任务:一类是高风险修改,比如重构、批量改文件、自动修 bug;另一类是并行任务,比如你手头正在做 A,同时想让 Claude 在另一个分支里推进 B。
如果你的习惯本来就是“一边开发一边开临时分支验证想法”,那 CLI 的 worktree 模式会非常顺手,因为它本质上就是把这种开发习惯直接接进了 Claude 的工作流里。
场景 3:CI 中 review PR
gh pr diff "$PR_NUMBER" | claude --bare -p \ --append-system-prompt "You are a security reviewer. Focus on vulnerabilities." \ --allowedTools "Read" \ --output-format json \ --max-turns 3 \ --max-budget-usd 1.00
每个 flag 的作用:
|
|
|
|
|---|---|---|
--bare |
|
|
-p |
|
|
--append-system-prompt |
追加
|
--system-prompt 会丢掉内建工具能力 |
--allowedTools "Read" |
|
|
--max-turns 3 |
|
|
--max-budget-usd 1.00 |
|
|
常见错误:用 --system-prompt 而不是 --append-system-prompt。前者替换了默认 system prompt,Claude 会丢失所有内建工具的使用说明,变成只能纯文本聊天的模型。
场景 4:多轮对话链
# 第 1 轮:分析(只给读权限)session_id=$(claude --bare -p "分析 auth.py 的性能问题" \ --output-format json --allowedTools "Read,Grep,Glob" | jq -r '.session_id')# 第 2 轮:修复(给编辑 + 测试权限)claude --bare -p "修复你发现的最严重的问题" \ --resume "$session_id" --allowedTools "Read,Edit,Bash(npm test *)"# 第 3 轮:验证(只给测试权限)claude --bare -p "运行完整测试确认修复有效" \ --resume "$session_id" --allowedTools "Bash(npm test *)"
设计思路是逐步放权:先只读看问题 → 确认方案后给编辑权限 → 最后只跑测试。用 --resume 按 session_id 精确定位会话,比 -c 更适合 CI(-c 恢复”最近的”会话,并发不安全)。
逐轮权限设计的思路:
|
|
|
|
|
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
场景 5:结构化输出
这个场景的关键,不是“让 Claude 回答得更漂亮”,而是让它吐出一份程序可以直接继续处理的数据。
很多时候,你要的不是一大段分析文字,而是一份能立刻拿去做后续处理的结果。比如:
-
• 抽取某个文件里的 API endpoint -
• 统计代码里的路由、表、配置项 -
• 扫描项目后输出固定格式的问题清单 -
• 让下一个脚本直接消费 Claude 的结果
如果这时候拿到的是自然语言,你还得再写一层字符串解析,既脆弱又容易出错。结构化输出的意义,就是把 Claude 的结果直接约束成一份固定 JSON。
claude --bare -p "列出 auth.py 中的所有 API endpoint" \ --output-format json \ --json-schema '{"type":"object","properties":{"endpoints":{"type":"array","items":{"type":"object","properties":{"method":{"type":"string"},"path":{"type":"string"}},"required":["method","path"]}}},"required":["endpoints"]}' \ --allowedTools "Read" | jq '.structured_output.endpoints'
这条命令做的事情,可以直接拆成四步:
-
1. 先让 Claude 去读 auth.py -
2. 找出里面所有 API endpoint -
3. 但最后别给我一段解释文字,而是严格按我指定的 JSON 结构返回 -
4. 然后我再用 jq直接把里面的endpoints字段取出来
它最有用的地方就在这儿:把“模型理解代码”这一步,直接接到“程序继续处理结果”这一步上。
它的原理也不复杂,拆开看就是两层。
第一层是“分析过程”不变。--json-schema不阻止 Claude 用工具。它还是会像平时一样读文件、分析代码、决定要不要调用工具。也就是说,前面的思考和执行过程没有被砍掉。
第二层是“最终输出”被约束住了。你传进去的 schema,本质上是在告诉 Claude:最后必须交一份满足这个结构的 JSON,比如这里必须有一个 endpoints 数组,数组里的每一项都必须同时带 method 和 path。如果最后产出的 JSON 不符合这个结构,系统会继续要求它重试,直到符合,或者最终失败。
所以可以把 --json-schema 理解成:不是限制 Claude 怎么思考,而是限制它最后怎么交卷。
结构化数据会放在 structured_output 字段里,不在 result 里。
result vs structured_output 的区别:
-
• result始终是纯文本(Claude 的自然语言回答) -
• structured_output是经过 schema 验证的 JSON 对象 -
• 两者可能不一样! result里可能有解释性文字
这也是为什么脚本里通常应该优先读 structured_output,而不是去拆 result 那段自然语言。
schema 验证失败时 structured_output 可能为 null,脚本里要做 null check:
result=$(claude --bare -p "..." --json-schema '...' --output-format json)structured=$(echo "$result" | jq '.structured_output')if [ "$structured" = "null" ]; then echo "Schema validation failed, falling back to text" echo "$result" | jq -r '.result'else echo "$structured"fi
场景 6:批量文件处理
这个场景适合的,不是“让 Claude 认真处理一个复杂任务”,而是让它按同一套规则,把很多文件依次扫一遍。
很多工程里的真实需求,其实都长这样:
-
• 扫一遍目录,找出可能有类型问题的文件 -
• 给一批文件生成检查报告 -
• 在迁移前先做一次全量盘点 -
• 先让 Claude 做一轮粗筛,再决定哪些文件值得人工细看
这时候,重点就不是某一个文件要分析得多深,而是这一整批文件能不能稳定、重复、成批地跑完。
for file in src/**/*.ts; do claude --bare -p "Check $file for type errors" \ --allowedTools "Read,Grep" --output-format json \ --no-session-persistence | jq -r '.result' >> report.mddone
这段脚本做的事情,其实很直接:
-
1. 先把 src/**/*.ts下面的文件一个个拿出来 -
2. 每拿到一个文件,就单独起一次 Claude 调用 -
3. 只给它读和搜索权限,不让它改文件 -
4. 把每次返回的结果追加写进 report.md
这样跑的好处,是每个文件都像一次独立任务。前一个文件分析成什么样,不会污染后一个文件;某一次跑坏了,也只影响那一个文件,不会把整批结果搅乱。
--no-session-persistence 在这里也很重要,因为这种任务往往不是“建立长期会话”,而是“一次性扫完就走”。不关掉它,跑 100 个文件就会多出 100 份你大概率不会再看的 transcript。
不过这种写法也有代价:它是串行的,而且每个文件都要走一次冷启动。文件少的时候没问题,文件一多,速度就会明显慢下来。
所以这里有个很实际的取舍:
-
• 如果你要的是结果干净、单文件边界清楚,用一文件一调用 -
• 如果你要的是更快扫完一批文件,可以把多个文件合并成一次调用
文件多的时候,比如超过 50 个,就可以考虑合并:
files=$(find src -name "*.ts" | head -50 | tr '\n' ' ')claude --bare -p "Check these files for type errors: $files" \ --allowedTools "Read,Grep" --output-format json
这种合并方式更快,但边界会变模糊一些。Claude 看到的是一组文件,不再是一次只看一个文件;输出也更像一份汇总结果,而不是按文件切开的独立记录。
所以这个场景最适合的,往往就是这几类任务:生成日报、批量扫描、静态分析前置过滤、迁移前盘点。你真正需要的不是“Claude 回答得像不像聊天”,而是它能不能像一个稳定的批处理组件一样,接进 shell loop 里把活干完。
常见问题
Q:-p 模式能用 /commit 这种 skill 吗?
不能直接用。因为 -p 模式执行完就退出,不会进入那种可以继续手动输入斜杠命令的交互界面,所以 /commit、/review-pr 这种命令在这里不能像平时那样手动触发。
要做同样的事,应该直接用自然语言描述任务,比如:claude -p "review staged changes and create a commit"。
不过这不等于相关能力彻底失效了。只要某个 skill 没有设置 disable-model-invocation: true,Claude 在执行任务时仍然可能自己决定调用它。不能用的是“你手动敲斜杠命令”这件事,不是底层能力完全不存在了。
Q:-p 下权限弹窗会挂住怎么办?
四种解法:--allowedTools 预授权、--permission-mode dontAsk 自动拒绝、bypassPermissions 跳过、--permission-prompt-tool 委托 MCP 工具处理。
Q:怎么控制成本?
--max-turns 5(最多 5 轮 agent 循环)+ --max-budget-usd 2.00(最多花 2 美元)+ --model sonnet(用更便宜的模型)。这三个参数只在 -p 模式下生效。
小节
-
1. Claude Code CLI 值得单独学,不是因为命令多,而是因为它本身就是终端、自动化和 SDK 共用的一层入口。 你在终端里手动跑、在 GitHub Actions 里自动跑、在程序里通过 SDK 调,底下复用的是同一套机制。 -
2. 这篇文章的主线其实很简单:怎么运行、怎么收边界、怎么进工程。 12 种执行模式回答的是第一段, --bare、权限和输出格式回答的是第二段,worktree、resume、CI review 这些场景回答的是第三段。 -
3. 这套 CLI 最值得先搞清的是“怎么启动”,也就是 6 种交互模式和 6 种非交互模式。 -p、TTY 判定、stdin 管道输入这些细节,决定的不是“多一种模式”,而是这些模式具体怎么运行。 -
4. --bare的价值在于确定性。 它关掉自动发现的配置、记忆、hooks、plugins、MCP 装配,让脚本、CI、SDK 调用只受你显式传入的参数影响。 -
5. 权限控制真正要分清的是三层:整体策略、工具边界、是否彻底绕过。 也就是 --permission-mode、--allowedTools/--disallowedTools,以及--dangerously-skip-permissions这三套思路。大多数场景优先用前两者,最后一种只适合高度受控环境。 -
6. stream-json和--json-schema代表的是“让 Claude 的输出能被程序接走”,而不只是给人看。 前者把执行过程变成事件流,后者把最终结果约束成固定结构,适合接进平台、前端、脚本和批处理链路。 -
7. 真正进入工程以后,CLI 的价值体现在 worktree、resume、批量处理、CI review 这些场景里。 它不是一个问答框,而是一个可以接进现有 shell、git、CI/CD 流程里的 Agent 入口。
附:源码位置速查
|
|
|
|---|---|
src/main.tsx |
|
src/setup.ts |
|
src/cli/print.ts |
runHeadless()
|
src/entrypoints/sdk/coreSchemas.ts |
|
src/types/permissions.ts |
|
src/utils/permissions/permissions.ts |
|
src/entrypoints/cli.tsx |
|
src/replLauncher.tsx |
|
src/screens/REPL.tsx |
|
src/bootstrap/state.ts |
setIsInteractive()
getIsNonInteractiveSession() |
src/utils/envUtils.ts |
isBareMode()
--bare 判断) |
夜雨聆风