Bash tool: shell-quote escapes ! to \! in non-interactive shells, corrupting command data

Resolved 💬 2 comments Opened Mar 18, 2026 by Barahlush Closed Apr 15, 2026

Bug Description

The Bash tool's bundled shell-quote library escapes ! to \! when the command string triggers the double-quote wrapping path (branch 2). Since claude -p and the Bash tool run commands via bash -c (non-interactive shell), history expansion is disabled and the backslash is treated as a literal character, corrupting data sent to downstream tools.

Reproduction

Start a simple HTTP server that logs raw request bodies:

from http.server import HTTPServer, BaseHTTPRequestHandler
class H(BaseHTTPRequestHandler):
    def do_POST(self):
        l = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(l)
        print(f'RECEIVED: {repr(body)}', flush=True)
        self.send_response(200); self.end_headers(); self.wfile.write(b'ok')
    def log_message(self, *a): pass
HTTPServer(('0.0.0.0', 9999), H).serve_forever()

Then via claude -p:

Run this exact command:
RESULT=$(curl -s -X POST http://127.0.0.1:9999 -d "flag=BCCTF{test!}" -o /dev/null -w '%{redirect_url}') && echo "$RESULT"

Expected: Server receives flag=BCCTF{test!}
Actual: Server receives flag=BCCTF{test\!}

The \! in the received data is a literal backslash + exclamation mark. The data is corrupted.

When it triggers

The bug hits shell-quote's branch 2 — when the full command string contains both a single quote AND whitespace/double-quote:

| Command | Branch | Result | Correct? |
|---------|--------|--------|----------|
| echo "hello!" | 1 (single-quote wrap) | 'echo "hello!"' | ✅ Yes |
| curl -d "flag=test!" -w '%{url}' | 2 (double-quote wrap) | "curl -d \"flag=test\!\" -w '%{url}'" | ❌ No → \! |
| grep -r 'TODO!' src/ | 2 (double-quote wrap) | "grep -r 'TODO\!' src/" | ❌ No → \! |
| git commit -m 'fix!' | 2 (double-quote wrap) | "git commit -m 'fix\!'" | ❌ No → \! |
| jq 'select(. != 2)' | 2 (double-quote wrap) | "jq 'select(. \!= 2)'" | ❌ No → \!= |

The trigger is mixed quoting (' and " or whitespace in the same command) combined with !.

Root cause

In the bundled shell-quote library:

// Branch 2: command has both ' and whitespace/" → wraps in "..."
if (/["'\s]/.test(q))
    return '"' + q.replace(/(["\\$`!])/g, "\\$1") + '"';
//                                    ^ escapes ! to \!

This escaping is intended for interactive bash where ! triggers history expansion inside "...". But Claude Code runs commands via bash -c (non-interactive shell) where histexpand is off by default. The \! doesn't get de-escaped and the backslash stays literal.

Impact

Any command with mixed quoting and ! is affected:

  • curl/wget with flags containing ! (common in CTF challenges)
  • jq filters with !=
  • awk expressions with !~ or !=
  • grep patterns containing !
  • git commit messages with !
  • SSH/sshpass commands with passwords containing !
  • Any tool receiving ! as part of data or syntax

This is particularly impactful for claude -p (SDK/headless mode) where agents submit data containing special characters.

Environment

  • Claude Code 2.1.78 installed via npm install -g @anthropic-ai/claude-code
  • Node.js v22.22.0
  • Bash 5.3.3 (Kali Linux container)
  • NOT reproducible with the SEA binary (installed via claude CLI installer) on Node v20.20.0 — the SEA binary appears to handle this differently

Suggested fix

Remove ! from the branch 2 escape pattern:

// Before:
q.replace(/(["\\$`!])/g, "\\$1")
// After:
q.replace(/(["\\$`])/g, "\\$1")

This is safe because bash -c runs non-interactive shells where ! has no special meaning.

Workaround

Avoid single quotes in commands that contain !. If the command has no single quotes, shell-quote uses branch 1 (single-quote wrapping) which preserves !:

# Broken (mixed quotes → branch 2):
curl -d "flag=test!" -w '%{redirect_url}'

# Works (no single quotes → branch 1):
curl -d "flag=test!" -w "%{redirect_url}"

When single quotes can't be avoided, write data to a file first:

printf 'flag=%s' 'test!' > /tmp/data.txt
curl -d @/tmp/data.txt -w "%{redirect_url}"

Related issues

  • #2941 (original report, closed due to inactivity)
  • #10335 (duplicate, closed)
  • #23740 (detailed root cause analysis, closed as duplicate)
  • #24979 (awk commands affected, closed as duplicate)

All previous issues were closed automatically by the inactivity bot despite active user comments. The bug remains unfixed.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗