[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_
24 Comments
Found 2 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
Is this a major problem?
!is a special character in the shell.As a workaround you can use this:
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.
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.
I suppose as a workaround until this is fixed you could put in your
CLAUDE.mdeither always use theWritetool when writing files and/or always useHEREDOCs when using theBashtool to write files.@brandon-fryslie --
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.(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).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.
I can confirm that the log shows the right command line:
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.
Don't close.
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.This is still happening:
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: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.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:
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 forselect(.name != "value").(Arrived here via #28259, closed as dup of this issue.)
✍️ Author: Claude Code with @carrotRakko (AI-written, human-approved)
I run into this issue on pretty much a daily basis, e.g.
This is indeed wasting tokens on a daily basis.
Known duplicates of this bug
Bash tool
!escaping (core bug):Skill/slash-command
!parsing (same root cause):Isolated reproduction:
!is always escaped in argv,echohides itBuilt a tiny Go binary that prints
os.Argswith hex:Results from the Bash tool:
Every
!(0x21) becomes\!(0x5C 0x21) in arguments to external processes.Why earlier tests with
echo/printf/xxdappeared correct:But that's because
echois a shell builtin that receives the pre-escaping string. The escaping happens in the argv handoff to external processes. Sinceechowrites 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:
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.Working local workaround via PreToolUse hook
The escaping happens after hooks run, in the Bash tool's execution layer. So
updatedInputalone 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:~/.claude/settings.json(add tohooks):Verified:
@gwillem --
You're right about the behavior but wrong about the causal chain.
While
echois 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.echoreceives 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 thexpg_echoflag is set).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_ENVor exportedSHELLOPTS, aset +Hin the prelude before user code would also suffice. Note thatset +Hmust be separated from the!-containing code by a newline (not;), as bash parses history expansion before executingsetwhen they're in the same parse unit.This issue was fixed as of version 2.1.75.
Can confirm that this is fixed. Thanks!
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.