[BUG] Bash tool refused in nested user namespace since 2.1.220: apply-seccomp: write /proc/self/uid_map: Operation not permitted

Status Open
Reported on v2.1.220
Maintainer reply None cached
Activity 1 comment · opened Aug 25, 2026

<!--
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

  1. 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
``

  1. Make the sandbox mandatory in ~/.claude/settings.json:

``json
{ "sandbox": { "enabled": true, "failIfUnavailable": true, "allowUnsandboxedCommands": false } }
``

  1. Ask Claude to run any Bash command.
  2. Every command is refused with the error above, before it runs.
  3. 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-699 documents 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 ALL emptied the bounding set (measured: CapBnd: 0000000000000000, and CapEff is still 0 inside 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.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗