[BUG] 2.1.277: sandbox.excludedCommands no longer matches an executable whose basename is `read`

Status Open
Reported on v2.1.278
Maintainer reply None cached
Activity 0 comments · opened Sep 19, 2026

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.278)

What's Wrong?

Since 2.1.277, a sandbox.excludedCommands entry no longer matches an executable whose basename is read, even when the command is a single absolute-path invocation. The command runs inside the sandbox as if the entry were absent. A sibling executable named write, with an identical entry, is still excluded. Same settings file and prompt under each binary; only the Claude Code version changes.

We hit this because our project's CLI tools are executables named read and write (one pair per tool directory), excluded from the sandbox and carrying their own narrower permissions. After the update every read tool failed and every write tool kept working.

What Should Happen?

An excludedCommands entry for an absolute-path executable matches regardless of its basename, as it did in 2.1.276. A path containing a slash never resolves to the read shell builtin, so a guard for the builtin only needs to cover the bare word.

Error Messages/Logs

/bin/sh: /private/tmp/cc-read-repro/bin/read: Operation not permitted
(exit code 126)

Steps to Reproduce

Self-contained, no project needed. It uses the side-by-side binaries the native installer keeps under ~/.local/share/claude/versions/.

D=/private/tmp/cc-read-repro; rm -rf $D; mkdir -p $D/bin $D/work
for n in read write; do printf '#!/bin/sh\necho "ok from %s: $*"\n' $n > $D/bin/$n; chmod +x $D/bin/$n; done
cat > $D/settings.json <<EOF
{
  "permissions": { "allow": ["Bash($D/bin/read *)", "Bash($D/bin/write *)"] },
  "sandbox": {
    "enabled": true,
    "autoAllowBashIfSandboxed": true,
    "allowUnsandboxedCommands": false,
    "failIfUnavailable": true,
    "excludedCommands": ["$D/bin/read *", "$D/bin/write *"],
    "filesystem": { "denyRead": ["$D/bin"] }
  }
}
EOF
P="Run exactly these two shell commands, each in its own Bash tool call, then print each command's exit code and output verbatim. 1: $D/bin/read hello  2: $D/bin/write hello"
for v in 2.1.276 2.1.278; do echo "== $v"; (cd $D/work && ~/.local/share/claude/versions/$v -p --setting-sources '' --settings $D/settings.json --model haiku "$P" </dev/null); done

Result:

| Version | bin/read hello | bin/write hello |
|---|---|---|
| 2.1.276 | exit 0, ok from read: hello | exit 0, ok from write: hello |
| 2.1.277 | exit 126, Operation not permitted | exit 0, ok from write: hello |
| 2.1.278 | exit 126, Operation not permitted | exit 0, ok from write: hello |

The denyRead entry is what makes the difference visible: when the exclusion is skipped, the sandboxed shell cannot read the script.

Claude Model

Not sure / Multiple models (the repro uses --model haiku; the model only relays the Bash result)

Is this a regression?

Yes, this worked in a previous version

Last Working Version

2.1.276

Claude Code Version

2.1.278 (also 2.1.277)

Platform

Anthropic API

Operating System

macOS (Darwin 25.5.0, arm64, native installer)

Terminal/Shell

Non-interactive/CI environment (claude -p)

Additional Information

In case it saves someone time: the 2.1.277 and 2.1.278 bundles contain a one-element set, new Set(["read"]), that the exclusion coverage check consults, and 2.1.275 and 2.1.276 don't have it. It reads like a guard for the read builtin that compares the basename. If so, limiting it to commands without a slash would keep the guard and fix this case.

This is in the same area as #95455 (the 2.1.277 "every part must match" change also dropping single commands with pre-subcommand flags), though the trigger here is different.

Our workaround is holding 2.1.276 with DISABLE_AUTOUPDATER=1, so there's no urgency on our side beyond wanting to get back on current releases. Happy to test a fix or provide anything else that helps. Thanks for the work on the sandbox.

View original on GitHub ↗