用 Claude Agent SDK 封装自定义 Agent
Claude Code 的内核是一个自带运行时的原生可执行文件,Claude Agent SDK 的作用就是驱动这个内核。解读 Claude Agent SDK 可以了解有哪些能力可被外部配置、以及自定义 Agent 能从哪几个位置介入。
这一篇的目标因此有两层:
- 其一是从 SDK 的类型与实现反推出 Claude Code 对外开放的能力清单;
- 其二是给出封装自定义 Agent 的可操作路径,即:四种扩展点各自的侵入程度、代价与适用场景。
一、SDK 在架构上的位置
1.1 三层结构

最上层是你的应用,即调用 SDK 的 Python 代码。中间层是 SDK,它做三件事,把配置翻译成命令行参数、派生 CLI 子进程、在标准输入输出上收发消息并把它们解析成类型。最下层是 CLI 子进程,也就是 Claude Code 原生可执行文件,Agent 循环、工具执行、权限判定、上下文装配与压缩全部发生在它内部。
1.2 一条通道上的两种流量
SDK 与 CLI 之间只有一条标准输入输出通道,然而这条通道上跑着两种性质完全不同的流量。
- 第一种是会话消息:用户消息、助手消息、系统消息、结果消息与流式事件,它们是对话内容本身。
- 第二种是控制请求与控制响应:在会话之外调整运行时状态、或者由 CLI 反向征询应用的判断。
二者靠消息的顶层类型字段区分,其中控制流量的类型是 control_request 与 control_response。
复用一条通道的设计带来了一个直接的后果,即:消息读取循环必须承担路由职责。SDK 里 Query._read_messages 就是这个路由器,它把 "控制响应" 交给等待中的请求、把 "控制请求" 交给对应的回调、把 "其余消息" 推进给应用的异步迭代器。值得注意的是控制请求是双向的。
1.3 版本约定
SDK 与 CLI 之间的版本关系是显式的,即 _cli_version.py 里写着一个绑定的 CLI 版本号,而仓库里另有 scripts/download_cli.py 与 scripts/_cli_version_validation.py 两个脚本负责下载与校验。src/claude_agent_sdk/_bundled/ 目录用于放置打包进来的 CLI。
二、能力清单:四十八个配置字段

2.1 按用途分组
ClaudeAgentOptions 共四十八个字段,而这份清单就是 Claude Code 对外开放的能力清单。按用途分组之后它的结构相当清楚。
| 分组 | 字段 |
|---|---|
| 会话与恢复 | continue_conversation · resume · session_id · fork_session · resume_session_at · resume_drops_turn |
| 模型与思考 | model · fallback_model · thinking · max_thinking_tokens · effort · betas |
| 工具与权限 | tools · allowed_tools · disallowed_tools · permission_mode · can_use_tool · permission_prompt_tool_name · sandbox |
| 上下文与预算 | system_prompt · max_turns · max_budget_usd · task_budget · output_format |
| 扩展 | mcp_servers · strict_mcp_config · hooks · agents · skills · plugins |
| 状态与落盘 | session_store · session_store_flush · enable_file_checkpointing · load_timeout_ms |
| 观测 | include_partial_messages · include_hook_events · forward_subagent_text · stderr · debug_stderr |
| 工程 | cwd · cli_path · settings · setting_sources · add_dirs · env · extra_args · max_buffer_size · user |
这张表里有几处值得单独说明。
- setting_sources 的取值是用户、项目与本地三者的列表,也就是说应用可以决定这次运行读取哪些层的配置文件。这一处对封装尤其要紧,理由是一个面向最终用户分发的自定义 Agent 通常不希望读取用户本机的个人配置,否则同一份产品在不同人机器上行为不同。把它显式置为空列表或只含项目一层,可以让运行环境变得可复现。
- extra_args 是一个逃生舱,即任意命令行参数的字典,其值为空时按布尔开关处理。它的存在说明 SDK 并不打算把 CLI 的全部参数都做成字段,因此新增的 CLI 能力不必等 SDK 发版即可使用。
- tools 与 allowed_tools 是两件事,而混淆二者是一处常见错误。前者决定这一次运行有哪些工具存在,其取值可以是一个列表或一个预设;后者决定哪些工具的调用无需逐次确认。第三篇里读类型声明时得出的那个结论在此得到源码确认,即工具可用性与权限判定是两套正交的东西。
2.2 配置字段作用
上一节的分组只回答了这些字段大致管什么,而下面这张表逐字段给出作用。表中的说明取自源码里每个字段自带的文档串,因此它是可核验的,而非本文的概括。
| 字段 | 作用 |
|---|---|
| tools | 声明本次运行存在哪些内置工具。空列表即禁用全部内置工具,预设取值即使用全部默认工具。 |
| allowed_tools | 声明哪些工具的调用无需征求确认。传入技能工具名的写法已被标为弃用。 |
| disallowed_tools | 声明哪些工具被禁用。它们被从模型上下文里移除,即便本会被允许也不可用。 |
| permission_mode | 会话的权限模式,取值见后文。 |
| can_use_tool | 自定义权限处理函数,仅在 CLI 的权限规则求值为询问时被调用。 |
| permission_prompt_tool_name | 把权限征询改由某个 MCP 工具承担,而不走默认处理。 |
| sandbox | 命令执行的隔离设置。文件与网络的限制由权限规则表达,此处只控制沙箱自身行为。 |
| system_prompt | 系统提示词。可传自定义字符串、默认预设、预设加追加内容,或一个文件路径。 |
| max_turns | 轮次上限,一轮指一条用户消息加一次助手回复。 |
| max_budget_usd | 本次查询的美元预算上限,超出即以一个专门的预算超限结果结束。 |
| task_budget | 以 token 计的任务预算,其作用是让模型知道自己的剩余额度从而自行收尾。 |
| output_format | 结构化输出的格式配置,其结构与 Messages API 一致。 |
| model | 使用的模型,未指定时取 CLI 默认。 |
| fallback_model | 主模型失败或不可用时的回退模型。 |
| thinking | 思考行为,三种取值为自适应、固定预算与关闭。 |
| max_thinking_tokens | 思考的 token 上限,已被标为弃用,新模型上仅按开关处理。 |
| effort | 努力等级,五档从低到最大,与自适应思考配合决定思考深度。 |
| betas | 启用 beta 特性,当前仅一项,即百万 token 上下文窗口。 |
| continue_conversation | 继续当前目录下最近的一次会话,与恢复指定会话互斥。 |
| resume | 要恢复的会话标识,加载该会话的历史。 |
| session_id | 指定会话标识而非自动生成,须为合法 UUID。 |
| fork_session | 恢复时派生出新的会话标识,而不是接着写原会话。 |
| resume_session_at | 恢复时只加载到指定 UUID 的那条消息为止,用于从中途分叉。 |
| resume_drops_turn | 与上一项配合,声明这次截断恢复打算丢弃哪一轮,由 CLI 在加载时校验。 |
| mcp_servers | MCP 服务器配置,键为服务器名,也可传一个配置文件路径。 |
| strict_mcp_config | 只使用本次传入的 MCP 服务器,忽略 CLI 本会加载的其余全部 MCP 配置 |
| hooks | 各生命周期事件的回调配置。 |
| agents | 以编程方式定义可被派发的具名子 Agent。 |
| skills | 为主会话启用哪些技能,也是启用技能的唯一入口。 |
| plugins | 为本会话加载插件,当前仅支持本地插件。 |
| session_store | 把会话记录镜像到外部存储,恢复时本地文件缺失可改从该存储生成。 |
| session_store_flush | 镜像刷写时机,批量或即时。 |
| enable_file_checkpointing | 启用文件检查点,从而可把文件回退到某条用户消息时的状态。 |
| load_timeout_ms | 恢复时每次调用外部存储的超时,默认六万毫秒。 |
| include_partial_messages | 在输出里包含流式的部分消息事件。 |
| include_hook_events | 把 hook 生命周期事件也作为消息发到流里。 |
| forward_subagent_text | 把子 Agent 的文本与思考块也转发到流里,默认只转发工具调用与结果。 |
| stderr | 子进程标准错误的回调。 |
| debug_stderr | 已弃用且传输层不再读取,改用上一项。 |
| cwd | 会话的工作目录,默认取进程的工作目录。 |
| cli_path | CLI 可执行文件路径,未指定时用打包进来的那一份。 |
| settings | 额外载入一份设置文件,其层级在用户可控设置中优先级最高。 |
| setting_sources | 控制加载哪几层文件系统设置,取值为用户、项目与本地。 |
| add_dirs | 除工作目录之外允许 Agent 触达的目录,须为绝对路径。 |
| env | 传给子进程的环境变量。 |
| extra_args | 任意 CLI 参数,取值为空时按布尔开关处理。 |
| max_buffer_size | 读取子进程标准输出时的缓冲上限字节数。 |
| user | 与会话关联的用户标识。 |
三、控制协议:十个正向方法与三个反向请求
3.1 SDK 向 CLI 发起的十个正向方法

从 Query 的实现里可以逐个数出 SDK 实际发送的控制请求子类型,共十个。
| 子类型 | 作用 |
|---|---|
| initialize | 握手,注册 hook 回调与自定义 Agent 定义 |
| interrupt | 中断当前轮次 |
| set_permission_mode | 运行中切换权限模式 |
| set_model | 运行中切换模型 |
| rewind_files | 把被跟踪的文件回退到某条用户消息时的状态 |
| mcp_status | 查询 MCP 服务器连接状态 |
| mcp_reconnect | 重连某个 MCP 服务器 |
| mcp_toggle | 启用或禁用某个 MCP 服务器 |
| stop_task | 停止某个正在运行的任务 |
| get_context_usage | 取上下文用量的分类明细 |
3.2 CLI 向应用发起的三个反向请求
反向请求共三个,而它们恰好对应三种必须由应用回答的问题。
- can_use_tool 问的是这一次工具调用是否允许,其载荷含工具名、入参、权限建议、被阻断的路径、判定理由与工具调用标识。应用的回答是允许或拒绝,其中允许可以附带改写后的入参与权限更新,而拒绝必须给出消息且可以要求中断。
- hook_callback 问的是某个已注册的 hook 该返回什么,其载荷含回调标识与该事件的输入。
- mcp_message 是一条 JSON-RPC 消息,它把 CLI 对进程内 MCP 服务器的调用转交给应用。
这三个的共同点是它们都不能被应用忽略,即 CLI 会等待响应。因此这三处回调里的任何阻塞都会挂住整个会话,而这一点在实现自定义 Agent 时是最容易踩的坑。
3.3 握手时发生了什么

initialize 请求的构造过程说明了 hook 的注册机制。SDK 遍历应用给出的 hook 配置,为每个回调函数生成一个形如 hook_N 的标识,把标识与函数的映射留在自己这边,只把标识、匹配器与超时发给 CLI。此后 CLI 触发 hook 时发回的是标识,由 SDK 查表调用对应的函数。
据此可以推断一条设计取舍,即 CLI 一侧完全不需要知道 hook 的实现语言与实现方式,它只持有标识。这使同一个内核可以被任意语言的 SDK 驱动,而代价是回调的往返要跨一次进程。
自定义 Agent 定义同样在握手时发送,而不经命令行参数,源码里对此有一行注释明确说明。理由可以推断,即 Agent 定义是结构化的嵌套对象,若走命令行则要序列化成一个很长的参数值。
四、四种扩展点,按侵入程度排序
封装自定义 Agent 的全部着力点就是这四处。它们的侵入程度依次递增,而选哪一处取决于你要改的是能力、是判定、是流程还是是角色。

4.1 进程内 MCP 工具:给 Agent 加能力
这一处是四种里最该优先考虑的,理由是它的成本最低而收益最直接。
机制是一个装饰器加一个工厂函数。@tool 装饰器接受工具名、描述与入参模式三项,把一个异步函数登记成工具;create_sdk_mcp_server 把若干这样的工具组装成一个 MCP 服务器配置,其类型为 sdk,随后把它放进 mcp_servers 即可。

关键之处在于这个服务器跑在你自己的进程里,而不是另起一个子进程。源码的文档串把收益列成四条,即没有进程间通信开销、部署只有一个进程、调试在同一进程内、以及可以直接访问应用自身的状态。最后一条是本文认为最要紧的,例如一个工具需要读你应用里的数据库连接池或已登录用户上下文,进程内工具可以直接拿到,而外部 MCP 服务器必须另建一条通路。
其代价是这些工具的调用要经过一次完整往返,即 CLI 把 JSON-RPC 消息经 mcp_message 反向请求发给 SDK,SDK 交给桥接层,桥接层送进你的服务器实例,再原路返回。因此工具实现里的阻塞会挂住会话。
mcp_servers 支持的配置类型共五种,即标准输入输出子进程、SSE、HTTP、进程内 SDK 服务器与一种代理形态。也就是说进程内工具与外部工具可以并存,而 Agent 那一侧看到的是同一批工具。
4.2 工具权限回调:给 Agent 加判定
第二处是 can_use_tool,其签名接受工具名、入参与一个上下文对象,返回允许或拒绝。允许可以改写入参,这一点使它不只是一道闸门,还是一处可以修正模型意图的位置。

而这一处有一个必须写在最前面的陷阱,即这个回调会被静默旁路,而 SDK 为此专门定义了一个警告类型。源码里的判定逻辑列出三种旁路成因。
- permission_mode 取 bypassPermissions 时,除显式拒绝规则之外的每一次工具调用都在回调之前被自动批准。
- allowed_tools 里任何一条整体放开某个工具的条目都会使该工具在回调之前被自动批准。源码里的规则解析器与 CLI 一致,即不带括号的条目、括号内为空的条目、以及括号内是单个通配符的条目都算整体放开,而带真实限定的条目不算。
- skills 取全部时,传输层会往有效的允许列表里追加一个不带限定的技能工具名,因此它与手写条目一样会旁路回调。
这三条之外还有一条更要紧的,即源码在警告文案里自己承认设置文件里的允许规则同样会旁路回调,然而那些规则在此处不可见。也就是说这个警告在构造上就是不完备的。
据此可以推断一条对自研的直接建议,即若你要的是每一次工具调用都必须经过你的判定,那么不要用权限回调,而要用工具使用前 hook。这也是源码里那段警告文案给出的建议。
4.3 hooks:给 Agent 加流程

第三处是 hooks,共十个事件。
| 事件 | 触发时机 |
|---|---|
| PreToolUse | 工具调用之前,可返回允许、拒绝、询问或延后,并可改写入参 |
| PostToolUse | 工具调用之后,可追加上下文或改写工具输出 |
| PostToolUseFailure | 工具调用失败之后,载荷含错误与是否为中断 |
| UserPromptSubmit | 用户提示词提交时,可追加上下文 |
| Stop | Agent 停止时 |
| SubagentStart | 子 Agent 启动时 |
| SubagentStop | 子 Agent 停止时,载荷含子 Agent 的记录文件路径 |
| PreCompact | 压缩之前,载荷含触发方式与自定义指令 |
| Notification | 通知时 |
| PermissionRequest | 权限征询时,可直接给出判定 |
hook 的返回结构分同步与异步两种。同步返回可含是否继续、是否抑制输出、停止原因、阻断决定、系统消息与一个按事件区分的专属输出对象;异步返回则只声明这是一个异步 hook 并给出超时。
这里有一处实现细节值得记,因为它是跨语言 SDK 的典型问题。Python 里 continue 与 async 是关键字,因此 SDK 的类型用 continue_ 与 async_,而发往 CLI 之前有一个专门的函数把这两个名字改回去。也就是说协议侧的字段名与 Python 侧的字段名不同,而这个差异被一个十几行的转换函数吸收了。据此可以推断若你要自建协议且计划支持多语言 SDK,则字段命名应当避开各语言的关键字,否则每个 SDK 都要维护一份这样的映射。
hook 与权限回调的分工可以这样把握,即前者覆盖面完整且能干预流程,后者只管工具调用且可被旁路。因此把它们叠加使用是合理的,即用 hook 保证覆盖,用回调处理需要改写入参的那些情况。
4.4 自定义 Agent 定义:给 Agent 加角色扮演
第四处侵入最深,即用 agents 字段声明若干具名 Agent,每个由十三个字段描述。
其字段包括描述与提示词、可用工具与禁用工具、模型、技能、记忆作用域、MCP 服务器、初始提示词、最大轮次、是否后台运行、努力等级与权限模式。也就是说一个自定义 Agent 定义几乎是一份完整的运行配置,其粒度与顶层选项相当。
值得注意的是工具限制写在被定义的 Agent 自身,而不写在派发它的那一侧。这与本系列第四篇在 Kiro 上观察到的做法一致,其收益是一个 Agent 的权限上界成为一处可查的事实。
据此可以推断这一处的适用场景是你要提供的不是一个通用助手,而是若干各有专长且权限各不相同的角色,例如一个只读的调查角色与一个可写的修改角色。而若你只需要一个角色,那么用顶层选项即可,不必引入这一层。
五、会话状态:可替换的存储与镜像

SDK 把 session 落盘做成了可替换的,其接口是一个只有六个方法的协议,即:追加、载入、列出会话、列出会话摘要、删除、列出子键。应用只要实现这六个方法即可把会话存到任何地方,仓库的示例里给了 PostgreSQL、Redis 与 S3 三份实现。
启用它的方式是给 session_store 赋值,而传输层随即在命令行上追加一个会话镜像开关。也就是说 CLI 仍然按自己的方式落盘,同时把记录镜像给 SDK,由 SDK 交给你的存储。据此可以推断这是一处刻意的双写而非替换,理由是 CLI 的恢复与回放逻辑依赖它自己的落盘格式。
刷写策略有两种取值,即批量与即时。仓库里另有一个专门的批处理器负责把镜像帧攒批,而镜像写入失败会被转换成一条系统消息交给应用,其类型是镜像错误。这一处处理值得学,即派生物的写入失败不能拖垮主流程,然而也不能静默丢弃,正确做法是把失败作为一条可观测的消息交出去。
除存储协议之外,SDK 还提供了一组会话管理函数,即列出会话、取会话信息、取会话消息、列出子 Agent、取子 Agent 消息、重命名、打标签、删除与派生,且每一个都有面向自定义存储的对应版本。也就是说会话管理这件事在 SDK 层是完整的,不需要应用自己解析落盘文件。
六、三处值得注意的实现细节
这三处都不是功能,而是四家实现里那种只有读源码才能看到的东西。
6.1 等号形式与参数注入
传输层构造命令行时,对 resume、session_id、resume_session_at 与 resume_drops_turn 四个参数一律使用等号形式,源码里的注释给出了理由。CLI 把这些参数声明为可带可不带取值,因此在两个词元的形式下,一个以短横线开头的取值不会被绑定到参数上,而会被解析成另一个参数,从而一个不可信的取值可以注入任意参数。等号形式总是把取值绑定到参数。
extra_args 里也有同一道防护,即取值以短横线开头时改用等号形式。
这一处对自研 Agent 的参考意义相当直接,即凡是把外部输入拼进命令行的地方都要问一遍这个问题,而这类漏洞的成因不是疏忽而是参数解析器的默认行为。
6.2 一个不完备的警告
前文那处权限回调旁路的警告还有两个实现细节值得记。其一是警告只在构造查询时发一次,且用了一个特定的栈层级参数,其效果是同一条消息在一个进程里只出现一次而不是每个调用模块各出现一次。其二是源码明确说明这是建议性的而不抛异常,理由是旁路有时是有意的,例如一个回调只用于处理允许列表之外的工具。
6.3 双向需求的判定
Query 里有一个方法叫做是否仍有双向需求,其作用是判断 CLI 是否还可能发来需要回复的控制请求。这一处存在的理由是关闭标准输入的时机,即若应用已经不再发送输入,但 CLI 仍可能反向征询,那么过早关闭输入会使那次征询无法被回答。
据此可以推断这是一个真实踩过的坑,而它对自研 Agent 的提示是双向协议的关闭时机不能只看一侧,必须由两侧的在途请求共同决定。
七、封装自定义 Agent 的骨架

把前面各节合起来,一个自定义 Agent 的封装骨架有四步,而每一步对应一处已经交代过的机制。
第一步是收紧运行环境。把 setting_sources 显式声明为你需要的那几层而非默认全读,把 cwd 与 add_dirs 限定到你允许 Agent 触达的目录,并按需给出 sandbox。这一步的目的是让同一份产品在不同用户机器上行为一致。
第二步是加能力。用 @tool 与 create_sdk_mcp_server 把你的业务动作做成进程内工具,理由是它们可以直接访问应用状态。此处的纪律是每个工具的入参模式要写准,因为它就是模型看到的契约。
第三步是加判定。若你需要每一次工具调用都经过判定,则用工具使用前 hook 而不是权限回调;若你需要改写入参,则叠加权限回调,同时明确知道它会被允许列表旁路。此处最要紧的一条是不要把 permission_mode 设为绕过权限,因为那会使前述两处判定同时失效。
第四步是接状态与观测。实现那六个方法的存储协议把会话落到你自己的存储里,并订阅镜像错误消息;打开部分消息与 hook 事件两个开关以获得完整的过程可见性,同时接住 stderr 回调。
而这套骨架有一处必须承认的边界。它能改的是 Agent 的能力、判定、流程与角色,改不了 Agent 循环、上下文装配与压缩策略。若你的需求落在后者,那么应当自建 harness,而本系列第六篇给的正是那条路的实现纪律。
全文小结
读 SDK 与读内核得到的是两类不同的东西。内核不可读,因此第三篇只能推断顺序;而 SDK 可读,因此本篇可以把配置面、协议面与扩展面写成可核验的清单,即四十八个配置字段、十个正向控制方法、三个反向请求与十个 hook 事件。
从这份清单里能得到一个判断,即 Claude Code 对外开放的是能力的配置权与流程的介入权,而不是循环的改写权。四种扩展点全部落在能力、判定、流程与角色这四件事上,没有一处能改动 turn 的推进方式。
本篇最值得记住的可能是那处不完备的警告。一个应用装上权限回调,通常会认为每一次工具调用都会经过它,而实际上允许列表里一条整体放开的条目、一个绕过权限的模式、或者设置文件里的一条允许规则都会让它静默失效。SDK 为此专门定义了一个警告类型,然而源码自己承认那个警告看不到设置文件那一侧。就自研 Agent 而言,这一处的教训不是要照抄这个警告,而是若某个回调可以被旁路,那么旁路的全部成因必须能被枚举并告知调用者。