Skip to content

Subagents

A subagent is a child agent spawned by a parent agent to handle a focused subtask. The parent delegates work, waits for (or ignores) the result, and continues. The key constraints are:

  1. Scoped tool surface — the subagent should only have access to the tools its task requires. An exploration subagent has no need to write files.
  2. Bounded iterations — a subagent should have a max_steps budget; it shouldn’t run indefinitely while the parent waits.
  3. Recursion prevention — subagents must not spawn further subagents without explicit permission, or you get infinite agent trees.
  4. Context isolation — the subagent starts with a fresh context window, seeded by the parent’s prompt. It does not inherit the parent’s full chat history.

The three reference implementations handle this very differently:

  • Aider has one mechanism: the architect pattern, which is sequential and synchronous (one coder delegates to another).
  • Codex has true async subagent spawning via AgentControl::spawn_agent(), but no tool filtering.
  • OpenCode has the most complete system: a task tool that creates a child Session with inherited permissions and a parent/child link in the database.

Aider has no general subagent mechanism. The closest equivalent is the architect pattern: ArchitectCoder completes its design phase, then spawns an editor coder to execute the changes.

File: aider/coders/architect_coder.py:6

class ArchitectCoder(AskCoder):
edit_format = "architect"
auto_accept_architect = False
def reply_completed(self):
content = self.partial_response_content
# If user hasn't pre-approved, ask now
if not self.auto_accept_architect:
if not self.io.confirm_ask("Edit the files?"):
return
# Spawn the editor coder as a "subagent"
editor_model = self.main_model.editor_model or self.main_model
editor_coder = Coder.create(
main_model=editor_model,
edit_format=self.main_model.editor_edit_format,
suggest_shell_commands=False,
map_tokens=0, # no repo map in editor phase
from_coder=self, # inherit files and history
)
editor_coder.run(with_message=content, preproc=False)
# Propagate cost back to parent
self.move_back_cur_messages("I made those edits to the files.")
self.total_cost = editor_coder.total_cost
  • Synchronous — the architect blocks while the editor runs. No parallelism.
  • Sequential, not hierarchical — the editor is not a “child” in any persistent sense. It runs once and the result is folded back into the architect’s history.
  • Context inheritance via from_coder — file lists, chat history, and cost totals are copied. The repo map is NOT inherited (map_tokens=0).
  • No recursion guard — Aider doesn’t prevent the editor from spawning another architect, though in practice no built-in editor format does this.
  • User approval gateauto_accept_architect defaults to False. The user sees the architect’s proposal and explicitly approves before the editor runs.
  • Parallel subagent execution
  • Named subagents with distinct permission sets
  • Output streaming from subagent to parent in real-time
  • Any mechanism for spawning more than one level of delegation

Codex has proper subagent spawning via AgentControl. A parent agent thread can create new child agent threads, each with their own Config and independent execution lifecycle.

File: codex-rs/core/src/agent/control.rs:40

pub async fn spawn_agent(
&self,
config: crate::config::Config, // full config for the child
items: Vec<UserInput>, // initial prompt
session_source: Option<SessionSource>,
) -> CodexResult<ThreadId>

Flow:

  1. Check Guards to ensure spawn count is within agent_max_threads limit (default: 6).
  2. If limit exceeded, return CodexErr::AgentLimitReached.
  3. Register new thread in ThreadManagerState.
  4. Spawn a tokio task running a full Codex event loop with the given config.
  5. Return ThreadId — the caller can subscribe to status updates via subscribe_status().

File: codex-rs/core/src/agent/status.rs

pub enum AgentStatus {
PendingInit,
Running,
Completed(String), // final message from agent
Errored(String), // error description
Shutdown,
NotFound,
}

State transitions:

TurnStarted → Running
TurnComplete → Completed(final_message)
Error → Errored(description)
TurnAborted → Errored("Interrupted")
ShutdownComplete → Shutdown

The parent subscribes to this channel and can poll or await completion:

let mut status_rx = control.subscribe_status(child_thread_id).await?;
// Wait until child completes
while let Some(status) = status_rx.recv().await {
match status {
AgentStatus::Completed(msg) => { /* use result */ break; }
AgentStatus::Errored(e) => { /* handle error */ break; }
_ => { /* still running */ }
}
}

File: codex-rs/core/src/agent/control.rs:2

pub(crate) struct AgentControl {
manager: Weak<ThreadManagerState>,
state: Arc<Guards>, // tracks active thread count
}

Guards enforces:

  • MAX_THREAD_SPAWN_DEPTH — prevents spawning beyond a nesting depth
  • agent_max_threads — total concurrent agent cap across the whole session

Test (control.rs:480):

#[tokio::test]
async fn spawn_agent_respects_max_threads_limit() {
let max_threads = 1usize;
let first = control.spawn_agent(config.clone(), input("hello"), None).await.unwrap();
let err = control.spawn_agent(config, input("hello"), None).await.unwrap_err();
assert!(matches!(err, CodexErr::AgentLimitReached { .. }));
}

File: codex-rs/core/src/agent/control.rs:71

pub async fn resume_agent_from_rollout(
&self,
config: crate::config::Config,
rollout_path: PathBuf, // path to JSONL session file
session_source: SessionSource,
) -> CodexResult<ThreadId>

This allows spawning a subagent that picks up from a previously-recorded session. The rollout file contains the full event log; the new agent replays it and continues from that state.

  • Tool filtering per subagent — child agents get the same tool set as the parent (controlled only by ApprovalPolicy).
  • Context isolation — there is no mechanism to give a subagent a pre-seeded context without creating a rollout file first.
  • Output routing — subagent events go to the same global event bus as the parent; the caller must filter by ThreadId.

OpenCode’s subagent system is the most complete. It’s built on the task tool, which creates a child Session in the database, runs an agent against it, and returns the output to the parent.

File: opencode/packages/opencode/src/tool/task.ts

Parameters:

const parameters = z.object({
description: z.string(), // 3-5 word label shown in UI
prompt: z.string(), // full task prompt for the subagent
subagent_type: z.string(), // agent name — must have mode: "subagent"
task_id: z.string().optional(), // resume an existing child session
command: z.string().optional(), // name of triggering slash command
})

File: opencode/packages/opencode/src/tool/task.ts:45

Step 1 — Permission check:

// Validate caller has permission to invoke this subagent type
const callerPermissions = ctx.session.permission;
if (!PermissionNext.allows(callerPermissions, "task", subagent_type)) {
throw new Error(`Permission denied: cannot spawn subagent '${subagent_type}'`);
}

Step 2 — Session creation or resume:

const session = task_id
? await Session.get(task_id) // resume existing
: await Session.create({
parentID: ctx.sessionID, // link to parent
title: `${description} (@${subagent_type})`,
permission: [
// Prevent subagents from spawning further subagents
// (unless the parent explicitly has task permission)
...(hasTaskPermission ? [] : [
{ permission: "task", pattern: "*", action: "deny" },
]),
// TodoWrite/TodoRead denied for subagents (no UI)
{ permission: "todowrite", pattern: "*", action: "deny" },
{ permission: "todoread", pattern: "*", action: "deny" },
],
});

Step 3 — Execute subagent:

const result = await SessionPrompt.prompt(
session.id,
prompt,
{ agentType: subagent_type }
);

Step 4 — Return output:

return {
task_id: session.id,
content: `<task_result>\n${result.output}\n</task_result>`,
}

File: opencode/packages/opencode/src/session/index.ts:30

export const Session.Info = z.object({
id: Identifier.schema("session"),
parentID: Identifier.schema("session").optional(), // null for root sessions
title: z.string(),
projectID: z.string(),
permission: PermissionNext.Ruleset.optional(), // per-session overrides
time: z.object({
created: z.number(),
updated: z.number(),
compacting: z.number().optional(),
archived: z.number().optional(),
}),
})

Child sessions are first-class entities in the database. They persist after the task completes, so:

  • The parent can re-read the child’s output later via task_id
  • Users can inspect child session history in the UI
  • The child can be resumed with a follow-up prompt

The child session’s permission rules are the union of:

  1. The subagent’s built-in permission rules (from AgentDef)
  2. The additional restrictions added by the task tool (TodoWrite deny, recursion deny)
  3. Any further restrictions from the parent session’s config

This means a parent running in a restricted session propagates those restrictions to its children. A child cannot exceed the parent’s permissions.

By default, subagents spawned by the task tool have the task permission denied. A subagent cannot spawn further subagents unless the parent’s permission config explicitly includes:

permission:
task: allow # explicitly permit spawning further subagents

The built-in general agent has task: allow. The built-in explore agent does not.

The task tool returns a structured string to the parent model:

task_id: ses_01abc123
<task_result>
[output from the subagent's final response]
</task_result>

The parent model can reference this output in subsequent reasoning. It can also resume the child by passing task_id in a follow-up task call with a new prompt.


Reference: Claude Code Subagents docs (fetched 2026-02-21)

Claude Code has the most configurable subagent system, with six built-in agents, custom agent definitions via markdown files, tool filtering, persistent memory, background execution, and git worktree isolation. Subagents are spawned via the Task tool — the same tool used in all other references — but with significantly more configuration surface.

AgentModelTool AccessPurpose
ExploreHaikuRead-only (Glob, Grep, Read, WebFetch, WebSearch)Fast codebase search and analysis
PlanInheritsRead-onlyResearch and architectural planning
general-purposeInheritsAll except Task, ExitPlanMode, Edit, Write, NotebookEditComplex multi-step tasks
BashInheritsTerminal onlyRunning commands in separate context
statusline-setupSonnetLimited (Read, Edit)Status line configuration
Claude Code GuideHaikuLimited (Glob, Grep, Read, WebFetch, WebSearch)Questions about Claude Code itself

Custom subagents are markdown files with YAML frontmatter (system prompt in the body). Stored at four scope levels with precedence:

LocationScopePriority
--agents CLI flagCurrent session1 (highest)
.claude/agents/Project2
~/.claude/agents/User3
Plugin agents/Plugin-scoped4 (lowest)
FieldTypePurpose
namestringUnique identifier (lowercase, hyphens, max 64 chars)
descriptionstringWhen Claude should delegate to this agent (critical for auto-delegation)
toolslistAllowlist of available tools. Task(agent_type) restricts which agents can be spawned
disallowedToolslistDenylist removed from inherited/specified tools
modelstringsonnet, opus, haiku, or inherit
permissionModestringdefault, acceptEdits, dontAsk, bypassPermissions, plan
maxTurnsintMaximum agentic turns before stop
skillslistSkills preloaded into context at launch (full content, not just metadata)
mcpServerslistMCP servers available to this subagent
hooksobjectLifecycle hooks scoped to subagent’s execution
memorystringPersistent memory scope: user, project, or local
backgroundbooltrue = always run as background task
isolationstringworktree = isolated git worktree copy
  1. No nesting: Subagents absolutely cannot spawn other subagents. This is an unconditional prohibition — no depth limit, no config to override it. Task(agent_type) in tools only applies when an agent runs as the main thread via claude --agent.

  2. Context isolation: Subagents get a fresh context with system prompt + specified skills + CLAUDE.md. They do not inherit parent conversation history. The parent must include all relevant context in the prompt field.

  3. Permission inheritance: Subagents inherit the parent’s permission context but can override mode via permissionMode. Exception: bypassPermissions from the parent takes absolute precedence and cannot be overridden by the child.

  4. Output returns to parent: Subagent results are summarized back to the main conversation. The Task tool returns a single message — not the full subagent transcript.

  5. Persistent memory: Agents with a memory field get a cross-session directory:

    • User scope: ~/.claude/agent-memory/<name>/
    • Project scope: .claude/agent-memory/<name>/
    • Local scope: .claude/agent-memory-local/<name>/ The first 200 lines of MEMORY.md in this directory are injected into the system prompt at startup. Agents can write to MEMORY.md to persist learnings across sessions.
  6. Background subagents: Agents with background: true run concurrently. Permissions are pre-approved upfront — anything not pre-approved is auto-denied. MCP tools are unavailable in background mode.

  7. Git worktree isolation: isolation: worktree creates a temporary git worktree, giving the subagent an isolated copy of the repository. The worktree is cleaned up if no changes are made; if changes exist, the worktree path and branch are returned.

  8. Transcript persistence: Subagent transcripts are stored as agent-{agentId}.jsonl. They survive main conversation compaction and are cleaned up after cleanupPeriodDays (default 30).

  9. Auto-compaction: Subagents auto-compact at ~95% context capacity (configurable via CLAUDE_AUTOCOMPACT_PCT_OVERRIDE).

Claude Code’s tool filtering is more granular than either Codex or OpenCode:

  • tools allowlist: Only the listed tools are available. Supports Task(agent_type) to restrict which agents can be spawned when the agent runs as main thread.
  • disallowedTools denylist: Listed tools are removed from whatever set the agent would otherwise have.
  • Combined: the effective tool set is tools - disallowedTools.

To disable a specific built-in subagent, add Task(subagent-name) to deny rules in settings or --disallowedTools "Task(Explore)".

Key Design Differences from Codex and OpenCode

Section titled “Key Design Differences from Codex and OpenCode”
FeatureCodexOpenCodeClaude Code
Spawn mechanismAgentControl::spawn_agent()task tool creating child SessionTask tool with typed agent selection
Tool filteringNone (same as parent)Permission-based denytools allowlist + disallowedTools denylist
NestingDepth-limited (MAX_THREAD_SPAWN_DEPTH)Permission-based denyAbsolute prohibition (no nesting)
Context isolationSeparate Config, no preset contextFresh Session with parentIDFresh context + system prompt + preloaded skills
Concurrency limitagent_max_threads (default 6)SequentialNo explicit limit (parallel supported)
Background modeN/AN/AYes, with pre-approved permissions
Persistent memoryVia rollout filesSQLite session persistenceDedicated memory directory with MEMORY.md
Model controlVia ConfigInherits from parentPer-agent model selection
Git isolationN/AN/Aisolation: worktree
Lifecycle hooksN/AN/AHooks in agent frontmatter

Sequential Delegation Is Not True Parallelism

Section titled “Sequential Delegation Is Not True Parallelism”

Aider’s architect→editor delegation is synchronous. The parent blocks completely while the editor runs. If the editor makes a mistake, the user must restart the whole turn. There is no mechanism to have the architect review the editor’s output and iterate.

The task tool in OpenCode returns the full subagent output to the parent. If the subagent writes 10,000 tokens of analysis, all of it goes into the parent’s context window. This can exhaust the parent’s context budget on the first tool call.

Fix: The OpenCode explore agent limits output via its permission config. For custom agents, always set steps and design the system prompt to produce concise output.

Codex’s MAX_THREAD_SPAWN_DEPTH is a compile-time constant. A prompt injection that convinces a subagent to call a tool that itself spawns agents can still trigger multi-level recursion if the depth tracking is not correctly threaded through the agent’s context.

OpenCode’s permission-based approach (task: deny for subagents) is more robust because it’s enforced at the tool dispatch layer, not the agent spawn layer.

Context Isolation Means Re-Explaining the Problem

Section titled “Context Isolation Means Re-Explaining the Problem”

A subagent starts with a fresh context. The parent must include all relevant background information in the prompt field. If the parent’s reasoning context is 20,000 tokens, the subagent doesn’t see any of it — only what’s in prompt. Forgetting this leads to subagents that ask questions the parent already knows the answer to.

In Codex, if a parent agent spawns 6 subagents (hitting agent_max_threads) and then waits for all of them, but each subagent tries to spawn a child to complete its work, those child spawns will fail with AgentLimitReached. The parent deadlocks waiting for children that can never complete.

Fix: Set agent_max_threads conservatively and reserve headroom for sub-level spawns. Alternatively, use a task queue pattern where children are queued rather than spawned immediately.

In OpenCode, each subagent creates a persisted Session in SQLite. If the parent is cancelled or crashes mid-task, child sessions are left in the database in an incomplete state. There is no garbage collection. Over time, the session DB fills with orphaned partial sessions that confuse users browsing session history.


#[derive(Debug, Deserialize, JsonSchema)]
pub struct TaskToolParams {
/// 3-5 word description for UI display
pub description: String,
/// Full task prompt for the subagent
pub prompt: String,
/// Registered agent name (must have AgentMode::Subagent or All)
pub subagent_type: String,
/// Resume an existing child session
pub task_id: Option<SessionId>,
}
pub async fn execute_task(
params: TaskToolParams,
parent_session: &Session,
registry: &AgentRegistry,
db: &Database,
) -> Result<TaskOutput> {
// 1. Validate subagent type exists and is callable
let agent_def = registry.get(&params.subagent_type)
.ok_or(TaskError::UnknownAgent)?;
if !agent_def.mode.allows_subagent() {
return Err(TaskError::NotASubagent);
}
// 2. Check parent session permits spawning this agent
parent_session.permissions.check("task", &params.subagent_type)?;
// 3. Create or resume child session
let child_session = match params.task_id {
Some(id) => db.get_session(id).await?,
None => db.create_session(SessionInit {
parent_id: Some(parent_session.id),
agent: params.subagent_type.clone(),
// Inherit parent restrictions + add recursion guard
permissions: build_child_permissions(parent_session, agent_def),
}).await?,
};
// 4. Run agent loop in current task (blocking), or spawn to background
let output = run_agent_loop(child_session.id, &params.prompt, agent_def).await?;
Ok(TaskOutput {
task_id: child_session.id,
content: output,
})
}
fn build_child_permissions(
parent: &Session,
agent_def: &AgentDef,
) -> PermissionRuleset {
let mut rules = agent_def.permissions.clone();
// Always deny TodoWrite/TodoRead for non-interactive subagents
rules.prepend(PermissionRule::deny("todowrite", "*"));
rules.prepend(PermissionRule::deny("todoread", "*"));
// Deny task spawning unless agent explicitly allows it
if !agent_def.permissions.allows("task", "*") {
rules.prepend(PermissionRule::deny("task", "*"));
}
// Intersect with parent's permission set (child cannot exceed parent)
PermissionRuleset::intersect(rules, parent.permissions.clone())
}

The task tool must truncate subagent output before returning it to the parent model. Without truncation, a verbose subagent exhausts the parent’s context budget.

const TASK_OUTPUT_MAX_TOKENS: usize = 8192;
fn truncate_task_output(output: &str, max_tokens: usize) -> String {
let token_count = count_tokens(output);
if token_count <= max_tokens {
return output.to_string();
}
let truncation_note = format!(
"\n\n[Output truncated: {} tokens total, showing first {}]",
token_count, max_tokens
);
let truncated = truncate_to_tokens(output, max_tokens - 50);
format!("{}{}", truncated, truncation_note)
}
  • openoxide-agentTaskToolParams, TaskOutput, execute_task()
  • openoxide-sessionSession, SessionInit, parent/child linking
  • openoxide-permissionsbuild_child_permissions(), PermissionRuleset::intersect()
  • tokio — async task execution for background subagents

Extend TaskToolParams and agent definitions with tool filtering:

pub struct AgentDef {
pub name: String,
pub description: String,
pub system_prompt: String,
pub mode: AgentMode,
pub permissions: PermissionRuleset,
pub model: Option<ModelSpec>,
/// Allowlist of available tools. Empty = inherit parent's tools.
pub tools: Vec<String>,
/// Denylist removed from inherited/specified tools.
pub disallowed_tools: Vec<String>,
/// Skills preloaded at startup (full content, not metadata-only).
pub skills: Vec<String>,
/// MCP servers available to this subagent.
pub mcp_servers: Vec<String>,
/// Lifecycle hooks scoped to this agent's execution.
pub hooks: Option<AgentHooks>,
/// Persistent memory scope.
pub memory: Option<MemoryScope>,
/// Always run as background task.
pub background: bool,
/// Git worktree isolation.
pub isolation: Option<IsolationMode>,
/// Maximum agentic turns before forced stop.
pub max_turns: Option<usize>,
}

The effective tool set is computed as: (tools OR parent_tools) - disallowed_tools. The Task(agent_type) syntax in tools restricts which agents can be spawned when this agent is the main thread.

pub enum MemoryScope {
/// ~/.openoxide/agent-memory/<name>/
User,
/// .openoxide/agent-memory/<name>/
Project,
/// .openoxide/agent-memory-local/<name>/ (gitignored)
Local,
}
pub struct AgentMemory {
pub scope: MemoryScope,
pub dir: PathBuf,
/// First N lines of MEMORY.md injected into system prompt.
pub bootstrap_lines: usize, // default: 200
}

Agents with persistent memory can write to MEMORY.md to persist learnings across sessions. This enables agents that improve over time for project-specific tasks.

pub struct BackgroundAgent {
/// Permissions are pre-approved upfront.
pub pre_approved_permissions: Vec<PermissionGrant>,
/// Anything not pre-approved is auto-denied (no user prompt).
pub auto_deny_unapproved: bool,
/// MCP tools are unavailable in background mode.
pub disable_mcp: bool,
}

Background subagents run concurrently with the main conversation. They are useful for long-running tasks (test suites, large refactors) that don’t need interactive input.

pub enum IsolationMode {
/// Create a temporary git worktree for the subagent.
/// Cleaned up automatically if no changes are made.
Worktree,
}

When isolation: worktree is set, the subagent works on an isolated copy of the repository. If it makes changes, the worktree path and branch are returned to the parent.

Update the recursion guard to match Claude Code’s absolute prohibition:

fn build_child_permissions(
parent: &Session,
agent_def: &AgentDef,
) -> PermissionRuleset {
let mut rules = agent_def.permissions.clone();
// ALWAYS deny task spawning for subagents — no exceptions.
// This is simpler and safer than Codex's depth-limited approach.
rules.prepend(PermissionRule::deny("task", "*"));
// Always deny TodoWrite/TodoRead for non-interactive subagents
rules.prepend(PermissionRule::deny("todowrite", "*"));
rules.prepend(PermissionRule::deny("todoread", "*"));
// Intersect with parent's permission set (child cannot exceed parent)
PermissionRuleset::intersect(rules, parent.permissions.clone())
}
  1. Subagent output is always truncated — cap at TASK_OUTPUT_MAX_TOKENS (configurable). Verbose subagents should not silently consume the parent’s context window.
  2. Child permissions are the intersection of parent and agent — a child can never exceed parent permissions. This is simpler and safer than OpenCode’s additive deny-list approach.
  3. Child sessions persist — store in SQLite with parent_id FK. Orphan cleanup runs at startup: sessions older than 7 days with no completed_at timestamp are archived.
  4. No nesting, ever — following Claude Code’s absolute prohibition. Subagents cannot spawn other subagents under any configuration. This eliminates the deadlock and depth-explosion risks from Codex’s depth-limited model.
  5. Tool filtering is dual-modetools allowlist + disallowed_tools denylist provides fine-grained control without requiring users to enumerate every tool (from Claude Code).
  6. Persistent memory — cross-session learning directory with MEMORY.md bootstrapping enables agents that improve over time (from Claude Code).
  7. Background mode — concurrent execution with pre-approved permissions for long-running tasks (from Claude Code).
  8. Git worktree isolation — safe parallel code modifications without affecting the working tree (from Claude Code).
  9. Lifecycle hooks in agent definition — hooks scoped to the agent’s execution, cleaned up when the agent finishes (from Claude Code).