Skip to content

claude-sdk agent with linux_bwrap dies at spawn on a runtime bwrap bind failure (not caught by #2749's prepare-time degrade) #3265

Description

@bobbyhyam

Summary

On Linux, a claude-sdk agent whose os_env carries sandbox.type: linux_bwrap can die at every session spawn — before the harness finishes connecting — when the spawn-time bwrap wrap of the Claude CLI supervisor hits a runtime bind failure. The prepare-time degrade added in #2749 does not cover this, because it only dry-runs the wrap-argv builder; it never executes bwrap, so a bind that fails only when bwrap actually assembles the namespace is not caught and the launcher dies.

Observed error (omnigent 0.6.0, built 2026-07-21; Ubuntu; bwrap 0.11.1):

[omnigent-sandbox] spawn-time wrap re-exec backend=linux_bwrap wrap_head=['bwrap', '--ro-bind-try', '/usr', ...]
bwrap: Can't create file at /tmp/claude-<uid>/<project>/<session>/tasks/<id>.output: No such file or directory

Every session for that agent fails identically at Claude SDK connect. Setting OMNIGENT_CLAUDE_SDK_NO_SANDBOX=1 (skip the CLI wrap; sys_os_* helpers stay independently sandboxed) is a clean workaround and confirms the CLI-supervisor wrap is the failing component.

Root cause (traced against main, still present)

  1. The CLI supervisor itself is wrapped, not only the sys_os_* helper. prepare_claude_cli_path() (omnigent/inner/claude_sdk_executor.py) resolves the agent's linux_bwrap sandbox and returns create_exec_launcher(real_cli_path, sandbox) with native tools enabled.

  2. The degrade path is prepare-time / dry-run only. Right before wrapping, it does:

    # "The real wrap happens later inside run_launcher, where an OSError
    #  (un-grantable interpreter layout, profile-size cap, cwd-scan
    #  overflow) kills the launcher and surfaces as an opaque connect timeout."
    get_backend(sandbox.backend_type).wrap_launcher_argv(
        [sys.executable, "-c", "pass"], sandbox, cwd, target=real_cli_path
    )

    wrap_launcher_argv only builds the bwrap argv (interpreter-visibility, profile size, cwd dotfile scan). It never runs bwrap, so it cannot observe a failure that occurs when bwrap applies a mount. The code comment above explicitly acknowledges the real wrap can still die later — that late death is exactly this bug, and it is not degraded.

  3. The failing bind is a nested path under a claude-internal writable root whose intermediate dirs don't exist yet. _claude_internal_write_roots() includes Path(tempfile.gettempdir()) / f"claude-{stable_user_id()}" (→ /tmp/claude-<uid>) and mkdir(parents=True, exist_ok=True) only the root. The Claude CLI creates the nested <project>/<session>/tasks/ chain at runtime. When run_launcher() re-execs under bwrap (_SPAWN_WRAP_BACKENDS), bwrap mounts /tmp as a fresh tmpfs and then applies the write-root / per-file binds on top; a bind whose destination parent (/tmp/claude-<uid>/<project>/<session>/tasks/) does not exist under that fresh tmpfs fails with Can't create file at ...: No such file or directory, and the launcher exits non-zero → session dead at spawn. (fix(sandbox): grant the private scratch tmpdir before the spawn-time wrap #2759 fixed a different late failure in this same two-pass region — the private scratch tmpdir on seatbelt — but not this one.)

Impact

Any claude-sdk agent with sandbox.type: linux_bwrap is unspawnable, with no automatic degrade — the failure surfaces as an opaque connect failure, not a clear "sandbox couldn't wrap the CLI" message. For us this took an autonomous agent fully offline until reverted.

Suggested fix (either or both)

  • Extend the fix(claude-sdk): degrade instead of crashing when the CLI wrap is infeasible #2749 degrade contract to runtime bind failures. When the spawn-time bwrap re-exec in run_launcher() exits non-zero (or in a fast pre-flight), fall back to the same path the prepare-time degrade uses — run the CLI unwrapped with native tools disabled, so file/shell stay confined to the independently-sandboxed sys_os_* helpers — instead of killing the seat.
  • Pre-create (or normalize) the claude-internal writable-root subtree the CLI needs before the wrap is built, so nested binds under /tmp/claude-<uid>/... have an existing destination parent inside the namespace.

Environment

Metadata

Metadata

Labels

BugSomething isn't workingP2-mediumPriority: bug with workaround, important feature requestcomp:harness-t1Highest-usage harnessescomp:harnessesComponent: SDK harnesses (Claude, Cursor, etc.)comp:runnerComponent: agent runner, execution enginecomp:sandboxSandbox isolation and egresstriagedIssue has been triaged by the botvalidated:reproducedRepro agent validated the issue can be reproduced

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions