乐于分享
好东西不私藏

从源码看 Claude Code CLI 的执行模式和用法

从源码看 Claude Code CLI 的执行模式和用法

这是 Claude Code 源码泄露后,结合源码整理的一手技术分析。不管你是 Claude Code 的深度用户,还是在做 Agent 相关的学习或工作,或许都能从中找到一些启发。本文是系列文章中的一篇,完整系列目录见文末「细说 Claude Code」。

本文内容脉络预览

这篇文章主要围着四个问题展开:

  1. 1. Claude Code CLI 为什么值得单独学
  2. 2. 它一共怎么运行,也就是 12 种执行模式怎么分
  3. 3. 脚本和自动化里,怎么控制边界,包括 --bare、权限和输出格式
  4. 4. 真进工程以后,哪些场景最常用

顺着这条线往下看,后面就不容易乱。

CLI 到底是什么

CLI 是 Command Line Interface,中文一般叫命令行界面。和更常见的 GUI 对着看,这个概念就很好理解了。

GUI 是图形界面,你看到的是按钮、菜单、弹窗和列表,靠鼠标去点;CLI 则是另一种方式,你面对的是终端和命令,输入一条命令,带上一组参数,程序执行后再把结果返回给你。

一句话说,GUI 是“把功能做成界面给人点”,CLI 是“把功能做成命令给人输”。像 git statusnpm run builddocker psgh 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 ...
  • • 把输出接给 jqxargs、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
打开交互对话模式,你说一句它做一步
带初始 prompt
claude "explain this"
进入交互对话模式,自动处理第一句,然后等你继续
恢复最近会话
claude -c
接着上次的对话继续(当前目录)
恢复指定会话
claude -r auth-refactor
按名字或 ID 恢复任意历史会话
worktree 隔离
claude -w feature-auth
在独立 git worktree 里工作,不影响主分支
Agent 模式
claude --agent code-reviewer
整个会话使用指定 Agent 的角色和工具限制

非交互模式(被脚本或 CI 调用)

模式
命令
效果
单次查询
claude -p "query"
执行完整 agent loop 后退出
管道输入
cat file | claude -p "query"
stdin 内容 + prompt 合并发给模型
JSON 输出
claude -p --output-format json
完成后输出包含 result/session_id/usage 的 JSON
流式 JSON
claude -p --output-format stream-json
逐行实时输出 JSON 事件
结构化输出
claude -p --json-schema '{...}'
输出符合指定 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. 1. 检测 !process.stdin.isTTY(stdin 不是终端 = 有管道输入)
  2. 2. 读取 stdin 全部内容(不是流式的,大文件注意内存)
  3. 3. 把 stdin 内容和 -p 后面的 prompt 合并成一个完整的用户消息
  4. 4. 发给模型

源码中还有个 peekForStdinData() 函数,会在 3 秒后发出告警——如果 stdin 在那个时间段内没有数据到来,说明可能是误操作(比如你打了 claude -p 但忘了管道输入,程序就挂在那等 stdin)。


脚本和自动化里,怎么把边界收清楚

前面讲的是“怎么运行”,下面这部分更关心“怎么把行为收住”。到了脚本、CI、SDK 这些场景,真正重要的无非三件事:运行环境尽量确定、权限边界尽量明确、输出结果尽量能被程序直接消费。

--bare 模式:确定性比便利性重要

--bare 在源码里只做了一行事:

process.env.CLAUDE_CODE_SIMPLE = '1'

但这一行关闭了整个自动发现体系

功能
正常模式
bare 模式
CLAUDE.md
✅ 自动找到并加载
❌ 跳过
Auto Memory
✅ 读写记忆文件
❌ 跳过
Hooks
✅ 从 settings 加载
❌ 跳过
Skills
✅ 发现和注册
⚠ 不做常规自动发现,但显式 /skill-name 仍可解析
Plugins
✅ 加载
❌ 跳过
MCP Servers
✅ 从 .mcp.json 加载
❌ 跳过
OAuth
✅ 自动认证
❌ 跳过,用 API key

为什么 CI 必须用 –bare

因为需要确定性

你写了一个 CI 脚本在 GitHub Actions 里跑。你的机器上有一个 PostToolUse hook 会自动跑 lint,同事小明的机器上没有。项目里有一个 .mcp.json 配置了一个 MCP server,但 CI runner 上这个 server 装没装不确定。

不加 --bare,同一个脚本在不同环境上可能:

  • • 因为 hook 的有无产生不同结果
  • • 因为 MCP server 连不上而报错
  • • 因为某个人的 ~/.claude/CLAUDE.md 里有特殊指令而行为不一致

加了 --bare,一切都确定了——只有你显式传入的参数才生效。

Anthropic 官方也说了:

--bare is the recommended mode for scripted and SDK calls, and will become the default for -p in 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 手动加回需要的部分:

需要什么
用什么 flag
CLAUDE.md 的内容
--append-system-prompt-file ./CLAUDE.md
替换整个 system prompt
--system-prompt-file ./prompt.txt
配置
--settings ./settings.json
MCP Servers
--mcp-config ./mcp.json
自定义 Agents
--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
会话开始
拿到 session_id
stream_event

 + text_delta
模型吐每个 token
实时打字效果
assistant

 消息
模型一轮回复结束
拿到完整回复(含 tool_use)
user

 + tool_result
工具执行完
看到工具返回结果
system/api_retry
API 限流或出错
显示重试进度
system/status
权限模式变化
得知当前模式
system/hook_*
Hook 执行中
调试用(需 --verbose
result
全部完成
最终结果 + usage + cost

注意: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 diffrg、测试命令、构建脚本,那 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 的作用:

flag
为什么
不用会怎样
--bare
CI 一致性
可能加载了不同人的本地配置
-p
非交互
进入交互对话模式,CI 挂住
--append-system-prompt 追加

角色定义
用 --system-prompt 会丢掉内建工具能力
--allowedTools "Read"
只读
Claude 可能改代码
--max-turns 3
防循环
Agent 可能读几十个文件
--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 恢复”最近的”会话,并发不安全)。

逐轮权限设计的思路:

轮次
目标
权限
为什么这样设计
第 1 轮
分析
Read, Grep, Glob
只读。Claude 会读代码、搜索模式、定位问题,但不改任何东西
第 2 轮
修复
Read, Edit, Bash(npm test *)
确认分析合理后才给 Edit。Bash 限定只能跑 npm test
第 3 轮
验证
Bash(npm test *)
收窄到只能跑测试。不需要再读或改文件了

场景 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. 1. 先让 Claude 去读 auth.py
  2. 2. 找出里面所有 API endpoint
  3. 3. 但最后别给我一段解释文字,而是严格按我指定的 JSON 结构返回
  4. 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. 1. 先把 src/**/*.ts 下面的文件一个个拿出来
  2. 2. 每拿到一个文件,就单独起一次 Claude 调用
  3. 3. 只给它读和搜索权限,不让它改文件
  4. 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. 1. Claude Code CLI 值得单独学,不是因为命令多,而是因为它本身就是终端、自动化和 SDK 共用的一层入口。 你在终端里手动跑、在 GitHub Actions 里自动跑、在程序里通过 SDK 调,底下复用的是同一套机制。
  2. 2. 这篇文章的主线其实很简单:怎么运行、怎么收边界、怎么进工程。 12 种执行模式回答的是第一段,--bare、权限和输出格式回答的是第二段,worktree、resume、CI review 这些场景回答的是第三段。
  3. 3. 这套 CLI 最值得先搞清的是“怎么启动”,也就是 6 种交互模式和 6 种非交互模式。-p、TTY 判定、stdin 管道输入这些细节,决定的不是“多一种模式”,而是这些模式具体怎么运行。
  4. 4. --bare 的价值在于确定性。 它关掉自动发现的配置、记忆、hooks、plugins、MCP 装配,让脚本、CI、SDK 调用只受你显式传入的参数影响。
  5. 5. 权限控制真正要分清的是三层:整体策略、工具边界、是否彻底绕过。 也就是 --permission-mode--allowedTools / --disallowedTools,以及 --dangerously-skip-permissions 这三套思路。大多数场景优先用前两者,最后一种只适合高度受控环境。
  6. 6. stream-json 和 --json-schema 代表的是“让 Claude 的输出能被程序接走”,而不只是给人看。 前者把执行过程变成事件流,后者把最终结果约束成固定结构,适合接进平台、前端、脚本和批处理链路。
  7. 7. 真正进入工程以后,CLI 的价值体现在 worktree、resume、批量处理、CI review 这些场景里。 它不是一个问答框,而是一个可以接进现有 shell、git、CI/CD 流程里的 Agent 入口。

附:源码位置速查

文件
职责
src/main.tsx
主入口,CLI 参数解析,交互/非交互模式分叉点,很多核心 flag 也在这里定义
src/setup.ts
启动阶段的环境与安全检查,尤其是危险权限相关限制
src/cli/print.ts runHeadless()

 非交互模式核心执行逻辑、JSON / stream-json 输出
src/entrypoints/sdk/coreSchemas.ts
SDK 侧 schema 定义,权限模式等枚举说明在这里能看到
src/types/permissions.ts
权限模式和权限相关类型定义
src/utils/permissions/permissions.ts
具体权限检查与规则应用逻辑
src/entrypoints/cli.tsx
CLI 入口引导
src/replLauncher.tsx
交互对话模式启动
src/screens/REPL.tsx
交互式 UI 主组件
src/bootstrap/state.ts setIsInteractive()

getIsNonInteractiveSession()
src/utils/envUtils.ts isBareMode()

--bare 判断)

#ClaudeCode #CLI #源码分析 #执行模式 #命令行工具 #GitHubActions #工程实践