query() 把它 spawn 起来走 stdio 通信。
因此结论是:不要自建 harness,给 SDK 装手脚;但要认清 harness 边界之外必须由你承担的那部分。
这一节是后面所有结论的地基。理解了进程模型,隔离、权限回调、session 存储的行为都会变得可推导。
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 行为。
线协议层面零差距(SDK 内部用的就是 claude -p --output-format stream-json --input-format stream-json)。剩余差异全在“工具调用能不能回到你的进程里”。
| 能力 | SDK | CLI 的替代 |
|---|---|---|
| 进程内 MCP 工具 差距最大 | createSdkMcpServer() + tool(),server 跑在你的应用进程里 | 必须另起一个 MCP server 进程或 HTTP 端点 |
| 进程内 hook 回调 | 宿主语言闭包,与文件系统 hooks 并行执行 | hooks 只能是 shell 命令 / HTTP / mcp_tool,拿不到宿主闭包 |
canUseTool | 走 stdio 控制协议回调进你的进程 | 需提供一个 MCP 权限工具(--permission-prompt-tool <name>) |
SessionStore | transcript 镜像到 S3 / Redis / Postgres alpha | 搬 jsonl 文件,且 cwd 必须一致 |
spawnClaudeCodeProcess | 自定义 spawn,把 CLI 跑到远端 VM / 容器 | — |
| 会话管理 API | listSessions / 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、结构化输出。
一个容器跑多个 SDK 子进程是官方推荐的部署形态之一,不是反模式。“Long-running sessions: Run persistent container instances, often hosting multiple SDK processes per container.”
| 模式 | 容器 : session | 适用 |
|---|---|---|
| Ephemeral | 1 : 1,跑完销毁 | 一次性任务(bug 修复、发票抽取、文档翻译) |
| Long-running | 1 : N,常驻 | 高频消息流(邮件 triage、Slack bot、站点构建器) |
| Hybrid | 临时容器 + 启动时从 SessionStore 水合 | 跨多次交互但中间长时间空闲 |
| Multi-agent | 1 : 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_id 走 resume |
| 宿主配置 | 有四项 settingSources: [] 关不掉(见下表) |
逐项用对应开关关闭 |
| 环境变量 | bash 工具是子进程的子进程 —— 容器里的 ANTHROPIC_API_KEY、DB 密码、云凭证,agent 一条 env 就读出来 |
代理注入(ANTHROPIC_BASE_URL 或 HTTP_PROXY + TLS 终止代理),让凭证根本不进 agent 环境 |
settingSources: [] 关不掉的四项| 输入 | 唯一关闭方式 |
|---|---|
| 托管策略(MDM plist / 注册表 / managed settings) | 只能从宿主删除;server-managed 部分由组织管理员控制,SDK 侧无解 |
~/.claude.json(“Always read”) | CLAUDE_CONFIG_DIR 重定向 |
| Auto memory(会话启动时注入 system prompt) | autoMemoryEnabled: false 或 CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 |
claude.ai MCP connectors(传 mcpServers: {} 也压不住) | strictMcpConfig: true / disableClaudeAiConnectors: true |
“Do not rely on defaultquery()options for multi-tenant isolation. Because the inputs above are read regardless ofsettingSources, an SDK process can pick up host-level configuration and per-directory memory.” code.claude.com/docs/en/agent-sdk/claude-code-features
cwdresume,不用 continuesettingSources: []CLAUDE_CODE_DISABLE_AUTO_MEMORY=1CLAUDE_CONFIG_DIRcwd,每次 query 显式传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", }, }, })) { /* ... */ }
没有顶层 session 超时——“A session does not time out on its own.” 只能用 Options.maxTurns 限工具往返轮数。
长 session 内存会涨——官方建议 “Cap session length or recycle subprocesses periodically.”
canUseTool:工具执行前回调到你的进程SDK 相对 CLI 唯一的硬增量。理解它的实现机制,也就理解了「进程内」这三个字的准确含义。
--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 在最后一步PreToolUse)100% 覆盖bypassPermissions 下依然生效bypassPermissions 全放行 · acceptEdits 放行文件操作 · dontAsk 整个跳过第 6 步直接 denyallowedTools: ["Bash"] 这种裸名直接放行 —— 该工具永不进回调。带 scope 的 ["Bash(ls *)"] 只放行匹配项,其余照样进回调canUseTool 有漏网suggestions,能改写入参 —— 但上面任一步短路后就到不了这里v2.1.198 起,如果你传了一个评估流永远到不了的 canUseTool,TS SDK 会在构造 query 时发一次 Node 进程警告,code 为 CLAUDE_SDK_CAN_USE_TOOL_SHADOWED。触发条件正是上面两种:bypassPermissions,或每一条都是裸名的 allowedTools。可以 process.on('warning', …) 匹配 code 捕获。
canUseTool 还是 PreToolUse hook| 维度 | canUseTool | PreToolUse 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 aPreToolUsehook: hooks run before every other step, and a hook deny applies even inbypassPermissionsmode.” code.claude.com/docs/en/agent-sdk/permissions
canUseTool。canUseTool。SessionStore:镜像,不是替代 alpha默认 transcript 写在本机 ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl。多实例部署时,用户第二次请求被打到另一台机器就 resume 不了 —— 这就是它要解决的问题。
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[]>; };
| 可选方法缺失 | 后果 |
|---|---|
listSessions | continue: true 抛错;listSessions() 抛错(除非实现了 listSessionSummaries) |
listSessionSummaries | 退化为 listSessions() + 逐个 load() |
delete | 删除变 no-op(适合 S3 这类 append-only / WORM 后端) |
listSubkeys | 只恢复主 transcript,subagent 记录丢失 |
| 内容 | 镜像 | 说明 |
|---|---|---|
| 主 transcript | 是 | subpath 未设时 |
| subagent transcript | 是 | 走 subpath: "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 更可靠。
把「agent」这个词拆成两层,问题就自己回答了。
Anthropic 在每周迭代 —— 本机这版 0.3.150↔CC 2.1.150,npm 上已经 0.3.220。
自建第一版要一个月,写到能用要半年,然后每个 CC 版本都得重新对齐。唯一能换来的是「完全可控」,代价是放弃别人全职团队的迭代速度。
通用 harness 永远给不了你这些。护城河在「agent 知道什么」和「怎么确认它做对了」。
CLAUDE.md / skills / 检索。同一个 harness,喂进去的领域知识不同,产出质量差一个量级。
这里的工作量是写文档和建索引,不是写代码。它决定 agent 知道什么 —— 是「手脚」里最像手脚的部分。
结构化输出 + gate。没有这层,agent 只能当「辅助」;有了这层才能进流水线。
判断标准很直接:agent 说「完成了」,你有没有一个不依赖它自述的方式确认?没有的话,再强的 harness 也只是个更贵的建议生成器。
默认选独立 MCP server 进程 —— CLI 和 SDK 都能用,复用面更广。
只有当「每次调用都要过一遍你的业务规则」(鉴权 / 配额 / 审计 / 脱敏)时才用进程内 MCP(createSdkMcpServer)—— 这也正是 SDK 相对 CLI 唯一的硬增量。
阶段流转、人工确认点、失败重试 —— 应该是状态机,而不是 prompt 里的一段描述。
模型负责每个阶段内部的开放式判断;阶段之间的跳转由代码决定。
没有它,你改 prompt 是在赌;有了它,每次改动能测。
| 情况 | 为什么 / 改用什么 |
|---|---|
| 任务不需要文件系统和 shell | 纯对话、抽取、分类、路由。一个 query() 起 1 GiB RAM 的完整 Claude Code 进程是杀鸡用牛刀 → 直接用 Messages API,或 SDK 的 tool runner |
| 流程必须严格确定 | 审批流、状态机、有合规要求的操作链 → 把 LLM 当成流程里的一个函数调,不要让它当调度者 |
| 模型不是 Claude | harness 与模型是绑定的 —— 非 Claude 模型不支持远程 MCP,这个约束会一路传导到架构上 |
这不是可选的加固,是 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_URL 或 HTTP_PROXY + TLS 终止代理 |
| 并发的 cwd 隔离 | 默认全部继承应用工作目录 | 每次 query() 显式传 cwd;不传就互相覆盖文件 |
| 业务态持久化 | SessionStore 只镜像 transcript |
把结果落成你自己的应用状态,不依赖 resume 恢复工作现场 |
| 超时与资源回收 | 无顶层 session 超时;maxTurns 只限轮数 |
自己定期回收子进程;后台 subagent 可用 CLAUDE_ASYNC_AGENT_STALL_TIMEOUT_MS(停顿看门狗,不是总时长上限) |
| 租户隔离 | 五件套参数,且有四项输入绕过 settingSources |
隔离边界只能画在容器之外 —— 每租户一个文件系统 / 一个容器 |
① 与宿主共享内核 —— 内核漏洞可逃逸。② 代理不做 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。