Intent
The agent HOME volume is created once per task and reused by every turn of that task. bootstrapAgentHome (task-isolation.ts:1163-1171) returns early when the volume already exists:
if (await inspectOk(exec, opts.runtime, 'volume', volume)) {
return { volume, credentials: 'already-bootstrapped' }
}
Inside the cage, the claude command is rewritten to always carry --dangerously-skip-permissions (task-isolation.ts:1584-1586). That trade is deliberate and documented (:1558-1561): "the container IS the guarantee. Nothing here restricts settings or MCP: a hostile repo file can only reach the cage."
The reasoning holds for a single turn. It does not hold across turns, because the HOME volume is exactly the part of the cage that survives one. CAGE_HOME_DIR is where the agent CLI reads its own settings, hooks and MCP configuration, and the agent has full write access to it with permission prompts disabled.
The consequence is that "reaching the cage" is no longer bounded by the turn: a turn driven by untrusted input (a hostile diff, a forge issue body parsed by forge-issues.ts, fetched web content) can write agent configuration that a later turn of the same task executes before it has read anything. The worktree those later turns write to is then committed from the host by the runner, so the effect leaves the cage through the commit path rather than through the container boundary.
Scope note: the volume is per task (agentHomeVolume(taskId), :1163), so this is turn-to-turn within one task (agent turn, review turn, fix turn, resumed turn), not task-to-task.
Scope
In scope:
- The lifetime of the per-task HOME volume across turns.
- Agent-owned configuration inside
CAGE_HOME_DIR (settings, hooks, MCP servers).
Out of scope:
--dangerously-skip-permissions itself. The container is the right boundary for a single turn.
- Provider session persistence, which is the legitimate reason the volume outlives a turn.
- The
policy fallback mode, which has no cage at all and is tracked separately.
Approach
Separate what must persist between turns (provider session/credentials) from what must not (executable configuration). Either reset the agent-configuration paths in the volume at the start of every turn, or split HOME into a persisted credential volume and a per-turn ephemeral overlay.
ADDED Requirements
Requirement: Agent configuration MUST NOT survive a turn
The system SHALL ensure that agent-executable configuration written inside the cage during one turn is not in effect at the start of any later turn of the same task.
Scenario: a poisoned turn tries to arm the next one
- GIVEN a task whose turn 1 processes untrusted input
- AND that turn writes a hook or MCP server definition under
CAGE_HOME_DIR
- WHEN turn 2 of the same task starts in the same cage
- THEN the configuration written by turn 1 is absent
- AND turn 2 starts from the same agent configuration as turn 1 did
Scenario: provider session still survives
- GIVEN a task whose HOME volume was bootstrapped with credentials
- WHEN a later turn of that task starts
- THEN the provider session is still usable
- AND no re-bootstrap of credentials is required
Tasks
1. Establish the boundary
2. Enforce it
3. Prove it
Intent
The agent HOME volume is created once per task and reused by every turn of that task.
bootstrapAgentHome(task-isolation.ts:1163-1171) returns early when the volume already exists:Inside the cage, the claude command is rewritten to always carry
--dangerously-skip-permissions(task-isolation.ts:1584-1586). That trade is deliberate and documented (:1558-1561): "the container IS the guarantee. Nothing here restricts settings or MCP: a hostile repo file can only reach the cage."The reasoning holds for a single turn. It does not hold across turns, because the HOME volume is exactly the part of the cage that survives one.
CAGE_HOME_DIRis where the agent CLI reads its own settings, hooks and MCP configuration, and the agent has full write access to it with permission prompts disabled.The consequence is that "reaching the cage" is no longer bounded by the turn: a turn driven by untrusted input (a hostile diff, a forge issue body parsed by
forge-issues.ts, fetched web content) can write agent configuration that a later turn of the same task executes before it has read anything. The worktree those later turns write to is then committed from the host by the runner, so the effect leaves the cage through the commit path rather than through the container boundary.Scope note: the volume is per task (
agentHomeVolume(taskId),:1163), so this is turn-to-turn within one task (agent turn, review turn, fix turn, resumed turn), not task-to-task.Scope
In scope:
CAGE_HOME_DIR(settings, hooks, MCP servers).Out of scope:
--dangerously-skip-permissionsitself. The container is the right boundary for a single turn.policyfallback mode, which has no cage at all and is tracked separately.Approach
Separate what must persist between turns (provider session/credentials) from what must not (executable configuration). Either reset the agent-configuration paths in the volume at the start of every turn, or split HOME into a persisted credential volume and a per-turn ephemeral overlay.
ADDED Requirements
Requirement: Agent configuration MUST NOT survive a turn
The system SHALL ensure that agent-executable configuration written inside the cage during one turn is not in effect at the start of any later turn of the same task.
Scenario: a poisoned turn tries to arm the next one
CAGE_HOME_DIRScenario: provider session still survives
Tasks
1. Establish the boundary
CAGE_HOME_DIRthat the supported agents read as executable configuration (claude, opencode)2. Enforce it
bootstrapAgentHomeor at the container run seam3. Prove it