Skip to content

fix(sandbox): Landlock bash sandbox never engaged — resolve bashMode lazily#61

Merged
Anton-Horn merged 1 commit into
mainfrom
fix/landlock-bash-mode-lazy-resolve
Jun 10, 2026
Merged

fix(sandbox): Landlock bash sandbox never engaged — resolve bashMode lazily#61
Anton-Horn merged 1 commit into
mainfrom
fix/landlock-bash-mode-lazy-resolve

Conversation

@Anton-Horn

Copy link
Copy Markdown
Contributor

The bug

The Landlock sandbox from #60 never actually engaged at runtime. On the deployed agent, bash ran fully unsandboxed — it could ls /, read sibling projects under the projects root, and list /root/. (Caught by a live sandbox-probe test against the deployed agent.)

Root cause

bashMode was resolved in a pi.on("session_start", …) handler. But session_start is only emitted from pi's bindExtensions(), which is called by the interactive/rpc/print modes. This server drives Pi via the bare SDK (createAgentSession + session.prompt) and never calls bindExtensions — so session_start never fires, the handler never runs, and bashMode stays at its initial "none" (unsandboxed).

What did work — the scoped read/write tools and the per-turn proxy-env injection — runs in the extension factory body, not in session_start. That's why proxy env worked in prod while containment silently didn't, and why it looked fine in local/interactive testing (where modes do call bindExtensions). The old SandboxManager path had the same gate, so bash was likely never sandboxed in the deployed server either.

Fix

  • Resolve bashMode lazily and memoized on first bash use, inside the bash tool's execute handler — which provably runs in prod (the proxy-env injection everyone relies on depends on it). No lifecycle event required.
  • session_start still calls ensureBashMode() so interactive/rpc modes resolve eagerly and keep the status line.
  • Log the resolved mode (info/warn). ctx.ui is a no-op in the headless SDK path, which is exactly why this was invisible in prod — now it's greppable (bash sandbox mode resolved … mode=landlock) and would have caught this regression.

Verification plan

After deploy: grep prod logs for bash sandbox mode resolved mode=landlock, and re-run the live probe (ls / should now be Permission denied).

🤖 Generated with Claude Code

…SDK path

The Landlock sandbox shipped in #60 never actually engaged at runtime. bash
ran fully unsandboxed in prod (confirmed: the deployed agent could `ls /`,
read sibling projects, and list /root/).

Root cause: bashMode was resolved in a `pi.on("session_start", …)` handler.
session_start is only emitted from pi's `bindExtensions()`, which is called
by the interactive/rpc/print *modes*. The server drives Pi via the bare SDK
(createAgentSession + session.prompt) and never calls bindExtensions, so
session_start never fires — the handler never ran and bashMode stayed at its
initial "none" (unsandboxed). The extension's registerTool calls (scoped
read/write, and the bash env injection) DO take effect because they run in
the factory body, which is why per-turn proxy env worked while containment
silently didn't. The old SandboxManager path had the same gate.

Fix: resolve bashMode lazily and memoized on first bash use, inside the bash
tool's execute handler (which provably runs in prod — proxy env injection
depends on it). session_start still calls ensureBashMode() so interactive/rpc
modes resolve eagerly and get a status line.

Also log the resolved mode (info/warn) — ctx.ui is a no-op in the headless
SDK path, so the sandbox state was invisible in prod; this makes it greppable
and would have caught the regression.
@Anton-Horn Anton-Horn merged commit e24d181 into main Jun 10, 2026
2 checks passed
@Anton-Horn Anton-Horn deleted the fix/landlock-bash-mode-lazy-resolve branch June 10, 2026 21:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant