[BUG] Insecure code generation: markdown backticks in an unquoted heredoc execute locally

Status Open
Reported on v2.1.229
Maintainer reply None cached
Activity 0 comments · opened Aug 16, 2026

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?

_(report generated by Claude)_

Insecure code generation: markdown backticks in an unquoted heredoc execute locally

Reporter: myuseridbws
Found: 2026-08-16, in code generated by Claude (Claude Code) on 2026-08-15
Class: insecure code generation → OS command injection sink (closest fit: CWE-78)
Not: prompt injection — see What this is and is not
Environment: Claude Code 2.1.229, model claude-opus-5, macOS 15 (Darwin 24.6.0)
Authorship basis: operator recollection + circumstantial; see Provenance — there is no
transcript or commit trailer proving model generation, and this report does not need one.

---

One-paragraph summary

Claude generated a bash installer that ships a setup script to a remote host through an
unquoted heredoc (<<REMOTE), which is correct and necessary — the body interpolates
local variables before transmission. Three weeks later, in a separate change, Claude added
an explanatory comment inside that heredoc body using markdown-style backtick quoting:

# ...leaving a live OAuth refresh token untracked-and-unignored, one `git add -A`
# from being committed.

In an unquoted heredoc, backticks are command substitution. The comment executed. Every
run of that installer silently ran git add -A against the operator's git repository, in
whatever directory the installer was invoked from. The comment warning that a stray
git add -A could commit a credential was itself running git add -A.

Minimal reproducer

cd /tmp && bash -c 'cat <<EOF
# a comment mentioning `id -un`
EOF'

Prints the output of id -un. With a quoted delimiter (<<'"'"'EOF'"'"') it prints the
comment literally. Substitute any command; comments are not inert here.

Impact

Observed impact was low: git add -A staged files whose blobs matched the working tree,
so nothing was destroyed. The realistic worst case is not low:

  • Inverted severity. The hazard scales with how dangerous the documented command is.

A comment explaining why rm -rf "$DIR" is risky would have deleted. A comment about
curl … | sh would have fetched and run. Writing a safety warning is what arms it.

  • Silent and remote from its cause. The effect lands on the operator's machine while

the code reads as remote setup, and produces no output.

  • Credential exposure is the realistic path. This exact instance staged the operator's

whole tree. A git add -A while an untracked token, .env or capture file sits in the
tree is precisely how a credential reaches a commit — the outcome the comment existed to
warn about.

  • Injection corollary. Independent of comments, an unquoted heredoc is a live command

injection sink whenever an interpolated value carries untrusted data (a filename, branch
name, commit message, or anything derived from a network response).

Why it went undetected for six occurrences and two debugging sessions

  • grep cannot find it. The script contains no git add in its text. grep -nw git

returns four lines: two comments, command -v git, git check-ignore.

  • Reading it does not find it. The region looks like remote script; the eye reads a

comment as documentation. It was read twice without being seen.

  • The construct is assumed inert. Comments are inert in every other context a shell

author meets. The heredoc is the exception.

  • Every diagnostic clue was true and misleading. Syscall tracing named the process

image (Homebrew bash — matching ~40 wrappers on the machine) and the working directory
(the repo root — reading as a deliberate command). The staging process was a forked
command-substitution subshell, so parent-process lookup returned null, which was
glossed as "a long-lived daemon" and pointed the investigation at the wrong suspect
for two occurrences.

  • A standing suspicion landed on the wrong party. By elimination, the Claude desktop

app was the lead suspect for several days. It was innocent.

What finally found it: bash -x with PS4='+${BASH_SOURCE##*/}:${LINENO}: ', which
printed ++install.sh:354: git add -A — one line. When a script does something its text
does not contain, static reading cannot help; only execution tracing can.

Provenance — the two halves were three weeks apart

| Commit | Date | Contribution |
|---|---|---|
| fae8746 | 2026-07-26 | Introduced the unquoted <<REMOTE heredoc. Safe — no backticks in the body. |
| 9835c1b | 2026-08-15 | Added .gitignore hardening and its explanatory comment, arming the heredoc. |

Neither change is defective alone. Reviewing either diff in isolation — which is how
diffs are reviewed — reveals nothing. This is why the pattern deserves a check rather
than reviewer vigilance.

How authorship is known, stated honestly

Both commits were made during Claude Code sessions working on this repository, and the
operator's account is that the installer and its comments were model-written. That is
the whole basis.
Two things a reader might expect as corroboration do not exist:

  • No generating transcript. Local session logs were searched across every project.

Every stored transcript containing the offending comment dates from 2026-08-16 — the
debugging sessions that read the file. The 2026-08-15 session that wrote it is not
on disk.

  • No commit trailer. This repo's policy is that the human runs all git commands, so

every commit is human-authored and carries no Co-Authored-By. Authorship of the code
is simply not recorded in git.

The report does not rest on this. The reproducer is three lines and
provider-agnostic; the construct is verifiable in any shell in one command, whoever
typed it. Provenance changes how interesting this is, not whether it is real. It is
volunteered here so a triager does not have to discover the gap themselves.

Version, and a trap in checking it

Every session on 2026-08-15 and 2026-08-16 recorded Claude Code 2.1.229 running
claude-opus-5, read from the version and message.model fields of the local
session logs (~/.claude/projects/<slug>/*.jsonl).

Note that claude --version on this machine reports 2.1.220 — a separate, older CLI
on PATH that is not what the sessions ran. The sessions used the version bundled with
the desktop app. Anyone reproducing this against a claimed version should read the
transcript's version field rather than trust claude --version; the two disagree, and
the command is the one people will reach for.

Resolution

Backticks replaced with plain quotes, and a caution placed at the heredoc opener,
where the mistake is made, naming the hazard and the escapes (` \ `, \$(`).

Verified: line-numbered bash -x trace shows ++install.sh:354: git add -A before the
fix and no match after; a full installer run now leaves the git index untouched.

A repo-wide scan for unquoted heredocs containing backticks or $( ) found 14 hits, 13
of them intentional local $( ) in local cat <<EOF output blocks. This was the only
genuine instance.

What this is and is not

Is: a code-generation correctness and security defect. The plausible mechanism is a
formatting convention leaking across contexts — backticks are the markdown idiom for
"this is code," used constantly in prose, comments and documentation. Inside an unquoted
heredoc that same character is an execution primitive. The model applied a documentation
habit in a region where it is executable, and did so specifically inside a comment, where
authors relax because comments are inert everywhere else.

Is not: prompt injection. No untrusted input steered the model; it generated the
construct unprompted. Filing it as prompt injection will likely get it triaged away.

What Should Happen?

Suggested mitigations

  1. Generation-time rule: when emitting a heredoc with an unquoted delimiter, never

emit a bare backtick or $( in the body — escape it or use plain quotes. This applies
to comments as strongly as to code.

  1. Prefer a quoted delimiter by default (<<'EOF'), switching to unquoted only when

interpolation is actually required, and then escaping the rest.

  1. Lint: flag unquoted-heredoc bodies containing unescaped ` ` or $(`. Cheap,

and it catches the cross-commit case that review cannot.

  1. Never use markdown backtick quoting inside generated shell comments. Single quotes

convey the same meaning and are inert everywhere.

Error Messages/Logs

The 'error' in my case was that files were staged without consent because a `git add -A` command was generated inside a heredoc command and was, therefore, executed indiscriminately.

The severity of the error depends on what type of command was generated inside the backticks (determined by the code generation's context for the heredoc).

Steps to Reproduce

This issue is a code generation issue that I am not sure how to reproduce. The following is a minimal example of the type of problematic code that was generated:

Minimal reproducer

cd /tmp && bash -c 'cat <<EOF
# a comment mentioning `id -un`
EOF'

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.229

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

iTerm2

Additional Information

Artifacts available on request

Line-numbered bash -x traces before and after the fix; eslogger exec fork captures
with process-ancestry chains for two occurrences; the investigation log
(notes/git-index-staging-anomaly.md) covering all six occurrences and the eliminations.

View original on GitHub ↗