AI Agent Security

Subagents Are Sandboxed Processes (And They Leak)

AI agent subprocesses inherit context, permissions, and MCP connections by default. We analyze the isolation model in production agent frameworks and identify five structural leakage points.

Part 6 of 10 | Previous: Guardrails | Next: Prompt Injection
AI Agent Security April 15, 2026 18 min read Scott Thornton

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:

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.