---
title: "命令执行流程"
description: "同一条命令本地能跑、进了 CI 却卡住，差别常常藏在链路的某一层。这一节按顺序拆开参数解析、配置叠加、上下文组装、模型请求、工具回合与结果输出，并给出定位卡点的办法。"
updated: 2026-11-13
canonical: https://docai.cc/learn/command-execution-flow/
markdown_mirror: https://docai.cc/learn/command-execution-flow.md
---
## 敲下回车之后，链路已经开始了

同一条命令，你在终端里跑得顺利，挪到流水线就报「没有权限」，或者干脆停在某个确认框前。多数人先怀疑模型不稳定，其实更常见的原因是：这条命令在中段撞上了和本地不同的配置。

把整个过程想成一条时间线，卡点就能落到具体某一层。下面顺着这条线走一遍，同时标出每层对应的开关。

## 前三步：解析、叠加配置、拼上下文

这些动作都发生在请求模型之前。

**解析参数。** 命令行里的子命令、标志、提示词先被切开。提示词含空格时必须整体加引号，否则后面的词会被当成新参数；写成 `--model=值` 也能把名字和值绑在一起。名字打错时进程会直接退出，不会带着半截配置继续跑。

**叠加配置。** 内置默认在最底层，往上依次是用户级设置、项目共享设置、项目本地设置，最后是命令行标志，越靠后越优先。企业托管策略通常还另有一层更高的位置，实际顺序以你所在环境为准。排查配置问题时，先看命令里有没有临时传参，它压过所有文件；设置文件本身写错时，`claude doctor` 会给出校验错误。

**拼上下文。** 系统提示、项目说明文件（CLAUDE.md）、工具清单、本轮输入，以及续接会话时带上的历史消息，共同组成这次请求的载荷。历史越长，输入 token 越多，这也解释了续接会话为什么比新开会话更贵。

## 中段：流式回话与工具回合

请求发出后，回答是流式回来的。文字先到先显示，不用等整段生成完。

真正的工作发生在工具回合里。模型不会直接动你的文件，它先输出一个结构化请求：要调哪个工具、参数是什么。程序执行工具，把结果塞回对话，模型看到结果再决定下一步。读文件、改文件、跑命令，都靠这样一轮一轮推进。

每一轮算一个 turn；在非交互模式里，`--max-turns` 限制的就是轮数。设上限不是保守，而是防止模型在同一个工具上来回打转；到了上限进程会停下，你拿到已有结果，而不是无限等下去。

## 权限判断插在工具执行之前

工具请求不是拿到就执行，程序先查规则：

- 命中允许列表，直接跑；
- 命中拒绝列表，回一条拒绝结果，模型只能换方案；
- 两边都没有，交互模式弹出确认框，由你选。

这解释了 `-p` 模式里最常见的一种失败：脚本运行没人点确认，未获授权的写操作被拒，模型于是回你一段「我改不了，但建议这样改」。解法不是关掉所有防护，而是把这次任务真正需要的工具写进允许列表，范围尽量窄。

`--dangerously-skip-permissions` 会跳过绝大部分确认，只适合隔离环境里的明确任务。生产脚本里用它，等于把风险交给模型的判断力。

## 末段：结果形态与退出方式

最后一步由输出格式决定：

| 格式 | 形态 | 适合 |
| --- | --- | --- |
| text | 人读的文本，逐字到达 | 终端里直接看 |
| json | 结束后给一个完整对象 | 脚本取结果 |
| stream-json | 一行一个事件，持续输出 | 前端或管道实时消费 |

非交互模式输出完即退出，退出码可供脚本判断；交互模式把光标还给你，会话留在内存里等下一句。

## 用链路定位一次失败

假设下面这条命令在流水线里没有改动任何文件：

```bash
claude -p "在 src/utils.ts 里补一个 debounce 函数" --max-turns 5
```

按阶段排查：解析正常；配置读到了，但允许列表只有读类工具；上下文里项目说明也在；模型确实发出了编辑请求；断点在权限判断，被自动拒绝。修法是把这个权限明确交出去：

```bash
claude -p "在 src/utils.ts 里补一个 debounce 函数" \
  --max-turns 5 \
  --allowedTools "Read" "Edit" "Glob"
```

同样的提示词再跑一次，就会走到工具执行并落地。看懂链路的价值在这里：不靠猜，直接找到断点。

## Hook 挂在链路的哪一段

整条链路里有几个固定插槽，配置好的外部命令会在对应时刻被调用：

| Hook | 触发位置 | 适合做什么 |
|---|---|---|
| SessionStart | 进程启动、会话建立时 | 打印环境信息、检查依赖是否装好 |
| UserPromptSubmit | 输入已提交、上下文尚未组装时 | 补充项目状态、拦截明显有问题的请求 |
| PreToolUse | 工具即将执行前 | 审计危险命令、按路径拦截写入 |
| PostToolUse | 工具执行完成之后 | 格式化、跑快速检查、记录改动 |
| Stop | 模型给出最终回答、回合结束前 | 提醒未完成事项、生成摘要 |
| SessionEnd | 退出或会话关闭时 | 清理临时文件、保存统计 |

以「每次改完文件自动跑测试」为例，配置形态大致如下：

```json
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{ "type": "command", "command": "npm test --silent" }]
      }
    ]
  }
}
```

`matcher` 决定匹配哪些工具，命令在匹配事件之后执行。两点边界要记住：一是 hook 里不要放长跑任务，它会把主流程拖住，整套测试很慢就改成只跑相关文件；二是 hook 失败是否阻断后续步骤，取决于当前版本和配置，升级后先验证一次实际行为。更完整的机制见[钩子](/concepts/hooks/)。

## 四个高频故障：症状、原因、处理

**症状一：脚本模式下它只给建议，文件一点没改。** 先从输出里确认有没有权限拒绝的提示，再看允许列表的粒度：只放行到 `Bash(git diff:*)` 这种命令前缀，比整类放开安全；需要写文件时，逐项列出 `Edit`、`Write` 和具体目录，不要用通配把风险一起兜进来。改完用同一条提示词复跑，确认改动真的落盘。

**症状二：命令迟迟不结束。** 先分清是模型在反复调用，还是某个工具本身在长时间执行。前者看轮次，给 `--max-turns` 设一个 5 到 15 之间的值；后者把长命令放后台，让模型继续做别的事，做法见[后台 bash 命令](/learn/background-bash-commands/)。两种情况都不要靠加大超时硬扛。

**症状三：拿到的输出无法按 JSON 解析。** 常见原因是把流式格式当成了单个对象。需要一次性拿到完整结果时，选 `json`；需要边生成边处理时，选 `stream-json`，并按行读取、逐行解析。两者都要求关掉那些会混入提示文字的装饰输出。

**症状四：改了设置却不生效。** 按生效顺序倒查：命令行临时传参压过一切，项目本地设置压过项目共享设置，用户级设置只在前面都没有时才轮得到。另一个常被忽略的是路径作用域——在 A 目录写的项目设置，到了 B 目录不会加载；确认文件位置无误后，还要重启进程让它重新读取。

还有一种表现是退出码正常、文件却没变：这多半说明提示词里只有讨论，没有可执行的验收条件，模型把任务当成咨询处理了。把「改哪个文件、跑哪条命令、看到什么算成功」写清楚，空转就会少很多。

另外两种情况也常见：续接会话后第一轮就超出上下文，是因为旧消息被整段带上，解决方式是在上次结束前压缩一次；按了中断键后发现回答缺了一截，是因为未完成的流被丢弃，重要任务尽量一次跑完，或把中间结果写进文件。

一个可以直接抄进流水线的例子：

```bash
claude -p "审查当前分支相对 main 的改动，按严重程度列出问题，指出文件与行号" \
  --allowedTools "Read" "Grep" "Bash(git diff:*)" \
  --output-format json \
  --max-turns 8 > review.json

jq -r '.result // .' review.json | head -n 40
```

这段命令把读文件、搜代码和查看差异放开，其余写操作仍需确认；输出落进文件，后续步骤再解析。字段名若与上面不同，先打印一次原始对象再调整取值。

记住这条线，后面每个开关在什么位置生效都能对上号。想先补工具调用的概念，可以看[什么是工具调用](/concepts/tool-use/)；流式响应本身见[流式输出](/concepts/streaming/)。下一节解决「忘了参数怎么办」——[命令帮助系统](/learn/command-help-system/)。
