[BUG] Bash tool refused in nested user namespace since 2.1.220: apply-seccomp: write /proc/self/uid_map: Operation not permitted
<!--
Paste-ready issue body for anthropics/claude-code#89478. Each ### heading is a field of .github/ISSUE_TEMPLATE/bug_report.yml. Lines are intentionally unwrapped for GitHub. No placeholders left to fill.
Title: [BUG] Bash tool refused since 2.1.220 when running inside an unprivileged user namespace: apply-seccomp: write /proc/self/uid_map: Operation not permitted
-->
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] I am using the latest version of Claude Code (2.1.236)
What's Wrong?
When Claude Code runs inside an unprivileged user namespace (a rootless container, or a bwrap --unshare-user wrapper), the Bash sandbox fails during setup and every command is refused before it runs:
apply-seccomp: write /proc/self/uid_map: Operation not permitted
This started in 2.1.220 and is still present in 2.1.236. 2.1.212 works in the same environment. It is model-independent — the failure happens at sandbox setup, before any tool logic runs.
The cause is a change in the bwrap arguments used by the bundled sandbox (anthropic-experimental/sandbox-runtime). The sandbox helper apply-seccomp has two ways to get CAP_SYS_ADMIN for its PID+mount unshare: (a) the caller keeps the capability for it via bwrap --cap-add, or (b) it creates its own nested user namespace and writes that namespace's uid_map. 2.1.220 added --cap-drop ALL to the secure-mode bwrap invocation, which forces path (b) — and path (b) cannot work in this environment, because --cap-drop ALL empties the capability bounding set, so the nested user namespace grants no capabilities and the map writes fail.
Binary evidence across versions (string search on the shipped executables):
| version | contains --cap-drop | contains --cap-add | Bash in a nested userns |
|---|---|---|---|
| 2.1.212 | no | no | works |
| 2.1.231 | yes | no | fails |
| 2.1.236 | yes | no | fails |
No shipped version passes --cap-add, so path (a) is never enabled and path (b) is the only path available — which --cap-drop ALL simultaneously breaks.
What Should Happen?
Bash should run, as it does on 2.1.212. Secure mode should hand apply-seccomp the capability it needs (--cap-add CAP_SYS_ADMIN) so it takes path (a), instead of relying on a nested-user-namespace bootstrap that an already-nested environment cannot perform. Failing that, the failure should be reported with the actionable message the code already has for the sibling case ("caller must provide CAP_SYS_ADMIN") rather than a bare EPERM.
Error Messages/Logs
apply-seccomp: write /proc/self/uid_map: Operation not permitted
Emitted at sandbox setup, once per Bash invocation, before the command string is executed. With a mandatory sandbox (failIfUnavailable: true, allowUnsandboxedCommands: false) this becomes a hard refusal of every command.
Steps to Reproduce
- Launch Claude Code inside an unprivileged user namespace, e.g.:
``sh``
bwrap --unshare-user --unshare-pid --dev /dev --proc /proc --ro-bind /usr /usr --ro-bind /etc /etc --symlink usr/bin /bin --symlink usr/lib /lib --symlink usr/lib64 /lib64 --bind ~/.claude ~/.claude --bind "$PWD" "$PWD" --chdir "$PWD" -- claude
- Make the sandbox mandatory in
~/.claude/settings.json:
``json``
{ "sandbox": { "enabled": true, "failIfUnavailable": true, "allowUnsandboxedCommands": false } }
- Ask Claude to run any Bash command.
- Every command is refused with the error above, before it runs.
- Repeat on 2.1.212 → Bash works.
The underlying mechanism reproduces without Claude Code, in a plain shell. Inside the outer namespace from step 1:
BW="bwrap --ro-bind /usr /usr --ro-bind /etc /etc --symlink usr/bin /bin --symlink usr/lib /lib --symlink usr/lib64 /lib64 --dev /dev --unshare-pid --unshare-user"
# Secure mode (what 2.1.220+ does) — no caps at all, so apply-seccomp's path (a) is impossible:
$BW --proc /proc --cap-drop ALL -- sh -c 'grep CapBnd /proc/self/status; unshare --pid --mount --fork true'
# CapBnd: 0000000000000000
# unshare: unshare() failed: Operation not permitted <- falls back to the uid_map path, which then fails
# Weaker mode (and 2.1.212's shape) — caps retained, path (a) succeeds, uid_map is never written:
$BW --bind /proc /proc -- sh -c 'grep CapBnd /proc/self/status; unshare --pid --mount --fork true && echo OK'
# CapBnd: 000001ffffffffff
# OK
# Proposed fix — drop everything except the one capability apply-seccomp needs:
$BW --proc /proc --cap-drop ALL --cap-add CAP_SYS_ADMIN -- sh -c 'grep CapEff /proc/self/status; unshare --pid --mount --fork true && echo OK'
# CapEff: 0000000000200000 (CAP_SYS_ADMIN only)
# OK
Claude Model
Not applicable / model-independent — the failure is at sandbox setup, before the model runs any tool. Observed with both Sonnet and Opus.
Is this a regression?
Yes.
Last Working Version
2.1.212
Claude Code Version
2.1.236 (also reproduced on 2.1.220, 2.1.221, 2.1.231; linux-x64).
Platform
Anthropic API
Operating System
Other Linux — Pop!_OS 24.04 LTS, kernel 7.0.11-76070011-generic (x86_64), bubblewrap 0.11.0. Unprivileged user namespaces are unrestricted here (user.max_user_namespaces=2147483647, no AppArmor apparmor_restrict_unprivileged_userns gate), so this is not the Ubuntu 24.04 LSM restriction documented in the sandbox README.
Terminal/Shell
Other — launched via a wrapper script; terminal-independent, and reproduces headlessly.
Additional Information
Root cause is in the bundled sandbox, anthropic-experimental/sandbox-runtime; I have filed the mechanism-level report there and cross-linked this issue. Line references are pinned to commit e5fb1b9 (v0.0.74, current main).
src/sandbox/linux-sandbox-utils.ts:1955(secure mode) pushes--unshare-user --cap-drop ALL --proc /proc. The weaker branch at 1965 pushes--unshare-user --bind /proc /proc— no cap drop.vendor/seccomp-src/apply-seccomp.c:685-699documents the two paths, and explicitly anticipates this failure mode: path (b) "can itself fail on hosts where unprivileged user namespaces are gated by an LSM … the unshare succeeds but the new namespace grants no capabilities, so the setgroups write fails. In that case we abort: the caller must supply CAP_SYS_ADMIN."- Same symptom, different trigger: here the new namespace grants no capabilities because
--cap-drop ALLemptied the bounding set (measured:CapBnd: 0000000000000000, andCapEffis still0inside the freshly created user namespace), not because of an LSM. - Path (a) is line 709; the path (b) fallback is 734–746, and the message users see is the
die()at 743.
Suggested fix: pass --cap-add CAP_SYS_ADMIN alongside the existing --cap-drop ALL at linux-sandbox-utils.ts:1955, which is exactly the arrangement apply-seccomp was written for (its comments at 688-689 and 834-836 both describe a caller that uses --cap-add, but no shipped build passes it). Confinement of the workload is unchanged: apply-seccomp clears the ambient set at 837 and sets PR_SET_NO_NEW_PRIVS at 859 before execvp at 871, so the sandboxed command still starts with zero effective capabilities plus the seccomp filter.
Impact: any deployment running Claude Code inside an unprivileged user namespace — rootless containers, nested bwrap/firejail wrappers, some CI sandboxes — loses the Bash tool entirely on 2.1.220+ when the sandbox is mandatory.
Workaround (verified): set enableWeakerNestedSandbox in ~/.claude/settings.json. This selects the branch at line 1965, which omits --cap-drop ALL, so apply-seccomp keeps CAP_SYS_ADMIN, takes path (a), and never writes a nested uid_map:
{ "sandbox": { "enabled": true, "failIfUnavailable": true, "allowUnsandboxedCommands": false, "enableWeakerNestedSandbox": true } }
Confirmed working on 2.1.236 in the environment described above, with the sandbox still enabled and mandatory. Note the trade-off flagged by the comments at 1941-1944: this branch binds the host /proc instead of mounting a fresh one, so host /proc is visible inside the sandbox, and the capability bounding set is left intact. The --cap-add CAP_SYS_ADMIN fix above would be strictly narrower than this workaround. Other options are to pin 2.1.212, or to disable the inner sandbox and rely on outer isolation ("enabled": false, or failIfUnavailable: false + allowUnsandboxedCommands: true).
Cross-reference
Reported upstream at anthropic-experimental/sandbox-runtime#498.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗