Table of Contents
We have already solved this problem.
It took Unix 50 years.
AI agents are starting from the beginning -- and making the same mistakes.
Subagents are not a new abstraction. They are processes. And they inherit everything.
When an AI agent spawns a subagent, it executes a fork() equivalent: the child receives the parent's context, permissions, MCP connections, environment, and file state. The child runs, produces a result, returns. Simple on the surface. But underneath, the spawning model carries the same class of inheritance bugs that plagued early process isolation -- file descriptors leak, environment variables propagate, permissions inherit by default rather than by explicit grant.
We analyzed the actual implementation in Claude Code's src/tools/AgentTool/runAgent.ts -- roughly 900 lines that handle context cloning, permission mode application, skill preloading, MCP server attachment, system prompt construction, transcript recording, and cleanup. It is the fork() of the AI agent world.
What we found: an isolation model that is thoughtfully designed but structurally insufficient. The developers clearly understand the risks -- there are explicit mechanisms for stripping context, scoping tools, and filtering rules. But the model operates on an opt-out basis (strip what you know is dangerous) rather than opt-in (grant only what the child explicitly needs).
That architectural choice makes leakage the default behavior, not the exception.
1. The Agent Spawning Model
Agent Types
The codebase supports several execution modes, each with different isolation characteristics:
- Synchronous agents -- run inline, share the parent's abort controller, block until complete. Built-in
ExploreandPlanagents (ONE_SHOT_BUILTIN_AGENT_TYPES). - Asynchronous (background) agents -- independent
AbortController, restricted tool set viaASYNC_AGENT_ALLOWED_TOOLS, auto-deny for permission prompts. - Worktree-isolated agents -- operate in a temporary git worktree for filesystem separation.
- Remote agents -- separate environment (internal-only, gated behind
USER_TYPE === 'ant'). - Fork agents -- experimental (
FORK_SUBAGENT), inherit the parent's full conversation context for prompt cache sharing. - Teammate agents -- multi-agent swarm participants via
spawnTeammate().
What Happens at Spawn Time
The runAgent() generator (line 248) orchestrates spawning:
Context construction. Fork agents receive the parent's filtered conversation history. Non-fork agents start fresh. filterIncompleteToolCalls() strips orphaned tool use blocks for API compatibility -- the remaining messages pass through unfiltered.
Permission mode application. The child overlays its declared permission mode on the parent's state. But parent modes bypassPermissions, acceptEdits, and auto take precedence and cannot be overridden downward (line 421).
Tool resolution. resolveAgentTools() in agentToolUtils.ts applies layered filtering: ALL_AGENT_DISALLOWED_TOOLS removes session control tools, CUSTOM_AGENT_DISALLOWED_TOOLS adds restrictions for non-built-in agents, async agents get ASYNC_AGENT_ALLOWED_TOOLS only.
MCP server attachment. initializeAgentMcpServers() (line 95) merges parent and agent connections: [...parentClients, ...agentClients]. Additive-only.
Transcript recording. All messages recorded via recordSidechainTranscript() (line 735), creating persistent state keyed by agent ID.
This is not isolation. This is controlled inheritance.
2. What Gets Inherited
The default behavior is inheritance. Isolation is the exception.
PARENT AGENT
/ \
+-----------+ +-----------+
| Context | | Permissions|
| Messages | | Mode |
| File Cache| | Allow Rules|
+-----------+ +-----------+
| |
v v
+-------------------------------------------+
| SUBAGENT (fork/spawn) |
| |
| Inherited: |
| - Full message history (filtered only |
| for structural validity) |
| - Permission mode (parent overrides) |
| - Session allow rules (when allowedTools |
| is undefined) |
| - All parent MCP server connections |
| - File state cache (cloned) |
| - Process env vars |
| - Working directory |
| - Parent + agent hooks (both active) |
| |
| Restricted: |
| - Tool set (DISALLOWED_TOOLS filters) |
| - Recursive agent spawning (blocked) |
| - Session control tools (blocked) |
| |
| Isolated: |
| - Abort controller (async only) |
| - Transcript (separate sidechain) |
| - Agent ID |
| - Git worktree (optional) |
+-------------------------------------------+
Context and Messages
Fork children receive the parent's full conversation history -- every prompt, every response, every tool result including file contents, command outputs, and credentials disclosed in conversation. filterIncompleteToolCalls() (line 866) is a correctness filter, not a security filter. No content classification. No redaction. No trust-level filtering.
Permission State
The child's permission context derives from the parent's AppState.toolPermissionContext. When allowedTools is provided, session-level allow rules are replaced (lines 469-479). The code comment: "Only clear session-level rules from the parent to prevent unintended leakage."
But when allowedTools is undefined -- the common case for built-in agents -- the parent's full session allow rules flow to the child unmodified.
MCP Servers
[...parentClients, ...agentClients] -- additive-only. Every MCP server the parent connected to is available to the child. No filtering. No scoping.
File State Cache
Fork children receive cloneFileStateCache() of the parent's read state (line 376) -- the content of every file the parent has read, including files the child was never asked to examine.
3. Where Isolation Fails -- The Leakage Points
Leakage 1: Permission Inheritance
If the parent runs in bypassPermissions mode, the child cannot restrict itself. The check at line 421 explicitly prevents downward restriction. A subagent spawned with permissionMode: 'plan' silently operates in bypassPermissions if the parent has that mode active. The child's declared permission mode becomes a dead letter.
Session-level alwaysAllowRules propagate when allowedTools is not set. A user approves "always allow Bash: npm *" for the parent -- every synchronous subagent inherits that rule. The approval was contextual. The propagation is not.
Permission is not scoped to the agent. It is scoped to the lineage.
Leakage 2: Context Inheritance
The fork path shares the parent's full conversation history. Every fork child sees everything the parent has seen: user instructions with business logic, tool results with credentials, API responses with tokens, database results with PII.
There is no content classification. No redaction. No trust-level filtering. The child -- potentially a custom agent defined in user-authored frontmatter -- sees the same context as the parent.
Context is not filtered. It is copied.
Leakage 3: Transcript Persistence
Every subagent's conversation is recorded via recordSidechainTranscript(). Transcripts include parent context passed to the child. Sensitive information from the parent's session is serialized to disk in the subagent's transcript directory.
CacheSafeParams (in forkedAgent.ts, line 57) packages the system prompt, user context, system context, tool use context, and fork context into a single object for background summarization. This contains everything needed to reconstruct the parent's API request -- including sensitive content.
Sensitive context does not just leak at runtime. It becomes persistent state.
Leakage 4: MCP Server Sharing
initializeAgentMcpServers() implements additive-only inheritance. Parent connections pass through unfiltered. When strictPluginOnlyCustomization is active, frontmatter MCP servers from user-controlled agents are skipped -- but parent servers still flow through:
return {
clients: parentClients, // Parent's full MCP set, unfiltered
tools: [],
cleanup: async () => {},
}
If the parent connected to a database, Slack, or filesystem MCP server, the child has access to all of them.
MCP servers are not granted per-agent. They are inherited per-session.
Leakage 5: Background Agent Drift
Background agents receive an independent AbortController but their tool set (resolvedTools) is computed once at spawn time (line 500) and never refreshed:
const resolvedTools = useExactTools
? availableTools
: resolveAgentTools(agentDefinition, availableTools, isAsync).resolvedTools
If the user revokes a tool permission after the background agent starts, the tool set remains frozen at its initial configuration. The agent continues operating with tools that may no longer be authorized.
The finally block (line 817) handles cleanup: MCP disconnection, hook removal, cache cleanup, shell task termination. But this only runs when the query loop exits. A long-running background agent accumulates state throughout its lifetime.
The killShellTasksForAgent() call (line 847) kills background bash tasks spawned by the agent. Without it, shell processes outlive the agent "as a PPID=1 zombie." The code comment acknowledges a real leak that required explicit mitigation.
The agent does not degrade safely. It continues operating with outdated authority.
This Was Predictable
When you build a system that spawns execution contexts, shares state by default, and allows capability inheritance, you get leakage.
Not occasionally. Systematically.
This is not a new problem space. It is a solved problem space being reimplemented without its constraints.
4. The OS Process Analogy
Every problem in this table has already been solved. AI agents are not using those solutions.
| Unix Concept | AI Agent Equivalent | Isolation Quality |
|---|---|---|
fork() |
runAgent() |
Inherits context (file descriptors = messages) |
exec() |
Agent system prompt | New program, inherited environment |
| File descriptors | MCP server connections | Shared, not copied; additive-only |
| Environment variables | process.env, permission state |
Fully inherited, no scrubbing |
| PID namespace | Agent ID (createAgentId()) |
Identifier isolation only |
setuid/capabilities |
filterToolsForAgent() |
Subtractive tool restriction |
seccomp |
ALL_AGENT_DISALLOWED_TOOLS |
Static blocklist, no runtime policy |
chroot/mount namespace |
Worktree isolation (optional) | Filesystem only, same process |
| IPC | SendMessageTool |
Explicit inter-agent messaging |
| Signal handling | AbortController |
Independent for async, shared for sync |
O_CLOEXEC |
(none) | MCP connections always propagate |
PR_SET_NO_NEW_PRIVS |
(none) | Cannot voluntarily drop permissions |
Unix moved from opt-out isolation (close what you don't need) to opt-in (explicitly grant what the child requires) over the course of decades. AI agent frameworks are still in the opt-out phase.
5. Recommendations
These are not optimizations. They are required for isolation.
Principle of least privilege. Agent definitions must declare required resources -- tools, MCP servers, context scope -- as an explicit allowlist. The spawning logic must grant only what is declared. The allowedTools code path (lines 469-479) already implements this pattern. It must be the default.
Explicit context filtering. Replace pass-all-filter-invalid with content-aware filtering. Strip tool results from non-relevant paths. Redact credential patterns. Scope context to the task, not the conversation.
Permission scope binding. Bind approvals to the agent that received them. "Always allow Bash: npm test" for the parent must not propagate to children. Scope by agent ID.
Transcript isolation. Separate storage by trust domain. Subagent transcripts must not contain parent context exceeding the child's trust level.
Runtime permission refresh. Background agents must periodically re-evaluate tool sets and permissions against current parent state. The frozen resolvedTools must be validated before each tool use.
Conclusion
Every isolation failure we are seeing in AI agents has already happened before.
File descriptor leaks. Permission inheritance. Environment propagation.
We solved these problems in operating systems.
The difference now is speed. We are rebuilding execution systems on top of language models -- and skipping the decades of hard-earned constraints that made them safe.
Subagents are not leaking because of bugs. They are leaking because that is what the architecture does.
Series Navigation
Anatomy of a Production AI Agent -- Part 6 of 10
Scott Thornton is an AI security researcher at perfecXion.ai, specializing in defensive research on LLM and agent vulnerabilities. All analysis was conducted on lawfully obtained, publicly distributed npm package code in an authorized research environment.