[BUG] Shell script occasionally fails due to crazy escaping

Open 💬 19 comments Opened Sep 10, 2025 by fsc-eriker

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

I have not found a way to reliably reproduce this, but from time to time, Claude Code submits wildly incorrect code to the shell even though the commands are printed with completely the right syntax.

Here is a captured example:

⏺ Bash(for f in $(find . -type f -exec tail -c 1 {} \; -print | grep -v '^$');
       do awk 1 "$f" > tmp && mv tmp "$f"; done)
  ⎿  Error: /opt/homebrew/bin/bash: eval: line 1: syntax error near unexpected 
     token `('
     /opt/homebrew/bin/bash: eval: line 1: `for f in \$ ( find . -type f -exec 
     tail -c 1 \{\} \; -print < /dev/null | grep -v \^\$ ) ; do awk 1 '' > tmp 
     && mv tmp '' ; done'

(I had separately tried to tell Claude to not use find in command substitutions but that's tangential here.)

You'll notice that not only has Claude inserted a spurious </dev/null (which I think is already reported multiple times, though I could quickly find only https://github.com/anthropics/claude-code/issues/4315) but also, the dollar sign in the command substitution has been escaped and weirdly followed by a space.

What Should Happen?

Command lines should not be weirdly escaped when sent to the shell. It would probably be better if Claude Code showed _exactly_ what it's going to try to run so that users who understand this language can intervene and correct (including then any spurious additions of </dev/null etc).

Error Messages/Logs

Error: /opt/homebrew/bin/bash: eval: line 1: syntax error near unexpected token `('

Steps to Reproduce

Like I wrote above, I have not been able to reliably reproduce this. It seems to happen in spurts and then go away again. Perhaps there is something in Claude's internal state which triggers this behavior?

Performing a task where you need Claude Code to do things in the shell seems to exhibit this sooner or later.

Claude Model

Sonnet (default)

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

1.0.110 (Claude Code)

Platform

AWS Bedrock

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

This is probably just another aspect of a more fundamental problem around how Claude Code runs commands. I have found related bugs such as

View original on GitHub ↗

19 Comments

github-actions[bot] · 10 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/3839
  2. https://github.com/anthropics/claude-code/issues/1132
  3. https://github.com/anthropics/claude-code/issues/4315

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

Buggy-McBugFace · 8 months ago

I frequently encounter bash tool errors like this across my two main environments (macOS & RHEL). When I asked Claude to search my conversation history for examples to add to this issue, it ironically hit this exact error:

⏺  Bash(file="$HOME/.claude/projects/-Users-x-phd/<redacted>.jsonl"; cutoff_date="2025-10-17T00:00:00Z"; timestamp=$(sed -n '404p' "$file" | jq -r
      '.timestamp // empty'); echo "Timestamp: $timestamp"; echo "Cutoff: $cutoff_date"; if [[ "$timestamp" > "$cutoff_date" ]]; then echo "PASSES"; else echo "FAILS"; fi)
  ⎿  Error: Exit code 1
     (eval):1: parse error near `('

∴ Thinking…

  Another bash tool error! LOL this is so ironic. Let me write it to a script.

⏺  LOL another bash tool error! Let me write a test script:

This example error (1 type of many) contains $(...) (command substitution) and [[ ]] (test construct), and has occurred many times for me, so perhaps it is one way to reproduce the error. Issue can be annoyingly worked around by instructing claude to not use the bash tool and instead write code to a script & run from disk; however, since this issue is so frequent for me, this issue slows me down significantly.

Environment 1 (macOS): Darwin 24.6.0, Terminal.app, Node v24.10, Claude Code v2.0.34, Model: claude-sonnet-4-5-20250929

Environment 2 (RHEL): RHEL 8.4 (Ootpa), Kernel 4.18.0-305.el8.x86_64, SSH, GNU bash 4.4.19(1)-release (x86_64-redhat-linux-gnu), Node v22.17, Claude Code v2.0.34, Model: claude-sonnet-4-5-20250929

EDIT: To clarify, when I run the command it says it is trying to run, it works perfectly. It is also identical in the script file it writes to disk, which also works. I am not sure why it errors so often when the code appears to be fine.

skawaji · 8 months ago

Additional Reproduction Case: Variable Expansion Fails in For Loop

I encountered a similar issue where variable expansion completely fails in a for loop when combined with certain commands.

Command Attempted

for pr_num in 1736 1729; do echo "=== PR #$pr_num ==="; gh pr view $pr_num --json body --jq '.body' 2>&1 | head -1; done

Expected Behavior

=== PR #1736 ===
(output from gh pr view 1736)
=== PR #1729 ===
(output from gh pr view 1729)

Actual Behavior

=== PR # ===
no pull requests found for branch "main"
=== PR # ===
no pull requests found for branch "main"

The variable $pr_num is not expanded in the echo command.

Trace Output (set -x)

When enabling command tracing with set -x, the actual executed commands reveal the issue:

+(eval):1> pr_num=1736
+(eval):1> echo '=== PR # ==='
+(eval):1> gh pr view ''
+(eval):1> head -1
+(eval):1> pr_num=1729
+(eval):1> echo '=== PR # ==='
+(eval):1> gh pr view ''
+(eval):1> head -1

Key observations:

  1. Loop variable assignment works correctly: pr_num=1736
  2. Double quotes are converted to single quotes: echo '=== PR # ===' instead of echo "=== PR #$pr_num ==="
  3. Variable passed to gh pr view becomes empty string: gh pr view '' instead of gh pr view 1736

Interesting Finding

The same for loop works correctly when simplified:

for pr_num in 1736 1729; do echo "PR #$pr_num"; done

Output:

PR #1736
PR #1729

The issue only appears when combining the for loop with certain commands like gh, jq, or complex pipelines.

Environment

  • OS: Linux (WSL2)
  • Shell: zsh (my default shell; Claude Code uses the user's default shell)
  • Claude Code version: (latest as of 2025-11-14)

Note: This issue appears to affect multiple shells. The original report was from bash environments (macOS and RHEL), and I've reproduced the same behavior in my zsh environment. This suggests the problem lies in how the Bash tool processes command strings before passing them to the shell, rather than in the shell itself.

This trace output might help identify where the escaping/quoting transformation is occurring in the Bash tool implementation.

skawaji · 7 months ago

This issue should NOT be auto-closed as a duplicate.

The duplicate candidates (#3839, #1132, #4315) all describe the < /dev/null injection problem. However, this issue (#7387) is about a different problem: variable expansion failure and quote transformation.

Evidence from reproduction:

The issue is still reproducible as of Claude Code v2.0.55:

for pr_num in 123 456; do echo "=== PR #$pr_num ==="; echo "test" | head -1; done

Expected:

=== PR #123 ===
test
=== PR #456 ===
test

Actual:

=== PR # ===
test
=== PR # ===
test

The variable $pr_num is not expanded. This occurs when combining a for loop with a pipeline. The same loop without a pipeline works correctly.

This is a distinct bug from the < /dev/null injection issue and should be tracked separately.

github-actions[bot] · 6 months ago

This issue has been inactive for 30 days. If the issue is still occurring, please comment to let us know. Otherwise, this issue will be automatically closed in 30 days for housekeeping purposes.

fsc-eriker · 6 months ago

This is indeed still very much an issue. It is rather annoying that you continually require assurances of this. It should be easy to see that there are many bug reports about this and related issues, and you should have plenty of ways to reproduce this yourselves.

rgaufman · 6 months ago

Yes, any solution/workaround for this? - very frequently I get things like this:

⏺ Bash(for f in /Users/hackeron/Development/myapp/spec/system/**/*.rb; do count=$(grep -c "have_\(no_\)\?content\s*(\s*['\"]" "$f" 2>/dev/null || echo 0); if…)
  ⎿  Error: Exit code 1
     /bin/bash: eval: line 0: syntax error near unexpected token `grep'
     /bin/bash: eval: line 0: `for f in /Users/hackeron/Development/myapp/spec/system/**/*.rb ; do count\=\$ ( grep -c "have_\\(no_\\)\\?content\\s*(\\s*['\"]" '' 2>/dev/null || echo 0 ) ; if \[ '' -gt 0 \] ; then echo ': ' ; fi ; done < /dev/null | sort -rn | head -15'

I tried to add this prompt but CLAUDE just ignores it:

**NEVER run complex bash one-liners** - they break with syntax errors:
```bash
# ❌ FORBIDDEN - Complex bash that breaks
for f in **/*.rb; do grep "pattern" "$f"; done
find . | xargs grep "x"
count=$(grep -c "x" file)

# ✅ REQUIRED - Use dedicated tools
Grep tool, Glob tool, Read tool
skawaji · 6 months ago

Verification Report

I verified that the following issues have been resolved in Claude Code v2.1.7.

1. Original issue (@Buggy-McBugFace)

The parse error near '(' error when using command substitution $(...) and [[ ]].

Test command:

data='{"name":"test","version":"1.0.0"}'; cutoff="0.5.0"; version=$(echo "$data" | jq -r '.version // empty'); echo "Version: $version"; echo "Cutoff: $cutoff"; if [[ "$version" > "$cutoff" ]]; then echo "PASSES"; else echo "FAILS"; fi

Result:

Version: 1.0.0
Cutoff: 0.5.0
PASSES

2. My reported issue

Variable expansion failing in for loops.

Test command:

for pr_num in 123 456; do echo "=== PR #$pr_num ==="; echo "test" | head -1; done

Result:

=== PR #123 ===
test
=== PR #456 ===
test

3. @rgaufman's reported issue

Syntax errors in for loops with complex regex patterns. I could not reproduce this in my environment, but it may be a different issue from the original report and my report.

Suggestion

The original issue and my reported issue appear to be resolved in v2.1.7. If @rgaufman's issue persists, it may have a different root cause and should be reported as a new issue.

I suggest this issue can be closed.

fsc-eriker · 4 months ago

Adding a noise comment to avoid having this closed.

rgaufman · 4 months ago
3. @rgaufman's reported issue Syntax errors in for loops with complex regex patterns. I could not reproduce this in my environment, but it may be a different issue from the original report and my report.

I can't reproduce if I disable oh-my-zsh, so it seems that is messing with Claude in some way. But I added hooks to stop it doing too much complex inline stuff and instead use skills and helper methods I created for our projects.

plavadp · 3 months ago

Confirmed: this bug caused rm -rf /* to execute on our systems

We traced an orphaned rm process that was attempting to delete / back to Claude Code v2.1.69. The root cause is the same shell-quote tokenization bug described in this issue.

Minimal Reproduction

Run this via the Claude Code Bash tool:

# Without pipe — works correctly (etD path)
MYVAR=hello && echo ${MYVAR}
# Output: hello

# With pipe — variable silently expands to empty (DsD path)
MYVAR=hello && echo ${MYVAR} | cat
# Output: (empty)

What happened to us

Claude generated a command roughly equivalent to:

CLEAN_DIR=/some/path && rm -rf ${CLEAN_DIR}/* && ./my_program --flag=value 2>&1 | head -200

Because the command contained | head -200, Claude Code took the pipe-aware quoting path (DsD), which tokenizes the command using shell-quote's parse(). This function expands ${CLEAN_DIR} against Node.js's process.env — where the variable obviously doesn't exist — so it silently expands to an empty string.

The actual command executed was:

eval 'CLEAN_DIR=/some/path && rm -rf /* && ./my_program ...' < /dev/null

The rm -rf /* ran and worked its way through the filesystem for several minutes before we found and killed the orphaned process.

Root Cause (from binary analysis of v2.1.69)

The Bash tool has two quoting paths:

  1. etD (no pipe) — wraps the command in single quotes: eval '...'. Shell variables like ${VAR} are preserved as literal text and correctly e

xpanded by eval at runtime. Safe.

  1. DsD (command contains |) — tokenizes the command via shell-quote.parse(), splits tokens at the pipe to insert < /dev/null, reconstructs

, then re-quotes. shell-quote.parse() expands ${VAR} references against process.env during tokenization. Any variable that only exists withi
n the bash command itself (e.g., set via VAR=x && ...) is not in process.env and silently expands to empty string. Dangerous.

The key code (deobfuscated from the binary):

// DsD - pipe-aware quoting
function pipeAwareQuote(cmd) {
    if (cmd.includes("`")) return fallbackQuote(cmd);
    if (cmd.includes("$(")) return fallbackQuote(cmd);
    // ...
    let result = shellQuote.parse(cmd);  // ← expands ${VAR} against process.env!
    // ... split at pipe, reconstruct, re-quote
}

Note: the function has guards for backtick and ` $( command substitution, but **no guard for ${VAR} parameter expansion**. Inline variable assig
nments chained with
&&` are a common shell pattern that this code silently corrupts.

Impact

This is not a cosmetic quoting issue — it is a data loss / security vulnerability. Any command that combines:

  1. An inline variable assignment (VAR=x && ...)
  2. A reference to that variable (${VAR} or $VAR)
  3. A pipe (|)

...will have the variable silently expanded to empty string before bash ever sees it. When combined with glob patterns and destructive commands like
rm -rf
, the results can be catastrophic.

fsc-eriker · 3 months ago

Adding a noise comment to prevent this from being closed.

mnardit · 3 months ago

Hey everyone still tracking the "crazy escaping" issues. I recently encountered a wild, real-world side effect of this exact shell-quote bug (specifically the ! being silently escaped to \!) and wrote a full post-mortem about it.

In my case, this escaping behavior silently corrupted a Tauri auto-updater signing key. I used Claude Code to generate the key, and because the Bash tool processes inputs through shell-quote, it silently injected a backslash into my password (mypassword! became mypassword\!). I never saw it, but the key was generated with it.

It worked flawlessly for 20 releases because Claude Code consistently applied the same buggy escaping every time I signed a release. However, when a recent update fixed this escaping behavior, my pipeline broke—the "correct" password no longer matched the key that was generated with the bug!

If anyone is currently digging into how shellQuoting.ts mutates commands or dealing with corrupted interactive CLI inputs, here is the full debugging story:

https://max.nardit.com/articles/the-invisible-backslash

A great example of Hyrum's Law in action where fixing the quoting behavior broke a downstream workflow.

fsc-eriker · 2 months ago

Adding a noise comment to prevent this from being closed.

fsc-eriker · 2 months ago

Adding a noise comment to prevent this from being closed.

fsc-eriker · 1 month ago

Adding a noise comment to prevent this from being closed.

fsc-eriker · 1 month ago

Adding a noise comment to prevent this from being closed.

fsc-eriker · 1 month ago

Adding a noise comment to prevent this from being closed.

fsc-eriker · 17 days ago

Adding a noise comment to prevent this from being closed.