[BUG] Incorrect escaping inside backticks: `!` is escaped as `\!`

Resolved 💬 24 comments Opened Oct 23, 2025 by ers35 Closed Apr 28, 2026
💡 Likely answer: A maintainer (claude[bot], contributor) responded on this thread — see the highlighted reply below.

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 ask Claude Code:

Run this command: /bin/echo '`!`'

but it returns:

⏺ Bash(/bin/echo '`!`')
  ⎿  `\!`

What Should Happen?

Claude should return:

⏺ Bash(/bin/echo '`!`')
  ⎿  `!`

Steps to Reproduce

Ask Claude Code:

Run this command: /bin/echo '`!`'

Another example:

> Make POST request to `https://echo.zuplo.io/` with this JSON data: {"text":"`!=`"}

⏺ I'll make a POST request to that URL with the JSON data.

⏺ Bash(curl -X POST "https://echo.zuplo.io/" -H "Content-Type: application/json" -d '{"text":"`!=`"}')
  ⎿  {
       "url": "https://echo.zuplo.io/",
       "method": "POST",
       "body": "{\"text\":\"`\\!=`\"}",
     }

vs:

% curl -X POST "https://echo.zuplo.io/" -H "Content-Type: application/json" -d '{"text":"`!=`"}'
{
  "url": "https://echo.zuplo.io/",
  "method": "POST",
  "body": {
    "text": "`!=`"
  },
}

Claude Model

Sonnet (default)

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.0.25

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

_No response_

View original on GitHub ↗

24 Comments

github-actions[bot] · 8 months ago

Found 2 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/2941
  2. https://github.com/anthropics/claude-code/issues/7387

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

brandon-fryslie · 8 months ago

Is this a major problem? ! is a special character in the shell.

As a workaround you can use this:

! echo !
  ⎿  !
ers35 · 8 months ago
Is this a major problem? ! is a special character in the shell. As a workaround you can use this: ! echo ! ⎿  !

The real problem is Claude Code can't use the Bash tool to write files that have \`!\ in them. For example, generating markdown that references code with !=`.

There are many contexts in the shell where backticks are valid. For example, wrapped in single quotes or a heredoc.

brandon-fryslie · 8 months ago

Ah good call. I hadn't considered the backticks since I was focusing on the bang. This does seem like an issue. I did a bit of testing in Claude.

> Print these characters using the bash tool: `!`

⏺ I'll print those characters using the Bash tool:

⏺ Bash(echo '`!`')
  ⎿  `\!`

⏺ The Bash tool printed: \!

  The exclamation mark gets escaped with a backslash in the output, which is expected behavior in bash when using single quotes with history expansion characters.

> why is that?

⏺ The backslash appears because of how bash handles history expansion and special characters:

  1. History Expansion: In bash, ! is a special character used for history expansion (like !! for the last command, !$ for the last argument, etc.)
  2. When You Run Locally: When you ran echo '!' in your terminal, you got ! without the backslash because your shell directly processed it. Inside single quotes, ! is treated literally in most contexts.
  3. When Claude Code Runs It: The Bash tool executes commands through a shell pipeline that may have different settings or processing layers. The backslash before ! is being added as an escape sequence
  somewhere in that execution chain - likely to prevent the shell from attempting history expansion.
  4. The Backticks: The backticks themselves are preserved because they're inside single quotes, which prevents command substitution.

  If you want to see the exact characters without any escaping, you could use:
  - printf '%s\n' '!' - printf is more predictable
  - cat << 'EOF' with a heredoc
  - Or just echo with double quotes: echo "\!"` (escaping the backticks)

  The behavior difference you're seeing between your local runs and the tool runs likely comes from different bash settings (like set +H to disable history expansion) or how the command execution environment is
   configured.

> OK, print `!` with each of those and we'll see what works

⏺ I'll test each method to see which one prints `!` correctly:

⏺ Bash(printf '%s\n' '`!`')
  ⎿  `\!`

⏺ Bash(cat << 'EOF'
      `!`…)
  ⎿  `!`

⏺ Bash(echo "\`!\`")
  ⎿  `!`

⏺ Great! Here are the results:

  1. printf '%s\n' '!' → `\!` ❌ (still escaped)
  2. cat << 'EOF' heredoc → `!` ✅ (perfect!)
  3. echo "\!"` → `!` ✅ (works!)

  Winners: Both the heredoc method and the double-quoted echo with escaped backticks work perfectly. The heredoc is probably the cleanest for multi-line content, while the double-quoted echo is simpler for
  single-line output.

  Interestingly, even printf with single quotes still shows the escaped !, which suggests the escaping is happening at a deeper level in how the Bash tool processes output, not just in the echo command itself.

I suppose as a workaround until this is fixed you could put in your CLAUDE.md either always use the Write tool when writing files and/or always use HEREDOCs when using the Bash tool to write files.

charles-dyfis-net · 7 months ago

@brandon-fryslie --

Is this a major problem? ! is a special character in the shell. As a workaround you can use this: ! echo ! ⎿  !

It's only a special character in INTERACTIVE shells with history expansion enabled. Noninteractive shells do not treat ! as special, and even interactive shells don't treat it as special inside single quotes. Normal single quotes ('...' as opposed to the ANSI C-like quoting style $'...') don't require _anything_ to be escaped -- only another single quote can be treated as non-literal in that context.

charles-dyfis-net · 7 months ago

(I hit this very regularly when jq is being used to filter JSON content -- the model can very easily want to run jq 'select(.foo != "bar")', and having that changed to \!= breaks the syntax).

ChadNedzlek · 6 months ago

So this is a really old (and embarrassing) bug. It's wasting a TON of my time and money. Claude's escaping is bash commands is absolutely inscrutable.

charles-dyfis-net · 6 months ago
So this is a really old (and embarrassing) bug. It's wasting a TON of my time and money. Claude's escaping is bash commands is absolutely inscrutable.

AFAICT, Claude itself is generating correctly-escaped output, but the bash tool is "sanitizing" that code before running it, and breaking it in the process.

atombender · 6 months ago

I can confirm that the log shows the right command line:

{
  "parentUuid": "2b64dad1-3387-4f3a-a139-b25edb7bbe3b",
  // ...
  "message": {
    "model": "claude-opus-4-5-20251101",
    "id": "msg_01X8cBBRhUJRDMVKTSCDB6Uo",
    "type": "message",
    "role": "assistant",
    "content": [
      {
        "type": "tool_use",
        "id": "toolu_01RqiXszQUJXkRuyhmzfgXn1",
        "name": "Bash",
        "input": {
          "command": "go build -o /tmp/debug ./cmd/debug && /tmp/debug --verbose '*[name != null]' 2>&1 | head -3",
          "description": "Test with debug output"
        }
      }
    ],
  // ...
github-actions[bot] · 5 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.

atombender · 5 months ago

Don't close.

timofriedlberlin · 5 months ago

For me claude constantly fails to close my MRs in gitlab, syntax is typically ... mr close "myproject!<mr_number>" my tooling will always receive an escaped \! - this is so sad. I just realized this bug is actually causing most of the failed bash attempts in my setup.

igor-makarov · 5 months ago

This is still happening:

⏺ Bash(node -e 'console.log(process.argv.slice(1))' -- '{"expression":"x!==null"}')
  ⎿  [ '{"expression":"x\\!==null"}' ]
brendanlong · 4 months ago

I'm running into this whenever Claude tries to run a GraphQL CLI with queries containing String!. Weirdly enough, you can tell Claude to use double quotes as a workaround:

❯ Maybe it's only escaping it within single quotes and not double quotes? Or maybe something weirder?

⏺ You're right, let me actually test this instead of theorizing.

⏺ Bash(echo 'String!')
  ⎿  String\!

⏺ Bash(echo "String!")
  ⎿  String!

⏺ So it's only escaping ! inside single quotes, and leaving it alone in double quotes. That's the opposite of normal bash behavior (where ! in double quotes triggers history
  expansion, and single quotes prevent it).

Update: This seems to be an inconsistent workaround and sometimes double-quoted ! gets escaped too. I'm very confused about what the rule is for this.

carrotRakko · 4 months ago

Adding another data point: you can isolate the escaping to the Bash tool layer (not bash itself) by writing the same command to a script file and executing it:

# Via Bash tool — escaped:
$ echo 'hello != world'
hello \!= world

# Via script file — correct:
$ cat <<'SCRIPT' > /tmp/test.sh
echo 'hello != world'
SCRIPT
$ bash /tmp/test.sh
hello != world

This confirms the escaping happens before the command reaches bash, consistent with @atombender's log analysis showing the model output is correct.

jq workaround: for those hitting this with != in jq expressions (as @charles-dyfis-net mentioned), select(.name == "value" | not) works as a drop-in replacement for select(.name != "value").

(Arrived here via #28259, closed as dup of this issue.)

✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)

Keno · 4 months ago

I run into this issue on pretty much a daily basis, e.g.

● Bash(echo 'test!' && echo "test!")
  ⎿  test\!
     test\!
gwillem · 4 months ago

This is indeed wasting tokens on a daily basis.

Known duplicates of this bug

Bash tool ! escaping (core bug):

  • #2941 (closed by stale bot)
  • #7387
  • #10335 (closed)
  • #17495 (closed)
  • #23740 (closed)
  • #24979 (closed by stale bot)
  • #28259 (closed as dup)
  • #29210
  • #30328

Skill/slash-command ! parsing (same root cause):

  • #8197
  • #13730
  • #13932
  • #17119
  • #21146
  • #28024
gwillem · 4 months ago

Isolated reproduction: ! is always escaped in argv, echo hides it

Built a tiny Go binary that prints os.Args with hex:

for i, a := range os.Args[1:] {
    fmt.Printf("arg[%d] = %q (len=%d, hex=%x)\n", i+1, a, len(a), a)
}

Results from the Bash tool:

$ /tmp/argtest '!' 'test!value' '--password=foo%!bar'
arg[1] = "\!" (len=2, hex=5c21)
arg[2] = "test\!value" (len=11, hex=746573745c2176616c7565)
arg[3] = "--password=foo%\!bar" (len=19, hex=2d2d70617373776f72643d666f6f255c21626172)

Every ! (0x21) becomes \! (0x5C 0x21) in arguments to external processes.

Why earlier tests with echo/printf/xxd appeared correct:

$ echo -n '!' | xxd
00000000: 21                  !     ← looks correct!

$ echo -n '!' | wc -c
1                                   ← looks correct!

But that's because echo is a shell builtin that receives the pre-escaping string. The escaping happens in the argv handoff to external processes. Since echo writes to a pipe before any external process sees the arg, the ! content flows through correctly.

Proof — the same string passed as an argument to an external process is corrupted:

$ python3 -c 'import sys; print(repr(sys.argv[1]))' '!'
'\\!'

TL;DR: The Bash tool unconditionally inserts \ before ! in all process arguments. Shell builtins mask this because they see the string before the escaping layer.

gwillem · 4 months ago

Working local workaround via PreToolUse hook

The escaping happens after hooks run, in the Bash tool's execution layer. So updatedInput alone can't strip the backslash — it gets re-added.

The trick: wrap the command in a single-quoted heredoc, which is immune to the escaping layer.

~/.claude/hooks/fix-bang-escape.sh:

#!/bin/bash
# Workaround for https://github.com/anthropics/claude-code/issues/10153
# The Bash tool escapes ! to \! AFTER hooks run, in the execution layer.
# Workaround: wrap the command in a heredoc so the shell interprets it
# without the escaping layer affecting arguments.

INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

# Only rewrite if the command contains !
case "$COMMAND" in
  *'!'*)
    # Wrap in a heredoc to bypass the escaping layer
    WRAPPED="eval \"\$(cat <<'CLAUDE_BANG_FIX'
$COMMAND
CLAUDE_BANG_FIX
)\""
    jq -n --arg cmd "$WRAPPED" '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "allow",
        updatedInput: {
          command: $cmd
        }
      }
    }'
    ;;
  *)
    exit 0
    ;;
esac

~/.claude/settings.json (add to hooks):

"PreToolUse": [
  {
    "matcher": "Bash",
    "hooks": [
      {
        "type": "command",
        "command": "~/.claude/hooks/fix-bang-escape.sh"
      }
    ]
  }
]

Verified:

$ python3 -c 'import sys; print(repr(sys.argv[1:]))' '--flag=val!ue' '!' 'a!b!c'
['--flag=val!ue', '!', 'a!b!c']
charles-dyfis-net · 4 months ago

@gwillem --

### Isolated reproduction: ! is always escaped in argv, echo hides it Built a tiny Go binary that prints os.Args with hex:

You're right about the behavior but wrong about the causal chain.

While echo is indeed a built-in and _could_ be written to have access to the pre-parsed command line, it does not in fact make any use of that access.

echo receives the same content every other command does -- however, like printf's format string, echo does an unescaping pass on its arguments before printing them. (Strictly, this is conditional -- it depends in part on whether the xpg_echo flag is set).

charles-dyfis-net · 4 months ago

Let me be explicit here --

The most appropriate fix is to simply remove this escaping entirely. ! only needs to be escaped in shells with history expansion enabled; history expansion is turned off by default in noninteractive shells; there's no good reason to explicitly enable history expansion in a shell started by Claude Code.

If one is concerned about edge cases where history expansion has been explicitly re-enabled via BASH_ENV or exported SHELLOPTS, a set +H in the prelude before user code would also suffice. Note that set +H must be separated from the !-containing code by a newline (not ;), as bash parses history expansion before executing set when they're in the same parse unit.

claude[bot] contributor · 2 months ago

This issue was fixed as of version 2.1.75.

Keno · 2 months ago

Can confirm that this is fixed. Thanks!

github-actions[bot] · 27 days ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.