CLAUDE LABJP
PRICING — September 1 was the scheduled date for the Sonnet 5 price increase, and it did not happen. The introductory $2/$10 per MTok now stands as the regular pricePARTNER — Salesforce and Anthropic announced Claudeforce, an expanded partnership. The Salesforce in Claude plugin ships with 37 prebuilt sales skills, from meeting prep to pipeline managementTRUST — Claudeforce serves Claude through Amazon Bedrock inside the Salesforce Trust Boundary, so data and inference never leave the security perimeter — an answer aimed squarely at regulated industriesBETA — Salesforce in Claude is with select pilot customers for now, with an open beta expected during SeptemberLIMITS — The 50% weekly-limit boost runs through September 13. From September 14 the permanent level is 25% above the pre-promotion baseline, roughly a 17% cut from todayRELEASE — Claude Code has shipped nothing since v2.1.251 on August 28. Against a pace of one release every 0.8 days, a four-day gap is among the longest yetPRICING — September 1 was the scheduled date for the Sonnet 5 price increase, and it did not happen. The introductory $2/$10 per MTok now stands as the regular pricePARTNER — Salesforce and Anthropic announced Claudeforce, an expanded partnership. The Salesforce in Claude plugin ships with 37 prebuilt sales skills, from meeting prep to pipeline managementTRUST — Claudeforce serves Claude through Amazon Bedrock inside the Salesforce Trust Boundary, so data and inference never leave the security perimeter — an answer aimed squarely at regulated industriesBETA — Salesforce in Claude is with select pilot customers for now, with an open beta expected during SeptemberLIMITS — The 50% weekly-limit boost runs through September 13. From September 14 the permanent level is 25% above the pre-promotion baseline, roughly a 17% cut from todayRELEASE — Claude Code has shipped nothing since v2.1.251 on August 28. Against a pace of one release every 0.8 days, a four-day gap is among the longest yet
Articles/Claude Code
Claude Code/2026-09-01Intermediate

Drop the quotes on a heredoc and the prices in your log quietly change

An unquoted heredoc runs the variables and backticks inside its body. Here are the four ways my log got rewritten, how the three quoting forms compare, a safe placeholder-and-sed pattern, and a small script for auditing what you already have.

Claude Code243Shell scripting2heredocAutomation44Troubleshooting14

I opened a run log and the line I had written as Cost: $2 / MTok read Cost: / MTok. The number was simply gone.

One line above it, a command string I had pasted in for future reference had been replaced by the output of that command. And when I counted, the log had six lines where I had written seven.

Nothing in the script explained it. The only unusual thing was that the EOF in cat << EOF had no quotes around it.

The fix, before anything else

Quote the delimiter word. That is the entire diff.

# Before
cat << EOF > log.txt
Cost: $2 / MTok
EOF
 
# After
cat << 'EOF' > log.txt
Cost: $2 / MTok
EOF

When the delimiter word is unquoted, the shell treats the heredoc body as something to expand. When it is quoted, the body is passed through untouched, character for character.

If that is all you needed, you are done. The rest of this article is what I measured: exactly what gets rewritten, how to fill in values without giving up the quoting, and how to audit the scripts you already have.

What the unquoted heredoc actually rewrote

I meant to write seven lines. DATE was set to 2026-09-01 beforehand.

DATE="2026-09-01"
cat << EOF > a2.txt
Date: $DATE
Generated by: `date -u +%Y-%m-%dT%H:%M`
Backup path: /var/backup/$USER/
Cost: $2 / MTok
Regex: ^\d{4}-\d{2}$
Continuation: trailing backslash \
next line
EOF

Here is what landed in a2.txt:

Date: 2026-09-01
Generated by: 2026-09-01T06:06
Backup path: /var/backup/masaki/
Cost:  / MTok
Regex: ^\d{4}-\d{2}$
Continuation: trailing backslash next line

wc -l reported 6. Side by side:

What I wroteWhat came outWhat happened
Date: $DATEDate: 2026-09-01Variable expanded (as intended)
Generated by: date in backticksGenerated by: 2026-09-01T06:06The command ran and was replaced by its output
/var/backup/$USER//var/backup/masaki/Environment variable expanded
Cost: $2 / MTokCost: / MTokRead as positional parameter $2, became empty
Regex: ^\d{4}-\d{2}$Regex: ^\d{4}-\d{2}$Survived intact (see below)
Line ending in a backslash, plus the next lineJoined into one lineEaten as a line continuation, losing a line

As an indie developer reviewing my own scripts, the fourth row is the one that worries me. $2 was not two dollars; it was the second positional argument, and since none was passed, it became an empty string. No error, no warning. A cost record just goes blank. That is the line that made me look in the first place.

The second row is nearly as unpleasant, because "paste the command in so I can check it later" is one of the most common things a log line does. Suddenly your log contains a value you never wrote, and reading the script gives you no clue where it came from.

Meanwhile the \d and the trailing $ in row five came through untouched. Inside an unquoted heredoc, a backslash only escapes $, a backtick, another backslash, or a newline. Everything else passes straight through. Some lines break and some do not, which is precisely why a quick visual review misses this.

Three ways to quote, all with the same result

I put $HOME and a backtick on one line and varied only the delimiter.

FormOutput
<< EOF/home/masaki|RUN (expanded and executed)
<< 'EOF'Literal
<< "EOF"Literal
<< \EOFLiteral

Single quotes, double quotes, a backslash — all identical. If any part of the delimiter is quoted, the whole body becomes literal.

This is where the rule diverges from ordinary shell intuition. Double quotes normally expand variables, but for a heredoc delimiter the only question is quoted or not. I standardized on 'EOF' so readers do not have to think about it, though there is no reason to go rewrite existing "EOF" usages.

<<- strips leading tabs. Only tabs — spaces stayed. If your editor converts tabs to spaces, that feature silently stops working.

Filling in values without giving up the quoting

Sometimes the heredoc is unquoted on purpose, because you do want a variable expanded. If there are only two or three such values, it is safer to keep the body literal and substitute afterwards.

DATE="2026-09-01"; SLUG="my-article"
 
cat << 'EOF' > tmpl.txt
Date: __DATE__ / slug: __SLUG__
Cost: $2 / MTok
EOF
 
sed -e "s/__DATE__/${DATE}/" -e "s/__SLUG__/${SLUG}/" tmpl.txt

Output:

Date: 2026-09-01 / slug: my-article
Cost: $2 / MTok

The two placeholders filled in and $2 stayed a literal string. I use this shape whenever the text being written out is something I cannot fully predict, which is most logs.

There is a second trap here, though. If the value you substitute contains a slash, sed falls over.

P="/var/log/app"
sed "s/__P__/${P}/" <<< "path: __P__"
# => sed: -e expression #1, char 10: unknown option to `s'

The delimiter in an s command is yours to choose, so use | when substituting paths:

sed "s|__P__|${P}|" <<< "path: __P__"
# => path: /var/log/app

Pinning the delimiter to | for path substitution is a reasonable default. If the value itself might contain a |, hand the job to python3 or awk instead of sed.

Auditing the scripts you already have

If you hit this once, the same shape is almost certainly elsewhere. This script picks out unquoted heredocs whose bodies actually contain something expandable.

import re, sys, pathlib
 
OPEN = re.compile(r"<<-?\s*(?P<q>['\"\\])?(?P<w>[A-Za-z_][A-Za-z0-9_]*)")
RISK = re.compile(r"`|\$\(|\$[A-Za-z_{0-9]|\\$")
 
def scan(path):
    lines = pathlib.Path(path).read_text(errors="replace").splitlines()
    hits, i = [], 0
    while i < len(lines):
        m = OPEN.search(lines[i])
        if m and m.group("q") is None:
            word, start = m.group("w"), i
            body = []
            i += 1
            while i < len(lines) and lines[i].strip() != word:
                body.append(lines[i]); i += 1
            risky = [b for b in body if RISK.search(b)]
            if risky:
                hits.append((start + 1, word, len(body), len(risky)))
        i += 1
    return hits
 
total = 0
for p in sys.argv[1:]:
    for ln, w, n, r in scan(p):
        total += 1
        print(f"{p}:{ln}: unquoted heredoc ({r} of {n} lines expandable)")
print(f"Found: {total}")
sys.exit(1 if total else 0)

Quoted delimiters are excluded up front, and so are bodies containing no backtick, $(, $variable, or trailing backslash — without something to expand, an unquoted heredoc does no harm.

I ran it over the 272 shell scripts on a Linux box, under /usr/bin, /usr/sbin, and /etc.

MeasureCount
Shell scripts scanned272
Files containing a heredoc60
Unquoted heredocs found138 (across 40 files)
Quoted heredocs found9

To be honest about what that means: most of those 138 are not bugs. Scripts that generate configuration files are unquoted deliberately, because expansion is the point. What this produces is not a bug list but a review list.

It is still worth having. Each hit carries a line number and how many body lines are expandable, so one glance at the file tells you whether it is generating config or writing a log. I now skim this output before pushing changes to anything that runs unattended.

The same failure shape — no error, just a different result — turns up elsewhere too. A One-Letter Typo in settings.json Is Ignored Without a Single Warning covers the configuration-key version of it. If you want to go one level deeper into shell quoting specifically, One Space in a Folder Name Turned 80 Checks Into Zero measures how a path containing a space quietly empties a batch run.

Why an AI tends to hand you the unquoted form

The original script came from asking Claude Code to "add something that writes out a log." What came back was a plain cat << EOF.

I think the numbers above explain it. Across those 272 scripts there were 9 quoted heredocs and 138 unquoted ones. Unquoted is the overwhelming majority in real-world shell code, and a model trained on that corpus reflects the ratio. A human reading the same code would pick up the same habit. This is less a model flaw than a consequence of the shell defaulting to expansion.

Which means one extra sentence in the request changes the output:

Add a step that writes out a log.
Always use the <<'EOF' form for heredocs (quote the delimiter),
and where a value must be filled in, leave a __PLACEHOLDER__ and substitute it with sed.

Since adding that, the generated scripts have been consistent. Putting it in your project configuration or a skill file means you do not have to say it every time.

The single thing worth doing today: search your scripts for lines starting with cat << EOF, and quote the delimiter on the ones that write logs or messages. Leave the config generators alone. That alone closes one path by which numbers and dates rewrite themselves without telling you.

I had read about this behaviour more than once before it ever cost me anything, and it took a log that was one line short to make it feel like my problem. If you have stopped in the same place, I hope this saves you the detour. Thank you for reading.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $15 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Claude Code2026-08-22
Handing a long job to another session — and the completion marker for when the notification never arrives
How to use notify_when_idle to hear when another Claude Code session finishes, and a small completion marker that keeps you from waiting forever when the notification is dropped.
Claude Code2026-05-18
Why Your `cd` and `export` Vanish Between Claude Code Bash Calls
Claude Code's Bash tool runs each call in a fresh shell, so cd and export never persist. Here's the symptom, the cause, and five practical patterns I use across my Dolice Labs pipelines.
Claude Code2026-08-16
A Runaway Build Dies Very Differently Under cgroup Than Under ulimit
Claude Code v2.1.233 added opt-in memory cgroup support for Bash tool commands on Linux. Capping virtual address space with ulimit -v and capping physical memory with a cgroup produce completely different failures — and only one of them lets your Node build start at all.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →