You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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)
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.
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 bwrapapplies 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.
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
omnigent 0.6.0 (built 2026-07-21T03:57:15Z); behavior re-traced against current main.
Linux (Ubuntu), bwrap 0.11.1, single external runner, os_env.type: caller_process.
Summary
On Linux, a
claude-sdkagent whoseos_envcarriessandbox.type: linux_bwrapcan die at every session spawn — before the harness finishes connecting — when the spawn-timebwrapwrap 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 executesbwrap, so a bind that fails only whenbwrapactually assembles the namespace is not caught and the launcher dies.Observed error (omnigent 0.6.0, built 2026-07-21; Ubuntu;
bwrap0.11.1):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)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'slinux_bwrapsandbox and returnscreate_exec_launcher(real_cli_path, sandbox)with native tools enabled.The degrade path is prepare-time / dry-run only. Right before wrapping, it does:
wrap_launcher_argvonly builds thebwrapargv (interpreter-visibility, profile size, cwd dotfile scan). It never runsbwrap, so it cannot observe a failure that occurs whenbwrapapplies 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.The failing bind is a nested path under a claude-internal writable root whose intermediate dirs don't exist yet.
_claude_internal_write_roots()includesPath(tempfile.gettempdir()) / f"claude-{stable_user_id()}"(→/tmp/claude-<uid>) andmkdir(parents=True, exist_ok=True)only the root. The Claude CLI creates the nested<project>/<session>/tasks/chain at runtime. Whenrun_launcher()re-execs underbwrap(_SPAWN_WRAP_BACKENDS),bwrapmounts/tmpas 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 withCan'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-sdkagent withsandbox.type: linux_bwrapis 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)
bwrapre-exec inrun_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-sandboxedsys_os_*helpers — instead of killing the seat./tmp/claude-<uid>/...have an existing destination parent inside the namespace.Environment
main.bwrap0.11.1, single external runner,os_env.type: caller_process.