Claude Code 子代理进阶:放在哪一层、每个字段管什么、前台和后台、嵌套、并发上限和 fork
子代理的定义文件不止名字、说明和工具三行。本站在 Claude Code 2.1.304 上逐项实测:同名时哪一层生效,十来个字段各管什么,哪些设置在 -p 里因为文件夹没被信任而悄悄失效,前台和后台拿到的工具有什么不同,嵌套和并发的上限,以及怎么续上一个子代理、fork 和普通子代理差在哪。
本站已实测,测试日期:2026-10-21。
本页目录(11 节)
《子代理实战》写了最基本的用法:一个 Markdown 文件,给名字、说明和工具清单,Claude Code 就能把一类活交给它。这一篇往下讲官方文档里那些没展开的部分,每一项都在服务器上实测过:
- 同一个名字的子代理放在好几处,哪一个生效;
- frontmatter 里十几个字段各管什么,哪些在
claude -p里会悄悄不生效; - 前台和后台跑的子代理拿到的工具不一样;
- 子代理还能再派子代理,最多几层,同时最多几个;
- 怎么续上一个跑完的子代理,fork 和普通子代理差在哪。
实测环境
| 项目 | 内容 |
|---|---|
| 日期 | 2026-10-21 |
| 版本 | Claude Code 2.1.304 |
| 系统 | Ubuntu 24.04 |
| 模型 | 主对话 Sonnet 5.5(--model claude-sonnet-5-5),effort 是 medium |
| 连接方式 | 通过酷站AI 中转站,不是直连官方 |
| 怎么跑的 | 一个小的 Git 仓库,项目层放了 17 个测试用的子代理,用户层放了 3 个(测完删了);全部用 claude -p 跑,一共 31 次,Claude Code 自己按官方价估算约 1 美元 |
| 怎么看结果 | 子代理的会话记录在 ~/.claude/projects/<项目>/<会话 ID>/subagents/ 下,每个子代理一个 agent-<ID>.jsonl。本站从里面读出它的类型、嵌套深度、前台还是后台、实际用的模型和 effort、拿到的工具清单,以及上下文里有没有某个词。读法见第七节 |
交互式界面里的东西(子代理面板、/tasks、Ctrl+B、/subtask)这次没有测,照文档写的地方会注明。
一、放在哪一层,同名时谁生效
子代理的定义可以放在五个地方,名字相同时按这个顺序,靠前的生效:
| 位置 | 范围 | 先后 |
|---|---|---|
| 托管设置(组织统一下发) | 整个组织 | 1 |
启动时的 --agents 参数 |
这一次会话 | 2 |
项目里的 .claude/agents/ |
这个项目 | 3 |
用户目录的 ~/.claude/agents/ |
你的所有项目 | 4 |
插件的 agents/ 目录 |
装了这个插件的地方 | 5 |
本站在项目层和用户层各放一个叫 whoami 的子代理,系统提示分别让它回答“我是项目层的”“我是用户层的”,又写了一个 --agents 用的 JSON 文件:
项目层 + 用户层 → 我是项目层的 whoami。
项目层 + 用户层 + --agents 文件 → 我是命令行的 whoami。
和文档的顺序一致。另外几点:
- 项目层的定义可以放在子目录里。 本站那个是
.claude/agents/review/whoami.md,照样认。认的是文件里的name,不看文件名和目录。同一个目录树里两个文件写了同一个name,只加载其中一个,加载哪个文档说没有规定。 --agents在-p模式下可以给一个 JSON 文件的路径,定义太长时不用全塞在命令行里(2.1.281 起)。交互式会话不接受文件路径。JSON 的写法:
{"whoami": {"description": "回答“你是谁”", "prompt": "不管收到什么任务,只回答一句:我是命令行的 whoami。", "tools": ["Read"]}}
- 插件里的子代理不认
hooks、mcpServers、permissionMode三个字段,文档说是出于安全考虑。要用这几个字段,把文件复制到项目层或用户层。
二、frontmatter 的字段
只有 name 和 description 必填。多个单词的字段名用驼峰写法(maxTurns、disallowedTools),拼错了 Claude Code 不报错,直接忽略。
| 字段 | 管什么 | 本站 |
|---|---|---|
tools |
允许用的工具;不写就继承 | 测了 |
disallowedTools |
不许用的工具 | 测了 |
model |
用哪个模型:sonnet、opus、haiku、fable、完整的模型 ID,或 inherit |
测了 |
effort |
这个子代理的 effort | 测了 |
permissionMode |
权限模式 | 测了 |
skills |
启动时预先装进上下文的 skill | 测了 |
omitClaudeMd |
启动时不读 CLAUDE.md | 测了 |
memory |
给它一个跨会话的记忆目录 | 测了 |
maxTurns |
最多跑几轮 | 测了 |
hooks |
只在这个子代理里生效的 hooks | 测了,有坑 |
mcpServers |
只给这个子代理的 MCP 服务器 | 测了,有坑 |
background |
设成 true 时总在后台跑 |
照文档 |
isolation |
设成 worktree 时在一个临时的 git worktree 里干活 |
照文档 |
autoCompactWindow |
让它提前压缩上下文,取值 100000 到 1000000 | 照文档 |
color、initialPrompt、experimental |
显示颜色;作为整个会话的代理时自动发的第一句;experimental.cacheTtl 选缓存时长 |
照文档 |
下面逐个讲测了的。
tools 和 disallowedTools:带参数的拒绝会拿掉整个工具
disallowedTools 里写 Bash(git push *),本意是“别的命令可以跑,就是不许 push”。实测这个子代理的工具清单里整个 Bash 都没了,让它跑 git status,它说自己没有能执行命令的工具。
文档也是这么写的:带参数的条目照样移除整个工具。只想拦某几条命令,应该在设置文件的 permissions.deny 里写 Bash(git push *),主对话和子代理都管得到。
两个字段同时写时,先按 disallowedTools 去掉,再在剩下的里面按 tools 挑。MCP 工具可以整台服务器一起写:mcp__github__* 放进 tools 是给它这台服务器的全部工具,mcp__github 放进 disallowedTools 是全部拿掉。服务器名本身带 __ 时(比如 reports__v2),按 10 月 22 日的文档:tools 里要写全名加 __*,即 mcp__reports__v2__*,不带结尾的 __* 一个工具也匹配不上;disallowedTools 里写给 reports 的 mcp__reports 或 mcp__reports__*,会把 reports__v2 的工具也一起拿掉(没有实测)。
model:先后顺序,以及走中转站时 haiku 用不了
子代理用哪个模型,按这个顺序定:
- Claude 派活时传的
model参数; - 定义里的
model(inherit就是跟主对话一样); - 环境变量
CLAUDE_CODE_SUBAGENT_MODEL; - 主对话的模型。
本站的测试 key 调不了 Haiku 5.5,正好用来看清这个顺序:
| 设置 | 子代理实际请求的模型 | 结果 |
|---|---|---|
定义里写 model: haiku |
claude-haiku-5-5 |
失败:Agent terminated early due to an API error,后面跟着 404、model_not_found |
同上,另设 CLAUDE_CODE_SUBAGENT_MODEL=sonnet |
claude-haiku-5-5 |
一样失败。环境变量排在定义后面,盖不过它 |
再加 CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1 |
claude-sonnet-5-5 |
成功 |
CLAUDE_CODE_SUBAGENT_MODEL_FORCE 设成 1(2.1.257 起),会忽略定义里的 model,所有子代理一律用环境变量指定的模型。走中转站、key 只开了部分模型时,这是不改别人写的子代理文件也能跑起来的办法。不过 fork 和 model: inherit 的 skill 子代理仍然跟主对话一样。
子代理失败时,主对话收到的是完整的报错,它照实转告了,没有自己代答。
/tasks 里每个子代理那一行会写它用的模型,文档说 2.1.242 起有;本站在 -p 里没法看这个界面。
effort
定义里写 effort: low,这个子代理的请求参数里 effort 就是 low,主对话还是 medium。文档说环境变量 CLAUDE_CODE_EFFORT_LEVEL 比它优先。
permissionMode:主对话的模式说了算
主对话在 -p 默认的 Manual(default)模式下:
| 子代理的定义 | 让它新建一个文件 |
|---|---|
permissionMode: acceptEdits |
写成了 |
| 不写 | 被拒:Permission to use Write has been denied. |
但这个字段不是总生效。按文档:
- 主对话是
bypassPermissions、acceptEdits或 auto 时,子代理一律跟主对话一样,你写的值被忽略。 - 主对话是
default、dontAsk或plan时,才用你写的值。例外是写了bypassPermissions(还是跟主对话),以及写了auto但这个子代理用不了 auto。
也就是说,主对话已经放开(acceptEdits、auto、bypass)时,子代理写什么都跟主对话一样;主对话是 Manual 时,子代理才能按自己写的放宽,但放不到 bypass。
skills:预先装进去,和自己去调不一样
项目里有一个 skill,内容里写着项目代号。两个子代理都只给了 Read、都被要求不读文件:
| 定义 | 问它项目代号 | 上下文里有没有这个词 |
|---|---|---|
写了 skills: [house-style] |
答对了,还说是从这个 skill 里看到的 | 有 |
| 没写 | 不知道 | 没有 |
skills 管的是“启动时预先装进去”,整篇内容都会进上下文(最多前 32 个),不只是说明。不写这个字段,子代理在干活时仍然可以自己用 Skill 工具去调项目、用户和插件里的 skill;不想让它碰 skill,把 Skill 从 tools 里去掉。
omitClaudeMd:启动时不读 CLAUDE.md
项目的 CLAUDE.md 里写了一个暗号:
| 定义 | 问它暗号 | 上下文里有没有 CLAUDE.md 的内容 |
|---|---|---|
写了 omitClaudeMd: true |
不知道 | 没有 |
| 没写 | 答对了 | 有 |
两个都看得到仓库的分支和最近的提交:git 状态的快照照样会给,文档说除了内置的 Explore 和 Plan,别的子代理都会带上。
除了 CLAUDE.md,文档列了子代理启动时还会拿到什么:它自己的系统提示、Claude 写的任务说明、git 状态、预装的 skill,以及(在它能用 SendMessage、而且会话里有别的具名代理时)一份可以发消息的代理名单。拿不到的:你的对话历史、你调用过的 skill、主对话读过的文件、输出风格、主对话的自动记忆。
memory:给它一个自己的笔记本
memory: project 的子代理只给了 Read。让它“记住:发布之前要先跑 make check”:
- 它的工具清单里自动多了
Write和Edit,系统提示里多了一段记忆的说明; - 它在
.claude/agent-memory/mem/下写了一个笔记文件,又在同一目录的MEMORY.md里加了一行索引; - 换一个新会话问它“发布之前要做什么”,它答出了
make check,并说这是记忆里的约定。
三种范围:user 存在 ~/.claude/agent-memory/<名字>/,project 存在项目的 .claude/agent-memory/<名字>/(可以提交到仓库,文档推荐这个),local 存在 .claude/agent-memory-local/<名字>/。关掉了自动记忆(autoMemoryEnabled 或 CLAUDE_CODE_DISABLE_AUTO_MEMORY)的话,这个字段不起作用。
maxTurns:到上限交回部分结果
maxTurns: 2 的子代理被要求一个一个读完 7 个文件再报告行数。两轮之后它停了,主对话收到的结果开头是:
NOTE: this agent stopped at its 2-turn limit before finishing. The text below is PARTIAL output; treat it as incomplete. Send the agent a message (SendMessage) to let it continue from where it stopped.
主对话照实说了它没做完,并问要不要用 SendMessage 让它接着做。
hooks 和 mcpServers:在 -p 里,项目层的会悄悄失效
这是这次测出来最容易踩的坑。
| 子代理放在 | frontmatter 里的 PreToolUse hook |
frontmatter 里自带的 MCP 服务器 |
|---|---|---|
项目层 .claude/agents/ |
没有触发 | 没有加载,工具清单里没有它的工具 |
用户层 ~/.claude/agents/ |
触发了 | 加载了 |
两个都没有报错,子代理照常跑,只是那部分配置不生效。原因是文档里的信任规则:项目层子代理的 frontmatter hooks 和自带的 MCP 服务器,要你信任过这个文件夹(交互式启动时那个信任对话框)才会加载;父目录信任过不算,-p 会话也不算。用户层的和用 --agents 传进来的不受这条限制(自带的 MCP 服务器,托管设置里的也不受)。
所以要在 CI 或脚本里用带 hooks、带 MCP 的子代理,定义放在用户层或者用 --agents 传,别依赖项目层。
用户层那个带 MCP 的子代理还碰到第二道关:工具在了,但调用时被拒,提示还没授予权限。-p 默认模式下 MCP 工具要批准,加上 --allowedTools mcp__history__get_release_notes 之后才查到了结果。
自带的 MCP 服务器只在子代理启动时连上、结束时断开,主对话里没有它的工具,也就不占主对话的上下文。写成服务器名字(比如 - github)则是复用主对话里已经配好的那一台。
三、前台和后台
同一个不限工具的子代理,跑两次:
| 方式 | 拿到的工具 |
|---|---|
| 前台(主对话等它做完) | 10 个:Agent、Bash、Edit、ListAgents、Read、ReportFindings、Skill、ToolSearch、Write,加一个延迟加载工具的占位项 |
后台(run_in_background) |
8 个:少了 ListAgents 和 ReportFindings |
文档说后台子代理只保留一张内置工具的白名单(读写、搜索、命令、网页、任务清单、Skill、SendMessage 等),不在名单上的一律去掉,而且不报错。同一个定义,前台和后台拿到的工具可能不一样,写子代理时如果依赖某个工具,要留意它在不在后台的名单里。
在哪里跑,各个入口不一样:
claude -p和 Agent SDK:由 Claude 自己判断要不要等。本站这 31 次里,大多数它选了前台,有几次放到后台、跑完再回答,你要求“等它做完”它就会等。- 交互式终端、VS Code、桌面应用:子代理默认在后台。想让 Claude 等某个子代理的结果,要先关掉 fork 模式(
CLAUDE_CODE_FORK_SUBAGENT=0),见下面 fork 一节。等的时候按Ctrl+B可以把它放到后台。这部分本站没有测。
后台子代理遇到要批准的操作,会把确认框弹到主对话里,并写明是哪个子代理在问。你选了“这次会话都允许”,这个许可对主对话也生效。
四、嵌套和并发
子代理也能派子代理。 本站写了一个会“看看自己有没有 Agent 工具,有就再派一个自己”的子代理:
| 设置 | 结果 |
|---|---|
| 默认 | 派到第 3 层,第 3 层的工具里没有 Agent,到此为止 |
CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH=2 |
到第 2 层为止 |
默认是主对话下面三层,设成 1 就是不许嵌套。到了上限,Claude Code 直接把 Agent 工具从子代理手里拿走,它只能自己干完、交回一份总结。不想让某个子代理再往下派,把 Agent 从它的 tools 里去掉就行。这个默认值改过几次(2.1.172 到 2.1.216 是五层,2.1.217 到 2.1.218 是一层),版本旧的话以自己的为准。
同时跑的子代理有上限。 默认 20 个。本站把上限设成 2:
export CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS=2
再让它一次在后台派 4 个:前两个正常,后两个当场返回
Concurrent subagent limit reached. You can run 2 subagents at once. Do not retry. If the user wants more concurrent subagents, ask them to increase CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS.
主对话照报错的要求没有重试,汇总时说明了有两个没派出去。这个上限只管同时在跑的,一个会话一共派多少个不限。
五、续上一个子代理,以及 fork
续上
每次派子代理都是一个新的实例。想接着它刚才的活干,让 Claude 续上它:子代理跑完时 Claude 会拿到它的 ID,之后用 SendMessage 发给这个 ID,子代理就带着完整的历史接着跑。
本站先在一个会话里让 lister 列出 src,再用 claude -p --resume <会话 ID> 说“让刚才那个 lister 接着列出 .claude/agents,续上它,不要新开”。主对话调了 SendMessage,返回 Resuming agent,还是原来那个 ID,它列完了第二个目录。
几个注意的地方,都出自文档:内置的 Explore 和 Plan 是一次性的,没有 ID,续不了;子代理的会话记录和主对话分开存,主对话压缩不影响它们,默认 30 天后清理。
fork
fork 是一种特殊的子代理:它继承到这一刻为止的整个对话,系统提示、工具、模型都和主对话一样,只是它中间的工具调用不进你的主对话,最后只交回结果。
本站在提示里先说了一个数字,再让 Claude 分别派一个 fork 和一个 general-purpose 子代理去回答“用户的幸运数字是多少”,并要求任务说明里不写这个数字:
| 子代理 | 回答 | 上下文里有没有这个数字 |
|---|---|---|
fork(主对话调 Agent 时 subagent_type 是 fork) |
答对了,说是从对话上下文里读到的 | 有 |
| general-purpose | 不知道,还翻了几个文件也没找到 | 没有 |
文档里 fork 和普通子代理的区别:
| fork | 普通子代理 | |
|---|---|---|
| 上下文 | 整个对话历史 | 从任务说明开始 |
| 系统提示和工具 | 和主对话一样 | 来自它的定义,后台跑时还要再筛一遍 |
| 模型 | 和主对话一样 | 来自 model 字段 |
| 提示缓存 | 和主对话共用,所以同样的活 fork 更便宜 | 单独一份 |
fork 模式从 2.1.302 起默认打开,Claude 可以自己决定开 fork。不想要:只禁 fork,在 permissions.deny 里加 Agent(fork);整个关掉,设 CLAUDE_CODE_FORK_SUBAGENT=0。交互式会话里还可以自己用 /subtask 任务 开一个 fork,本站没有测。
六、关掉某些子代理
| 想做的 | 怎么设 |
|---|---|
| 不许用某个子代理 | permissions.deny 里写 Agent(Explore)、Agent(你的子代理名),或者启动时加 --disallowedTools "Agent(Explore)" |
| 只去掉内置的 Explore 和 Plan | 环境变量 CLAUDE_CODE_DISABLE_EXPLORE_PLAN_AGENTS=1 |
-p 和 Agent SDK 里去掉全部内置子代理,只用自己的 |
环境变量 CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS=1 |
| 一个也不让派 | 在 permissions.deny 里拒绝 Agent 工具本身 |
另外,内置的 Explore 在主对话用 Fable、并且是通过 ANTHROPIC_BASE_URL 接网关时,用的是 opus 别名指向的模型。走中转站的读者,如果这把 key 没开 Opus,可以照文档在项目里定义一个同名的 Explore,写上自己能用的 model。这一条本站没有测。
七、怎么查一个子代理到底拿到了什么
文档给了会话记录的位置:~/.claude/projects/<项目>/<会话 ID>/subagents/agent-<ID>.jsonl。本站这次的结论都是从这些文件里读出来的。下面这几样是本站在 2.1.304 上观察到的格式,文档没有写,以后的版本可能会变:
- 同一目录下的
agent-<ID>.meta.json里有子代理的类型(agentType)、嵌套深度(spawnDepth)、前台还是后台(requestShape)。 .jsonl里type为api-request-shape的那一行,shape.tools是它拿到的工具清单,shape.system是它的系统提示。type为api-request的行,params里有实际请求的模型和 effort。
查“为什么这个子代理没有某个工具”“它到底用的哪个模型”,看这几行最直接。这些文件里有你的代码和对话内容,发给别人之前先看一遍。
走中转站时要注意的
- 子代理请求的模型这把 key 没开,子代理直接失败,主对话收到 404。用
CLAUDE_CODE_SUBAGENT_MODEL加CLAUDE_CODE_SUBAGENT_MODEL_FORCE(设成 1)统一换成能用的模型。 - 子代理换成和主对话不同的模型时,本站以前在这家中转站上碰到过主对话报 400 的问题,对方已经修好,经过见《子代理实战》。这次子代理用 Sonnet、主对话也用 Sonnet,没有碰到。
这次实测没有覆盖到的
- 交互式界面都没测:子代理面板、
/tasks、Ctrl+B、/subtask、后台子代理把确认框弹到主对话。 isolation: worktree、background、autoCompactWindow、initialPrompt、experimental.cacheTtl没有测,照文档写。文档特别提醒autoCompactWindow要写成200000这样的整数,写200k会被读成 200,超出范围后被忽略。--agent把整个会话变成某个子代理没有测,见《子代理实战》里的简介。- 拼错字段名的检查要用
claude --debug看日志,本站的测试环境不开调试日志(会带出请求的细节),没有测。 - 插件和托管设置里的子代理没有测。
- 每一项只跑了一两次。嵌套、并发、权限这类是硬规则,结果稳定;Claude 选前台还是后台这类是模型的判断,换个说法可能不一样。