fix(image): run bwrap from a copy in /usr/local/bin #65

Merged
claudiomaradonna merged 2 commits from fix/bwrap-nested-userns into master 2026-11-09 11:02:26 +01:00
Collaborator

Why

On a Linux host whose AppArmor ships Ubuntu's bwrap-userns-restrict
profile, Claude Code's sandbox cannot start in the claude client
container, even with sandbox.enableWeakerNestedSandbox, which the
Nested sandboxes section recommends: every sandboxed Bash command fails
with apply-seccomp: write /proc/self/setgroups ... Permission denied.
The host profile attaches by path to /usr/bin/bwrap inside the
container too, runs the jail as bwrap//&unpriv_bwrap with an empty
capability bounding set, and so the user namespace Claude Code nests
there cannot map its IDs. A copy of the same binary at another path keeps
the container's own confinement and nests one. Fixes #64.

Unmasking the container's /proc, as the issue proposed, only lets a
fresh /proc mount and leaves this failure in place, so it is not part
of this change.

What changed

The images now run bwrap from /usr/local/bin/bwrap, a copy the base
stage makes of Ubuntu's binary, which stays in place; PATH finds the
copy first, and Claude Code resolves bwrap through PATH. With the
documented setting, Claude Code's sandbox runs commands on such hosts.
The image smoke gate's bubblewrap probe now nests a user namespace in its
jail, and Nested sandboxes says where bwrap runs from, that a custom
image upgrading bubblewrap copies it again, and that
sandbox.bwrapPath, when set, names /usr/local/bin/bwrap.

How to verify

On a host with rootless Podman and bwrap-userns-restrict loaded:

  1. make test-e2e on this branch prints
    smoke-images: all three image targets passed. With this branch's
    tests/e2e/smoke_images.sh on master's Dockerfile, it stops at
    assert_bubblewrap after
    unshare: write failed /proc/self/uid_map: Operation not permitted.

  2. Build an image from the branch and launch it on an empty workspace
    with the sandbox on:

    make build-claude BASE_TAG=issue-64 CLAUDE_TAG=issue-64
    mkdir -p /tmp/sandbox-check/.claude
    echo '{"sandbox": {"enabled": true, "enableWeakerNestedSandbox": true}}' \
      > /tmp/sandbox-check/.claude/settings.json
    claude /tmp/sandbox-check --image localhost/claude-code:issue-64 --skip-custom-images
    

    Asked to run command -v bwrap && ls /, Claude Code runs it as one
    sandboxed command and prints /usr/local/bin/bwrap.

make lint && .agents/hooks/shellcheck.sh repo && make test passes on
the tip (349 tests); test_base_copies_bwrap_to_usr_local_bin and the
smoke unit test's nested-probe assertion fail on master. Both scenarios
passed on Ubuntu with Podman 5.7.0 and kernel 7.0.0-42-generic. Not
tried: hosts without that profile, and macOS.

Considerations

The copy is made when the base builds, so it lags a bubblewrap upgrade
done in a custom image until that image copies it again; the nightly
base rebuild keeps the published one current. Two documentation
suggestions were set aside: a note on images built before this change,
since Updates already says when a launch rebuilds one, and a
troubleshooting entry for the two errors, which lies outside this fix.

## Why On a Linux host whose AppArmor ships Ubuntu's `bwrap-userns-restrict` profile, Claude Code's sandbox cannot start in the `claude` client container, even with `sandbox.enableWeakerNestedSandbox`, which the Nested sandboxes section recommends: every sandboxed Bash command fails with `apply-seccomp: write /proc/self/setgroups ... Permission denied`. The host profile attaches by path to `/usr/bin/bwrap` inside the container too, runs the jail as `bwrap//&unpriv_bwrap` with an empty capability bounding set, and so the user namespace Claude Code nests there cannot map its IDs. A copy of the same binary at another path keeps the container's own confinement and nests one. Fixes #64. Unmasking the container's `/proc`, as the issue proposed, only lets a fresh `/proc` mount and leaves this failure in place, so it is not part of this change. ## What changed The images now run `bwrap` from `/usr/local/bin/bwrap`, a copy the base stage makes of Ubuntu's binary, which stays in place; `PATH` finds the copy first, and Claude Code resolves `bwrap` through `PATH`. With the documented setting, Claude Code's sandbox runs commands on such hosts. The image smoke gate's bubblewrap probe now nests a user namespace in its jail, and Nested sandboxes says where `bwrap` runs from, that a custom image upgrading `bubblewrap` copies it again, and that `sandbox.bwrapPath`, when set, names `/usr/local/bin/bwrap`. ## How to verify On a host with rootless Podman and `bwrap-userns-restrict` loaded: 1. `make test-e2e` on this branch prints `smoke-images: all three image targets passed`. With this branch's `tests/e2e/smoke_images.sh` on master's Dockerfile, it stops at `assert_bubblewrap` after `unshare: write failed /proc/self/uid_map: Operation not permitted`. 2. Build an image from the branch and launch it on an empty workspace with the sandbox on: ```bash make build-claude BASE_TAG=issue-64 CLAUDE_TAG=issue-64 mkdir -p /tmp/sandbox-check/.claude echo '{"sandbox": {"enabled": true, "enableWeakerNestedSandbox": true}}' \ > /tmp/sandbox-check/.claude/settings.json claude /tmp/sandbox-check --image localhost/claude-code:issue-64 --skip-custom-images ``` Asked to run `command -v bwrap && ls /`, Claude Code runs it as one sandboxed command and prints `/usr/local/bin/bwrap`. `make lint && .agents/hooks/shellcheck.sh repo && make test` passes on the tip (349 tests); `test_base_copies_bwrap_to_usr_local_bin` and the smoke unit test's nested-probe assertion fail on master. Both scenarios passed on Ubuntu with Podman 5.7.0 and kernel 7.0.0-42-generic. Not tried: hosts without that profile, and macOS. ## Considerations The copy is made when the base builds, so it lags a `bubblewrap` upgrade done in a custom image until that image copies it again; the nightly base rebuild keeps the published one current. Two documentation suggestions were set aside: a note on images built before this change, since Updates already says when a launch rebuilds one, and a troubleshooting entry for the two errors, which lies outside this fix.
A host AppArmor profile for /usr/bin/bwrap, such as Ubuntu's
bwrap-userns-restrict, also attaches to that path inside the client
containers and leaves the jail no capability to nest a user namespace,
which Claude Code's sandbox needs. The base stage now copies the binary
to /usr/local/bin, which PATH finds first and which keeps the
container's own confinement.

The smoke gate's bubblewrap probe now nests a user namespace inside its
jail, and the unit tests require the copy and the nested probe.
docs(configuration): say where the images run bwrap from
All checks were successful
publish-images / publish (push) Successful in 36m26s
73462cb08f
The Nested sandboxes section now says that the images run bwrap from a
copy in /usr/local/bin, which a host AppArmor profile for
/usr/bin/bwrap leaves free to nest the user namespace Claude Code's
sandbox needs, and that sandbox.bwrapPath, when set, names that copy.
claudiomaradonna deleted branch fix/bwrap-nested-userns 2026-11-09 11:02:33 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
claudiomaradonna/azkaban!65
No description provided.