Claude Code CLI 与 Claude Agent SDK
机制、边界,以及要不要自建业务 agent

两者不是两个产品,是同一个进程的两种调法——SDK 包里捆着一份编译好的 Claude Code 原生二进制,query() 把它 spawn 起来走 stdio 通信。 因此结论是:不要自建 harness,给 SDK 装手脚;但要认清 harness 边界之外必须由你承担的那部分。
核实基线:本机 @anthropic-ai/claude-agent-sdk 0.3.150 ↔ Claude Code 2.1.150 · 官方文档 code.claude.com/docs/en/agent-sdk

01同源关系:SDK 就是在跑 CLI

这一节是后面所有结论的地基。理解了进程模型,隔离、权限回调、session 存储的行为都会变得可推导。

你的应用进程 Node / Python · 你部署、你隔离 query(prompt, options) canUseTool 回调 进程内 MCP 工具 / hooks SessionStore 适配器 spawn stdio 控制协议(双向) claude 原生二进制(子进程) 213 MB · --version → 2.1.150 (Claude Code) agent loop · 上下文压缩 内置工具 Read/Write/Bash/Grep… 权限评估六步序 持有 shell · cwd · 本地 jsonl CLI claude -p --output-format stream-json = 同一个二进制 容器 / 宿主文件系统 · 环境变量 · ~/.claude agent 的 bash 工具能看到这里的一切 —— 隔离边界只能画在这一层之外 容器编排 / 凭证注入 / 出口管控 = 接入方负责
官方原文:“The Agent SDK spawns and supervises a claude CLI subprocess that owns a shell, a working directory, and session files on disk.” 本机硬证据:package.json"claudeCodeVersion": "2.1.150";捆绑二进制执行 --version 输出 2.1.150 (Claude Code);SDK 0.3.x ↔ CC 2.1.x 一一对应。
一条广为流传但已过时的说法

“SDK 默认不加载 CLAUDE.md”——文档原文:“This default was briefly changed in v0.1.0 and then reverted, so no migration action is needed. Current behavior: Omitting settingSources loads user, project, and local filesystem settings, matching the CLI.”

真实存在的默认差异只有一个:system prompt。SDK 默认是 minimal prompt,要显式传 { type: 'preset', preset: 'claude_code' } 才能拿回 CLI 行为。

02差异清单:只剩一维——宿主进程能不能被回调

线协议层面零差距(SDK 内部用的就是 claude -p --output-format stream-json --input-format stream-json)。剩余差异全在“工具调用能不能回到你的进程里”。

能力SDKCLI 的替代
进程内 MCP 工具
差距最大
createSdkMcpServer() + tool(),server 跑在你的应用进程里必须另起一个 MCP server 进程或 HTTP 端点
进程内 hook 回调宿主语言闭包,与文件系统 hooks 并行执行hooks 只能是 shell 命令 / HTTP / mcp_tool,拿不到宿主闭包
canUseTool走 stdio 控制协议回调进你的进程需提供一个 MCP 权限工具(--permission-prompt-tool <name>
SessionStoretranscript 镜像到 S3 / Redis / Postgres alpha搬 jsonl 文件,且 cwd 必须一致
spawnClaudeCodeProcess自定义 spawn,把 CLI 跑到远端 VM / 容器
会话管理 APIlistSessions / getSessionMessages / renameSession / tagSession交互式 resume picker
类型化 message / Options自己解 NDJSON
反向 —— CLI 独有:Agent teams(文档明说 “Agent teams are a CLI feature”)、交互 TUI、--worktree--bare--from-pr、Remote Control、后台会话视图
判断标准

只需要「发 prompt、收事件流、工具走独立 MCP server、权限用固定 allow/deny 规则」→ headless CLI 与 SDK 实质无差距,SDK 只是省了 NDJSON 解析和类型定义。

一旦需要「把宿主进程的函数暴露成工具」或「每次工具调用都要过一遍业务鉴权 / 审计 / 配额」→ 变成硬差距,必须用 SDK。

另一个常见误解

「CLI 面向个人单机」偏保守了。官方明确把 CLI 当作跨语言编程接入路径:“The SDK is available as a library for Python and TypeScript only. To drive the same agent loop from another language, run the CLI as a subprocess with the -p flag.” 整个 headless 文档页讲的就是 CI/CD、GitHub Actions、结构化输出。

03并发与隔离:危险的不是并发,是共享

一个容器跑多个 SDK 子进程是官方推荐的部署形态之一,不是反模式。“Long-running sessions: Run persistent container instances, often hosting multiple SDK processes per container.”

四种 session 生命周期模式(按空闲模式选,不是按安全性选)

模式容器 : session适用
Ephemeral1 : 1,跑完销毁一次性任务(bug 修复、发票抽取、文档翻译)
Long-running1 : N,常驻高频消息流(邮件 triage、Slack bot、站点构建器)
Hybrid临时容器 + 启动时从 SessionStore 水合跨多次交互但中间长时间空闲
Multi-agent1 : N,紧密协作多 agent 仿真
容量算法(原文)

agents per host = (host RAM − overhead) / (per-session RAM ceiling)

没有硬性并发上限,唯一约束是内存。起步资源每 agent 1 GiB RAM / 5 GiB disk / 1 CPU,文档强调这是 “a floor, not the ceiling”——要用代表性 session 跑到目标长度实测 peak RSS。水平扩展靠对 sessionId 一致性哈希,把 session 钉到固定容器。

真正会踩的四类干扰

来源默认行为处置
共享 cwd
第一个炸
“By default they all inherit your application's working directory.” 多个 agent 在同一目录读写同一批文件,Edit/Write 直接互相覆盖,没有任何锁保护 每次 query() 显式传 cwd(官方在两处重复强调)
continue: true 语义是「找当前目录下最近的一个 session」。文档自述 “Works well when your app runs one conversation at a time.” session 文件名是 UUID 不会撞车,但 continue 语义会串 并发场景显式捕获 session_idresume
宿主配置 有四项 settingSources: [] 关不掉(见下表) 逐项用对应开关关闭
环境变量 bash 工具是子进程的子进程 —— 容器里的 ANTHROPIC_API_KEY、DB 密码、云凭证,agent 一条 env 就读出来 代理注入(ANTHROPIC_BASE_URLHTTP_PROXY + TLS 终止代理),让凭证根本不进 agent 环境

settingSources: [] 关不掉的四项

输入唯一关闭方式
托管策略(MDM plist / 注册表 / managed settings)只能从宿主删除;server-managed 部分由组织管理员控制,SDK 侧无解
~/.claude.json(“Always read”)CLAUDE_CONFIG_DIR 重定向
Auto memory(会话启动时注入 system prompt)autoMemoryEnabled: falseCLAUDE_CODE_DISABLE_AUTO_MEMORY=1
claude.ai MCP connectors(传 mcpServers: {} 也压不住)strictMcpConfig: true / disableClaudeAiConnectors: true
“Do not rely on default query() options for multi-tenant isolation. Because the inputs above are read regardless of settingSources, an SDK process can pick up host-level configuration and per-directory memory.” code.claude.com/docs/en/agent-sdk/claude-code-features

关键区分

同租户多并发

官方支持的形态
  • 只需工程隔离,无需安全边界
  • 每 session 独立 cwd
  • resume,不用 continue
  • 按 RAM 算并发数
  • 长会话定期回收子进程

跨租户共享容器

文档明说不能靠默认值
  • settingSources: []
  • CLAUDE_CODE_DISABLE_AUTO_MEMORY=1
  • 每租户 CLAUDE_CONFIG_DIR
  • 每租户 cwd,每次 query 显式传
  • 代理层每租户出口规则
TS 特有陷阱:env 是替换语义

Options.env 注释原文:“When set, this value REPLACES the subprocess environment entirely — it is not merged with process.env.” 传了 env 就必须自己展开 process.env,否则 PATH / HOME / ANTHROPIC_API_KEY 全丢。Python 侧相反,是叠加。

// 跨租户参考写法(注意展开 process.env)
for await (const message of query({
  prompt,
  options: {
    cwd: tenantDir,
    settingSources: [],
    env: {
      ...process.env,
      CLAUDE_CONFIG_DIR: configDir,
      CLAUDE_CODE_DISABLE_AUTO_MEMORY: "1",
    },
  },
})) { /* ... */ }
Known limitations 里两条值得记

没有顶层 session 超时——“A session does not time out on its own.” 只能用 Options.maxTurns 限工具往返轮数。

长 session 内存会涨——官方建议 “Cap session length or recycle subprocesses periodically.”

04canUseTool:工具执行前回调到你的进程

SDK 相对 CLI 唯一的硬增量。理解它的实现机制,也就理解了「进程内」这三个字的准确含义。

调用链:stdio 双向控制协议

你的 canUseTool SDK 父进程 claude 子进程 工具执行 ① control_request {request_id, subtype:"can_use_tool"} ② canUseTool(toolName, input, opts) ③ PermissionResult allow + updatedInput | deny + message ④ control_response(按 request_id 匹配) ⑤ 执行 / 不执行
SDK 拼命令行时塞的是 --permission-prompt-tool stdio —— 值是字面量 "stdio",不是某个真实 MCP server 名。这是 SDK 与 CLI 之间的内部协议约定,未在 CLI 文档中列为公开取值,所以 CLI 单独做不到(它的公开取值是一个真 MCP 工具名)。机制来自 sdk.mjs 打包代码,属实现细节,随版本可能变化。

类型定义(sdk.d.ts L188-233 / L1953-1965,一字不差)

type CanUseTool = (
  toolName: string,
  input: Record<string, unknown>,
  options: {
    signal: AbortSignal;                // 用户关页面/超时就 abort
    suggestions?: PermissionUpdate[];   // 「总是允许」用,原样回传即可
    toolUseID: string;
    title?: string;        // bridge 已渲染:「Claude wants to read foo.txt」
    displayName?: string;  // 「Read file」
    description?: string;
    blockedPath?: string;
    decisionReason?: string;
    agentID?: string;      // 来自 subagent 时标识来源
  }
) => Promise<PermissionResult>;

type PermissionResult =
  | { behavior: 'allow'; updatedInput?: Record<string, unknown>;
      updatedPermissions?: PermissionUpdate[]; /* ... */ }
  | { behavior: 'deny'; message: string; interrupt?: boolean; /* ... */ };
三个容易被忽略的能力

allow 能改写入参updatedInput)—— Claude 实际执行的是你改过的参数。可用于强制加 --dry-run、重写路径、脱敏。

deny 的 message 是必填(不是可选)—— 这句话作为 tool result 回给 Claude,它会读到并调整策略。

deny 能中断整轮interrupt: true)—— 而不只是拒这一个工具。

权限评估六步序:canUseTool 在最后一步

1
Hooks(PreToolUse100% 覆盖
每次工具调用都过,且 hook 的 deny 在 bypassPermissions 下依然生效
2
Deny 规则
命中即拦,不再往下
3
Ask 规则
4
Permission mode
bypassPermissions 全放行 · acceptEdits 放行文件操作 · dontAsk 整个跳过第 6 步直接 deny
5
Allow 规则
allowedTools: ["Bash"] 这种裸名直接放行 —— 该工具永不进回调。带 scope 的 ["Bash(ls *)"] 只放行匹配项,其余照样进回调
6
canUseTool 有漏网
能拿到渲染好的展示文案和 suggestions,能改写入参 —— 但上面任一步短路后就到不了这里
SDK 自带防呆

v2.1.198 起,如果你传了一个评估流永远到不了的 canUseTool,TS SDK 会在构造 query 时发一次 Node 进程警告,code 为 CLAUDE_SDK_CAN_USE_TOOL_SHADOWED。触发条件正是上面两种:bypassPermissions,或每一条都是裸名的 allowedTools。可以 process.on('warning', …) 匹配 code 捕获。

选型:canUseTool 还是 PreToolUse hook

维度canUseToolPreToolUse hook
评估位置6 步(最后)1 步(最先)
覆盖率有漏网 被 allow 规则 / acceptEdits / bypassPermissions 提前放行的不经过每次都过 deny 在 bypassPermissions 下依然生效
改写入参能(updatedInput不能
下发权限更新能(updatedPermissions,实现「总是允许」)不能
中断整轮能(interrupt: true未见等价字段 未核实
能否来自文件系统否,只能编程传入能,.claude/settings.json 可与 CLI 共享
“For checks that must run on every tool call, use a PreToolUse hook: hooks run before every other step, and a hook deny applies even in bypassPermissions mode.” code.claude.com/docs/en/agent-sdk/permissions

05SessionStore:镜像,不是替代 alpha

默认 transcript 写在本机 ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl。多实例部署时,用户第二次请求被打到另一台机器就 resume 不了 —— 这就是它要解决的问题。

写路径(dual-write) claude 子进程 产生 transcript 条目 本地磁盘 jsonl 权威 · 先写这里 SDK 父进程 转发批次 ~100ms append() → 外部存储 镜像 · best-effort ②写成功后 失败:拒绝重试 3 次 · 超时(60s)不重试 → 丢弃该批 + 发 {type:"system", subtype:"mirror_error"} · query 继续跑 跨主机 resume 路径 外部存储 S3 / Redis / Postgres SDK 父进程 load() spawn 之前 · 调一次 临时 JSONL 文件 物化到本地 子进程原有 resume 对 CLI 完全无感知 cwd 约束仍在:projectKey 默认由 cwd 推导,新主机 cwd 不一致就会查错的 scope
“The store is a mirror, not a replacement. The Claude Code subprocess always writes to local disk first; the SDK then forwards each batch to append().” append 的注释同样写死顺序:“Called AFTER the subprocess's local write succeeds.”

接口定义

type SessionKey = {
  projectKey: string;   // 默认由 cwd 推导
  sessionId: string;
  subpath?: string;     // 未设 = 主 transcript;设了 = subagent
};

type SessionStore = {
  // 必需
  append(key: SessionKey, entries: SessionStoreEntry[]): Promise<void>;
  load(key: SessionKey): Promise<SessionStoreEntry[] | null>;
  // 可选
  listSessions?(projectKey: string): Promise<Array<{sessionId: string; mtime: number}>>;
  listSessionSummaries?(projectKey: string): Promise<SessionSummaryEntry[]>;
  delete?(key: SessionKey): Promise<void>;
  listSubkeys?(key: {projectKey: string; sessionId: string}): Promise<string[]>;
};
可选方法缺失后果
listSessionscontinue: true 抛错;listSessions() 抛错(除非实现了 listSessionSummaries
listSessionSummaries退化为 listSessions() + 逐个 load()
delete删除变 no-op(适合 S3 这类 append-only / WORM 后端)
listSubkeys只恢复主 transcript,subagent 记录丢失

镜像什么、不镜像什么

内容镜像说明
主 transcriptsubpath 未设时
subagent transcriptsubpath: "subagents/agent-<id>",需实现 listSubkeys
CLAUDE.md 记忆文件“Mount a shared volume or sync those separately.” 跨机 resume 拿回的是对话上下文,不是工作现场
工作目录产物(agent 改的代码等)
file-checkpointing 备份 blob组合使用会直接抛错
四个硬约束

1. 不能与 persistSession: false 同用(会抛错)—— mirror 依赖本地写成功这个钩子。想让本地副本临时化,官方做法是 CLAUDE_CONFIG_DIR 指向临时目录,不是关本地持久化。

2. mirror 写是 best-effort。在意完整性就必须监听 mirror_error

3. 适配器必须按 entry.uuid 去重 —— 重试会重复投递。没有 uuid 的条目(title、tag、mode marker)直接追加不去重。

4. 保留策略完全归你 —— “The SDK never deletes from your store on its own.”

另外三个行为差异

getSessionMessages 返回的是压缩后的链 —— auto-compaction 之后 store 里 503 条原始 entry 可能只返回 18 条消息。要原始历史必须直接 store.load(key)

forkSession 不是字节拷贝 —— SDK 会重写每个 sessionId 字段、重映射 message UUID。所以不能在适配器层用 S3 CopyObject 这类捷径。

参考适配器(S3 / Redis / Postgres)在 TS SDK 仓库 examples/session-stores/未发布到 npm,需把 src/ 拷进项目。内置 InMemorySessionStore 仅供开发测试。另有 conformance 测试套件验证自研适配器。

更实在的替代方案

官方在 sessions 页给了第二条路:干脆不依赖 resume —— 把结果作为应用状态存起来,喂进新 session。既然工作现场本来就不镜像,这条路在很多场景下比接 SessionStore 更可靠。

06选型:还有必要自建业务 agent 吗

把「agent」这个词拆成两层,问题就自己回答了。

通用 harness 层

别在这上面投人
  • agent loop
  • 上下文压缩 / compaction
  • 权限评估六步序
  • session 管理
  • 工具调度 / subagent 编排

Anthropic 在每周迭代 —— 本机这版 0.3.150↔CC 2.1.150,npm 上已经 0.3.220。

自建第一版要一个月,写到能用要半年,然后每个 CC 版本都得重新对齐。唯一能换来的是「完全可控」,代价是放弃别人全职团队的迭代速度。

业务层

只有你能做,也只该做这层
  • 上下文供给(CLAUDE.md / skills / 检索)
  • 验收闸门(结构化输出 + gate)
  • 领域工具(内部系统封装)
  • 流程编排(状态机)
  • 评测集

通用 harness 永远给不了你这些。护城河在「agent 知道什么」和「怎么确认它做对了」。

手脚具体装什么,按投入产出排序

1

上下文供给 最便宜 · 最有效 · 最被低估

CLAUDE.md / skills / 检索。同一个 harness,喂进去的领域知识不同,产出质量差一个量级。

这里的工作量是写文档和建索引,不是写代码。它决定 agent 知道什么 —— 是「手脚」里最像手脚的部分。

2

验收闸门 决定能不能无人值守

结构化输出 + gate。没有这层,agent 只能当「辅助」;有了这层才能进流水线。

判断标准很直接:agent 说「完成了」,你有没有一个不依赖它自述的方式确认?没有的话,再强的 harness 也只是个更贵的建议生成器。

3

领域工具

默认选独立 MCP server 进程 —— CLI 和 SDK 都能用,复用面更广。

只有当「每次调用都要过一遍你的业务规则」(鉴权 / 配额 / 审计 / 脱敏)时才用进程内 MCP(createSdkMcpServer)—— 这也正是 SDK 相对 CLI 唯一的硬增量。

4

流程编排 用代码,不要塞给模型

阶段流转、人工确认点、失败重试 —— 应该是状态机,而不是 prompt 里的一段描述。

模型负责每个阶段内部的开放式判断;阶段之间的跳转由代码决定。

5

评测集 最容易跳过 · 最决定长期速度

没有它,你改 prompt 是在赌;有了它,每次改动能测。

什么时候确实不该用这个 harness

情况为什么 / 改用什么
任务不需要文件系统和 shell 纯对话、抽取、分类、路由。一个 query() 起 1 GiB RAM 的完整 Claude Code 进程是杀鸡用牛刀 → 直接用 Messages API,或 SDK 的 tool runner
流程必须严格确定 审批流、状态机、有合规要求的操作链 → 把 LLM 当成流程里的一个函数调,不要让它当调度者
模型不是 Claude harness 与模型是绑定的 —— 非 Claude 模型不支持远程 MCP,这个约束会一路传导到架构上

07harness 边界之外:必须你自己担的

这不是可选的加固,是 harness 架构决定的必然分工。文档白纸黑字:“put authentication at a gateway in front of the agent container. The agent should not be the component that validates user tokens.”

职责harness 给到哪你要补什么
沙箱生命周期 只有 bash 沙箱(Linux bubblewrap / macOS sandbox-exec)。且这是 Claude Code 自带特性,不是 SDK 特性 容器编排、复用、污染回收。@anthropic-ai/sandbox-runtime 是独立 npm 包
凭证不进 agent 环境 无。子进程从自己的环境读 key 代理注入 —— ANTHROPIC_BASE_URLHTTP_PROXY + TLS 终止代理
并发的 cwd 隔离 默认全部继承应用工作目录 每次 query() 显式传 cwd;不传就互相覆盖文件
业务态持久化 SessionStore 只镜像 transcript 把结果落成你自己的应用状态,不依赖 resume 恢复工作现场
超时与资源回收 无顶层 session 超时;maxTurns 只限轮数 自己定期回收子进程;后台 subagent 可用 CLAUDE_ASYNC_AGENT_STALL_TIMEOUT_MS停顿看门狗,不是总时长上限
租户隔离 五件套参数,且有四项输入绕过 settingSources 隔离边界只能画在容器之外 —— 每租户一个文件系统 / 一个容器
bash 沙箱自身的两个弱点(文档承认)

① 与宿主共享内核 —— 内核漏洞可逃逸。② 代理不做 TLS 解密 —— 可被 domain fronting 绕过。

进程级 / 租户级隔离 SDK 完全不提供,容器 / gVisor / Firecracker / 云网络管控全部要自己搭。

如果目标是「不想自己管沙箱和租户隔离」

答案不是 Agent SDK,是 Managed Agents —— overview 页原文:“Hosted REST API, a separate product from the Agent SDK. Anthropic runs the agent and the sandbox.”

代价:beta 阶段;因有状态设计不支持 ZDR 和 HIPAA BAA。另有一条商业约束:未经批准,第三方不得为自己基于 Agent SDK 的产品提供 claude.ai 登录或额度,必须用 API key。