[BUG] autoAllowBashIfSandboxed bypassed for commands containing shell expansions and several other constructs

Open 💬 35 comments Opened Apr 5, 2026 by sgrankin

Summary

With autoAllowBashIfSandboxed: true and sandboxing enabled, commands containing certain shell constructs prompt for permission despite the setting, with reasons like Contains simple_expansion or Unhandled node type: string. Syntactically-simple commands (literal argv, pipes, &&, ;, ||, control flow, [[ ]], <<<) auto-approve as expected.

Repro

  1. ~/.claude/settings.json:

``json
{ "sandbox": { "enabled": true, "autoAllowBashIfSandboxed": true } }
``

  1. Start a Claude Code session.
  2. Ask Claude to run echo $USER.
  3. Observe: permission prompt, reason: Contains simple_expansion.
  4. Ask Claude to run echo "user is $USER".
  5. Observe: auto-approved.
  6. Ask Claude to run echo "$HOME".
  7. Observe: permission prompt, reason: Unhandled node type: string.

Observed behavior matrix (v2.1.92, macOS)

Auto-approved ✓:

  • ls /tmp, cat /etc/hosts, date; uptime | tail -1
  • if true; then echo y; fi
  • [[ -f /etc/hosts ]] && echo yes
  • cat <<< "hello"
  • echo "user is $USER", echo "$HOME/x", echo "h=$HOME" (string with literal content + expansion)

Prompts ✗:

| Command | Reason shown |
|---|---|
| echo $USER, ls $HOME | Contains simple_expansion |
| echo "$HOME", echo "$USER" | Unhandled node type: string |
| echo $(date) | Contains command_substitution |
| echo $'hello world' | Contains ansi_c_string |
| echo {a,b,c} | Contains brace_expression |

Expected

When autoAllowBashIfSandboxed: true and sandboxing is enabled, commands that would run inside the sandbox should auto-approve regardless of whether a static analyzer can prove them safe. The sandbox is the security boundary — that's the documented purpose of the setting. Failure to statically analyze a command shouldn't weaken the sandbox trust model.

Impact — this is severe for automated workflows

autoAllowBashIfSandboxed is the standard configuration for running Claude with meaningful autonomy while keeping a real security boundary. It's what lets an agent run an edit → test → fix loop unattended. Shell variables and command substitutions appear in practically every realistic test/build command (go test -coverprofile=\$TMPDIR/x.cov, python -m pytest --basetemp=\$(mktemp -d), cargo test --target-dir \"\$CARGO_TARGET_DIR\", etc.), so in practice this bug means:

  • Every edit+test loop now hits a prompt per iteration. Unattended runs stall.
  • The only remaining way to run unattended is --dangerously-skip-permissions, which disables the entire permission system — a massive security regression compared to \"sandbox contains everything.\"
  • The documented safe middle ground (sandbox + auto-allow) no longer exists in practice.

Users have to choose between constant prompts or dropping all security. That's a significant workflow regression.

Partial source-level workarounds:

  • Quote the variable AND ensure the string has literal content (\"\$TMPDIR/file\" works; \"\$TMPDIR\" alone prompts).
  • No workaround for \$(...), brace expansion, ANSI-C strings, heredoc bodies, etc.

Suggested fix

When sandboxing is enabled and autoAllowBashIfSandboxed: true, check sandbox auto-allow before returning \"ask\" for commands the static analyzer can't handle. The sandbox contains the blast radius regardless of what the parser couldn't prove.

Environment

  • Claude Code CLI v2.1.92 (macOS, Apple Silicon)
  • sandbox.enabled: true, autoAllowBashIfSandboxed: true

View original on GitHub ↗

35 Comments

sgrankin · 3 months ago

Forgot to mention in the original report — this worked as expected about a week ago. I noticed prompts appearing for commands that previously auto-approved. Flagging in case this helps narrow down which change introduced it.

github-actions[bot] · 3 months ago

Found 2 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/31373
  2. https://github.com/anthropics/claude-code/issues/31191

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

diogofernandesc · 3 months ago

+1 seeing similar behaviour with same settings flags, was working as of 2.1.90 afaik

AgainPsychoX · 3 months ago

So damn annoying. OpenCode way better, but Anthropic closing down...

tuomas2 · 3 months ago

Same issue in Linux btw.

judewang · 3 months ago

This is so frustrating. I use a sandbox not just for security, but specifically to bypass those Bash compound commands that constantly ask for permission.

Now that this bug has appeared, the entire point of using the sandbox is completely gone.

connorbrinton · 3 months ago

This is affecting me as well, and is quite annoying. One hacky way I've found to work around it is to add a PreToolUse hook that parses the tool call arguments to see if "dangerouslyDisableSandbox" is omitted/false or set to true. If it's omitted or set to false, the hook allows the command, which means no permission prompt in the sandbox. Otherwise the hook should signal that it doesn't have a particular opinion about the tool call, which should cause Claude Code to fall back to the standard permission system (permission prompts when appropriate outside of the sandbox).

My personal PreToolUse hook does a bunch of unrelated stuff related to gh CLI commands, so I won't post it here, but I bet that Claude Code could come up with a minimal hook (though it's frustrating that it's necessary in the first place).

The main downside of this approach is that you need to make sure you only use it when sandboxing is enabled. If you use it somewhere where sandboxing isn't enabled, "dangerouslyDisableSandbox" will not be present for Bash tool calls, and they'll all be (very dangerously!) auto-approved.

Of course, the ideal solution would be for Anthropic to fix this themselves. Short of that, this is the best hacky solution I've found for this issue

olejorgenb · 3 months ago

Workaround:

  • Disable auto-updates: "env": { "DISABLE_AUTOUPDATER": "1" } in settings.json
  • Downgrade to 2.1.87: claude install 2.1.87

NB: I think they changed the default effort level in recent versions, so you might want to increase effort to "high" after the downgrade.

(I don't get how bugs like this get through in a supposedly dog-fed product like Claude Code? Anthropic does not use the sandbox themself?)

flacoste · 3 months ago

Confirming this also reproduces on 2.1.89 from the claude-code (stable) Homebrew cask on macOS, not just 2.1.92+ / claude-code@latest. Same config (sandbox.enabled: true, autoAllowBashIfSandboxed: true), same repro: echo $USER prompts with Contains simple_expansion, while echo "user is $USER" auto-approves. So the regression landed earlier than the original report suggested, and the stable channel is affected too — downgrading to claude-code@latest pre-2.1.92 isn't enough; you'd need to go back further on the stable cask (2.1.87 was the prior stable release).

flacoste · 3 months ago

Further narrowed: 2.1.87 is clean (same config, same echo $USER repro, no prompt). So the regression landed in 2.1.88 or 2.1.89. The claude-code stable cask jumped 2.1.87 → 2.1.89 directly, so anyone on stable can't test 2.1.88 without a manual install; a user on claude-code@latest would need to verify which of the two introduced it.

cinardoruk · 3 months ago

+1 I want to use patterns like $PROJECT_ROOT but get asked to OK it every time e.g:

<img width="460" height="354" alt="Image" src="https://github.com/user-attachments/assets/ee1d7581-dddd-4b80-9c2e-53938a1356e4" />

co-dh · 3 months ago

tell claude to use xargs instead.

cinardoruk · 3 months ago

this also triggers it

<img width="837" height="349" alt="Image" src="https://github.com/user-attachments/assets/fb006515-3829-410f-923e-815f09cb8bd0" />

dnalor123 · 2 months ago

Still happening in 2.1.114. Prompts because of Contains simple_expansion even with autoAllowBashIfSandboxed = true

 Bash command

   for pid in 707652 714626 720668 722939 732463 752153 791594 4092702; do cwd=$(readlink
    /proc/$pid/cwd 2>/dev/null); printf '%s\t%s\n' "$pid" "${cwd:-<gone>}"; done
   Read cwd for each claude pid, sandboxed

 Contains simple_expansion

 Do you want to proceed?
 ❯ 1. Yes
   2. No
tuomas2 · 2 months ago

Please make Claude with sandbox usable again. Still stuck to 2.1.87 because of this and #43454.

nicad · 2 months ago

This is a big deal, I completely agree with the other comments. Claude code always comes up with "complex commands" so the current permission system results in constant approval requests. The sandbox isn't perfect (read all by default ??) but provided a reasonable state and clear blast radius.

Running with --dangerously-skip-permissions is the alternative but that requires an external sandbox or docker to control the blast radius.

  • docker means no MacOS specifics, claude code auth within containers is flacky, laptop napping interferes with it so tmux is needed to keep claude code session but making tmux work properly with claude code and docker and the parent terminal is very difficult. And also the is tons of additional complexity setting up docker itself for heavy development (packages, mounts for claude code, etc ...)
  • custom sandboxes add supply chain risks and the ones I tried weren't very promising, the built-in sandbox worked well enough (not anymore)

So please do one of (but really, ideally, all of):

  • change claude code to generate commands that are not "complex"
  • smarter handling of "complex command"
  • don't break sandbox + allow all bash commands, otherwise the sandbox has no more purpose (or provide clear information about the security issue this is potentially solving)
  • stable auth in containers (I guess we can then invest time fixing the issues around it)

The only options I can think of today:

  • find a better 3rd party sandbox and use --dangerously-skip-permissions within it
  • the tool hook mentioned above but from past experience adding hooks generate other problems (for example preventing --continue to work because it gets bogged down by tool use so the work around is to edit config, continue, restore)

I'm also dying to know how Anthropic's own employees go around this ? The build system is such that it's only a few commands and nobody there sees this problem ? What are we all missing in this thread ?

Artforge · 2 months ago

Additional impact: plugin/skill ! backtick patterns are fully blocked

This parser bug isn't just cosmetic for plugin ! backtick expressions — it's a hard blocker that prevents skills from loading.

The compound-engineering plugin uses ! backtick patterns in skill files to pre-resolve values at load time. For example, in skills/ce-compound/SKILL.md:

**Repo name (pre-resolved):** !`common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) && [ -n "$common" ] && basename "$(dirname "$common")"`

When this pattern hits the permission checker, it fails with:

Error: Shell command permission check failed for pattern "!`common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) && [ -n "$common" ] && basename "$(dirname "$common")"`": Unhandled node type: string

The skill cannot execute at all — the error fires before any permission matching happens, even with Bash(git rev-parse:*) in the allowlist.

Affected skills: ce-compound, ce-sessions, and any skill using worktree-safe repo name resolution via ! backtick patterns.

Environment: Claude Code on macOS (Darwin 24.6.0), compound-engineering plugin v3.2.0

Amund211 · 2 months ago

+1 I use the sandbox to be able to not constantly babysit permissions.

When Claude tries to run find ... -exec ..., it similarly gets blocked and requests permission to run with the message find with '-exec' executes commands or modifies files — cannot be auto-allowed by a Bash(find:*) prefix rule. If the command had just run inside the sandbox it would have succeeded, as it was not modifying any files outside the current dir (and usually it is find ... -exec grep ... anyway)

When claude writes temp files I find that it will often reach for $TMP, which also triggers this. For this specifically I have resorted to telling it to write temp files to a literal ./tmp directory in the current dir.

I have also experienced all the other cases mentioned so far.

howie · 2 months ago

The Contains expansion hook also fires on "${VAR}" -- the bash form that uses braces to
explicitly delimit the variable name boundary and double quotes to prevent word-splitting. This
form is often cited as the safest variable expansion style, yet it triggers the prompt while the
less-explicit "$VAR" (no braces) does not.

Reproduction:

# Triggers "Contains expansion":
test -n "${MY_VAR}" && echo "set" || true

# Does not trigger:
test -n "$MY_VAR" && echo "set" || true

Expected: both forms are already quoted; neither would cause word-splitting or glob expansion.
The hook should treat them identically.

Observed: "${VAR}" triggers Contains expansion; "$VAR" passes without a prompt.

The practical effect is that users who write the more defensive form get more confirmation prompts
than users who omit the braces. Not certain whether this fits within #43713's scope or warrants
a standalone report -- mentioning it here for triage.

olejorgenb · 2 months ago

This bug is now a month old... It basically make the sandbox useless without using hook hacks. How is this possible? I can not see how the fix is even hard to fix. Just accept any bash action inside the sandbox... (yes, that means I accept the risk of the agent trying to break out, but that is just the tradeoff of using this kind of sandbox)

I'm getting extremely tired of bugs like this being long-lived and unacknowledged...

olejorgenb · 2 months ago
olejorgenb · 2 months ago

Not fully fixed :/

 Bash command

   TASK_DIR=task/162-compass-browser-hang BIGGEST=$(du -b "task/162-compass-browser-hang"/workbench/*.txt 2>/dev/null
   | sort -n | tail -1 | cut -f2) && echo "biggest dump: $BIGGEST ($(wc -l < "$BIGGEST") lines, $(stat -c %s
   "$BIGGEST") bytes)"
   Find biggest dump file via env vars and inline $()

 Contains shell syntax (variable_assignments) that cannot be statically analyzed

 Do you want to proceed?
 Bash command

   HELLO=1 echo wtf
   Run echo with inline env var

 This command requires approval
olejorgenb · 2 months ago

v2.1.139:

find with '-exec' executes commands or modifies files — cannot be auto-allowed by a Bash(find:*) prefix rule
mdallongeville · 2 months ago

I can confirm that 2.1.139 fixed many of the permission issues I had, it's really nice!

There are still a few patterns that are still triggering a prompt though, here's a recap of the most common ones

| Pattern | Example | Native message |
|---|---|---|
| for-loop var as standalone | for sha in a b c; do git log -1 $sha; done | Contains simple_expansion (bare $sha) / Contains shell syntax (string) that cannot be statically analyzed ("$sha") / Contains expansion (${sha}) / Contains for_statement (when the for-loop is wrapped in a pipe, e.g. … \| head) |
| X=$(cmd) then "$X" later | BODY=$(gh issue view 1); gh issue edit 1 --body "$BODY" | Contains shell syntax (string) that cannot be statically analyzed |
| inline VAR=val cmd | GIT_SEQUENCE_EDITOR="sed -i '1s/pick/edit/'" git rebase -i HEAD~3 | This command requires approval |
| while read loop var as standalone | gh pr view … --json commits \| while read oid; do git show $oid; done | (same as for-loop) |
| find -exec | find /p -name "*.vue" -exec grep X {} \; | find with '-exec' executes commands or modifies files — cannot be auto-allowed by a Bash(find:*) prefix rule |
| IFS= | find … \| while IFS= read -r f; do …; done | IFS assignment changes word-splitting — cannot model statically |
| while … <<< | while read x; do echo "$x"; done <<< "hello" | Contains while_statement |
| cd + output redirection | cd /repo && yarn --version > /tmp/out.txt | Compound command contains cd with output redirection - manual approval required to prevent path resolution bypass |
| cd + git | cd packages/backend && git checkout dev | This command changes directory before running git, which can execute untrusted hooks from the target directory. |

olejorgenb · 2 months ago

What is the rationale for not blanket allowing all commands with auto-allow? I don't understand why auto-allow in sandbox is not a simple flag.

Auto edit (or a command which allow to write to file) + auto-allow give the model the option to write whatever to a file and run that file, no?

Where is the rationale for this complexity?

I agree that having the agent respect deny patterns can be good to control the workflow, but I just can't see the reason for the current state at all.

dnalor123 · 2 months ago

Per the docs, sandboxed commands run without prompting -- only two named exceptions: deny rules, and rm/rmdir on critical paths. In practice there's a third exception: commands containing certain shell constructs. Is this a bug, or an undocumented feature?

dnalor123 · 2 months ago

still biting in 2.1.140

RyanMentley · 2 months ago

I've filed #58214 for the specific issues around FOO=bar and similar patterns within arguments.

lyphyser · 1 month ago

Please fix this ASAP, it completely breaks sandboxed autonomous coding, and it's a trivial fix.

When setting autoAllowBashIfSandboxed: "true the harness must NEVER stop to ask permission for shell calls that don't want to disable the sandbox.

That's the whole point of having a sandbox...

Ten0 · 1 month ago

I found a workaround for latest versions:
which looks like in nix + homemanager:

      # Fix claude-code#43713: with the sandbox on, auto-allow Bash so the static
      # command analyzer doesn't prompt for things it can't parse ($vars, $(...),
      # loops). A PreToolUse "allow" does NOT disable the sandbox (the command
      # still runs inside it), and deny/ask rules are evaluated first, so the
      # secret denies keep applying. Calls that ask to LEAVE the sandbox
      # (dangerouslyDisableSandbox / sandbox=false) are NOT auto-allowed -- the
      # hook defers (exit 0, no decision) so the normal escape prompt still
      # fires, preserving the escape hatch. Verified: the escape flag arrives as
      # tool_input.dangerouslyDisableSandbox, so a normal call (no such key) is
      # auto-allowed and an escape call is deferred to the usual prompt.
      hooks.PreToolUse =
        let
          gate = pkgs.writeShellScript "claude-pretooluse-gate" ''
            input=$(cat)
            esc=$(printf '%s' "$input" | ${pkgs.jq}/bin/jq -r 'if (.tool_input.dangerouslyDisableSandbox == true) or (.tool_input.sandbox == false) then "yes" else "no" end')
            [ "$esc" = "yes" ] && exit 0 # escape call: defer -> normal prompt fires
            printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"sandboxed; auto-allowed (claude-code#43713)"}}'
          '';
        in
        [
          {
            matcher = "Bash";
            hooks = [
              {
                type = "command";
                command = "${gate}";
              }
            ];
          }
        ];

Which you can also apply in non-nix configs:

settings.json:

{
  "hooks": {
    "PreToolUse": [
      {
        "hooks": [
          {
            "command": "/nix/store/qdb1j3zq96f7w9j5480vqfdnppagq3rl-claude-pretooluse-gate",
            "type": "command"
          }
        ],
        "matcher": "Bash"
      }
    ]
  }
}

/nix/store/qdb1j3zq96f7w9j5480vqfdnppagq3rl-claude-pretooluse-gate:

input=$(cat)
printf '%s\n' "$input" >> /tmp/claude-pretooluse-debug.jsonl
esc=$(printf '%s' "$input" | /nix/store/s04d6wm3kqjdgr7zdhlk214f6a2291ks-jq-1.8.1-bin/bin/jq -r 'if (.tool_input.dangerouslyDisableSandbox == true) or (.tool_input.sandbox == false) then "yes" else "no" end')
[ "$esc" = "yes" ] && exit 0 # defer -> normal escape prompt fires
printf '%s' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"sandboxed; auto-allowed (claude-code#43713)"}}'
orome · 28 days ago

Claude is a mess in this regard. The decision fatigue makes it useless for me. It seems to spend most of its effort coming up with elaborate one-liners or shell scripts that require approval (the Claude-assisted config files I've set up).

matangover · 24 days ago

What's the point of using the sandbox if this isn't fixed? It's so unusable currently, it prompts me all the time for permissions

AgainPsychoX · 22 days ago

I wish I could go back safely to OpenCode without risking my account banned. Claude Code is just bad, they don't address issues, unless you know someone to hit via private channels, or when it blows up on social media. Pointless to report issues or care at this point.

tuomas2 · 21 days ago

Motivated by this super frustrating issue, I ended up making my own solution to this - very generic and satisfying and easy to use cli sandboxing app for Linux users using Linux Containers (LXC's; Incus in particular). In my opinion, it is much better than their /sandbox could be even in theory. I will release it open source as soon as I can (and as soon as I get permission from my employer) and will post a link here. As it's holiday season things could be still delayed perhaps month or two...

orome · 16 days ago

The solution that has worked for me is to add a hook that matches the most common and annoying culprits directs Claude to pay attention to the relevant instructions I've given in CLAUDE.md and skills.

So far it has worked like a charm, and with the added bonus that all the opaque (and blocked, and impenetrable, and ephemeral) shell commands have been replaced with saved Python scripts (in a hidden scratch directory) that I can examine and even potentially reuse.