●MCP — Claude now supports more of the MCP 2026-07-28 spec, bringing a stateless core, stronger OAuth and OIDC authorization, and versioned extensions for Apps and Tasks●DESIGN — /design arrived as a research preview on August 17. Hand it an idea, a screenshot, or an existing design and it returns editable artboards in Claude Design●OUTPUT — A Concise output style now leads with the result, which helps when you would rather not read past the preamble●PERMISSIONS — Auto mode is the new default, and you write allow and deny rules as plain sentences rather than patterns●LIMITS — The 50 percent increase to weekly limits runs through August 31 for Pro, Max, Team, and seat-based Enterprise plans. Six days remain●RESUME — Sessions that hit a usage limit now continue automatically once the limit resets, and protections against credential leaks have been strengthened●MCP — Claude now supports more of the MCP 2026-07-28 spec, bringing a stateless core, stronger OAuth and OIDC authorization, and versioned extensions for Apps and Tasks●DESIGN — /design arrived as a research preview on August 17. Hand it an idea, a screenshot, or an existing design and it returns editable artboards in Claude Design●OUTPUT — A Concise output style now leads with the result, which helps when you would rather not read past the preamble●PERMISSIONS — Auto mode is the new default, and you write allow and deny rules as plain sentences rather than patterns●LIMITS — The 50 percent increase to weekly limits runs through August 31 for Pro, Max, Team, and seat-based Enterprise plans. Six days remain●RESUME — Sessions that hit a usage limit now continue automatically once the limit resets, and protections against credential leaks have been strengthened
One Space in a Folder Name Turned 80 Checks Into Zero
An inspection loop reported 80 files checked and 0 readable. The files were fine. Here is how word splitting turns path fragments into real directories, measured side by side, plus the count assertion I now put in front of every delete-heavy batch.
I wanted a quick pass over the documents scattered through a working folder, so I wrote a short loop: match by extension, read the first byte, count what opens. Roughly 80 files.
It printed this.
checked=80 readable=0 unreadable=80
Eighty files checked, none readable. My first suspicion was sync. The folder lives under cloud storage, so I assumed the contents had not been materialized yet.
But opening any one of them with cat worked fine. Nothing was missing.
The files were never the problem. The folder name contained a single space. That was the whole story.
The 80 files had become 160 iterations
The loop looked like this — probably the most commonly written shape in personal inspection scripts.
for f in $(find "$ROOT/_documents" -type f -name "*.md"); do if head -c 1 "$f" >/dev/null 2>&1; then ok=$((ok+1)); else ng=$((ng+1)); fidone
"$ROOT/_documents" is quoted. The loop variable "$f" is quoted. It still breaks.
What breaks is outside the $( ). The result of a command substitution sits in an unquoted position, so the shell splits it on IFS — space, tab, newline by default. A single line of output from find gets torn in two at the space.
Here is the measurement. Eighty 100-byte files under a directory whose name contains a space, same loop:
One hundred sixty iterations — exactly twice the file count. Each path split into two fragments, and the loop treated each fragment as a filename. Neither half opens, naturally.
Switch to NUL-delimited output and the picture changes:
while IFS= read -r -d '' f; do head -c 1 "$f" >/dev/null 2>&1 && ok=$((ok+1))done < <(find "$ROOT/_documents" -type f -name "*.md" -print0)
All 80. The files had been readable the entire time.
When a fragment turns into a real path
So far this is only a missed read. What actually made me sit up was the next test.
The leading fragment of a split path can be a directory that exists.
Split /work/Dolice Labs/out/tmp_1.txt and the first half is /work/Dolice. If a folder named Dolice happens to live at that level, the fragment does not point at nothing. It points at somewhere real.
To check, I put 5 files I wanted deleted in Dolice Labs/out/ and 3 files that must survive in Dolice/keep/, then ran the naive loop with a delete in it.
for f in $(find "$T/Dolice Labs/out" -type f -name "tmp_*.txt"); do rm -rf "$f" 2>/dev/nulldone
The result:
before: keep=3 tmp=5
after : keep=0 tmp=5
Target
Intent
Before
After
Dolice Labs/out/
delete these
5
5 (nothing deleted)
Dolice/keep/
never touch these
3
0 (all gone)
Not one intended file was removed, and the side that should have been untouched was wiped. The loop exited 0 and printed nothing to stderr.
Note that rm -rf "$f" is quoted. Quoting only guarantees that the fragment arrives as a single argument. It says nothing about where that fragment points. That is the core of this failure.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Be able to tell, in a few minutes, whether a batch script running under a path with spaces is quietly skipping every file
✦Be able to separate 'nothing matched' from 'nothing could be opened' mechanically, by reconciling expected and processed counts
✦Be able to decide where a guard belongs in a delete-heavy batch, based on a run that kept all 5 files it meant to delete and removed all 3 it was supposed to leave alone
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Putting set -euo pipefail at the top of a script is standard hygiene. I do it too. So I measured what it does here.
completed under set -euo pipefail: exit=0 iterations=160
Nothing stops. Broken down:
Option
What it catches
Why it misses this
-e
commands exiting non-zero
a fragment either skips silently as a missing file, or hits a real path and succeeds
-u
unset variables
the variable is set; only its value is wrong
-o pipefail
failures mid-pipeline
splitting happens during expansion, not in a pipe
Shell safety flags watch for commands that fail. Nothing failed here. It succeeded, against the wrong target. That is outside what those flags can see.
An empty variable produces the same shape of problem. I wrote about a late-night cleanup that nearly reached far too wide because a variable went empty, in The night an empty variable met rm -rf. That time the pre-execution check caught it. This time the string shown in the check looks perfectly reasonable — rm -rf "/work/Dolice" is not suspicious on its own.
How many files each style actually reaches
Same 80 files, common styles side by side. All measured on GNU bash 5.1.16.
Style
Files reached
Notes
for f in $(find ...)
0 / 80
160 empty iterations, no error output
grep -l "x" $(find ...)
0 / 80
returns 0, which reads as "no matches"
IFS=$'\n' then for
80 / 80
still breaks on newlines in filenames
find ... -print0 | xargs -0
80 / 80
first choice when driving an external command
find ... -exec ... {} +
80 / 80
no shell in between, so no splitting
while IFS= read -r -d ''
80 / 80
use this when the loop body branches
The second row worries me most. When grep -l returns zero results, people read it as "no files matched." Nobody reads it as "no files could be opened." The same zero means the opposite thing, and an audit script that prints "no violations found" will sail straight through review.
IFS=$'\n' is convenient and does work in the moment. It falls over the same way on filenames containing newlines, so I avoid it anywhere that handles downloaded assets or material received from outside.
The guard I keep in front of these jobs
Rather than memorizing the correct idiom, I have shifted toward making a miss visible when it happens. Idioms slip out of memory. A guard stays in the file.
It is just a reconciliation of counts.
# 1. Count what we expect, without going through word splittingexpected=$(find "$ROOT" -type f -name "*.md" -print0 | tr -cd '\0' | wc -c)# 2. Count what we actually processedprocessed=0while IFS= read -r -d '' f; do head -c 1 "$f" >/dev/null 2>&1 && processed=$((processed+1))done < <(find "$ROOT" -type f -name "*.md" -print0)# 3. Stop on mismatchif [ "$expected" -ne "$processed" ]; then echo "Missed files: expected=$expected processed=$processed" >&2 exit 1fiecho "Processed $processed files"
The first line counts NUL bytes directly so that the expected count itself is immune to splitting. find ... | wc -l inflates on filenames containing newlines.
With this in place, the deletion example above stops at expected=5 processed=0 before any rm runs. Dolice/keep/ is never reached.
For delete-heavy batches I add one more layer: confine what may be deleted to a directory whose name says so.
# Deletion is allowed in exactly one placeDISPOSABLE="$ROOT/_scratch"case "$target" in "$DISPOSABLE"/*) : ;; *) echo "Target is outside _scratch: $target" >&2; exit 1 ;;esac
Even if a path splits into $ROOT/Dolice, it fails the case match. This defends by narrowing what can match, rather than by writing a more expressive rule. I trust that more. The more denial rules I express in prose, the wider the gap grows between "I wrote it" and "it is in effect," and I would rather have something structurally simple catching the last mile.
Five minutes of checks before you hand it off
Delegating batch work to Claude Code or Cowork makes this failure harder to spot, because nobody is reviewing results one by one — a report of "0 files" passes through unexamined. Since auto mode became the default on August 14, with fewer mid-run confirmations, that gap has felt wider to me.
Three checks cover it.
Check
How
Warning sign
Does the working path contain a space?
echo "$ROOT" | grep -q " " && echo "has space"
if yes, always run the next two
Any bare command substitution?
grep -n 'in \$(find' script.sh
even one hit means rewrite
Do the counts agree?
add the guard above to the first job in the chain
processed is 0, or an integer multiple off
That third sign — an integer multiple — comes from the fact that fragments multiply with the number of spaces. Eighty files became 160 iterations because there was one space. Two spaces gives you 240. The error scales predictably, which makes the cause easy to guess.
When your working folder sits under cloud sync, "unreadable" has several plausible causes and diagnosis gets muddy. I covered the not-yet-materialized case in Why Cowork's bash says the file is missing when Finder clearly shows it. The two problems look alike from the outside. The distinguishing test is whether cat opens the file individually. If it does, stop suspecting the file and start reading the loop.
What to check once you notice
By the time you spot the miss, a destructive operation may already have run. This is the order I work through.
Count the iterations. Add a counter and re-run. If the count is an integer multiple of the real file count, splitting is your cause — 2x means one space, 3x means two. If it matches exactly, look elsewhere
Test whether the leading fragment exists. Run ls -d "${ROOT%% *}" to resolve the string truncated at the first space. If something is there, everything beneath it is a collateral candidate
Look at modification times underneath it.find "${ROOT%% *}" -newermt "-2 hours" -ls surfaces anything touched recently. An empty result means no real damage was done
Only after step 3 can you call it "merely unreadable." I nearly stopped after step 1 and told myself it was fine.
One thing to do tomorrow morning
Open one automation script and run grep -n 'in \$(find' *.sh. If anything comes back, that line is today's work.
Rewriting it is a straight swap to while IFS= read -r -d ''. But I would add the count assertion first. Idioms shift as environments change; the question "did I process as many as I expected?" holds its shape across every language and every tool.
Until I hit this, I assumed quoting was enough. It turns out the practices I trust most are exactly the ones whose limits I have never measured. 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.