One Space in a Path, and Nine Commands Reported Success While Counting the Wrong Place
A single space in a workspace name sends agent-written commands somewhere else, quietly. Measurements across eleven unquoted-path forms, and the entry-point script that closes the boundary in one cd.
I asked an agent to count the articles in my workspace. It came back with 0 files.
There are 977 MDX files in there. Typing the same command by hand returns 977, every time.
No error. The command succeeded. It just counted somewhere else.
The culprit was the workspace name: Dolice Labs. That one space in the middle.
As an indie developer I get to pick where my workspace lives, which is exactly how it ends up somewhere convenient rather than somewhere safe. Mine sits inside a Dropbox sync folder, and I never thought twice about the space in the folder name. Names are for humans to read, and a space reads better.
Handing the keyboard to an agent is what finally sent me the bill for that decision.
The space is in the prefix, not the leaf
The first thing I measured was how much of the tree was even affected.
Same workspace, same files. Change how you count and you get 100% or 0.3%.
The space almost never lives in a filename. It lives in exactly one place — the Dolice Labs segment of the prefix. And every absolute path you build carries that one segment along with it.
A tidily named file buried deep in node_modules picks up a space the instant you address it absolutely.
That was the fork in the road. The problem wasn't the filenames. It was how paths get assembled.
Eleven unquoted forms, and what each one actually returns
An agent writing shell won't reliably type "$WS" with the quotes. So I measured what happens when it writes plain $WS, across eleven forms.
Setting up a workspace with a space in it:
BASE="$HOME/pathprobe"WS="$BASE/Dolice Labs"mkdir -p "$WS/content/articles/ja/tips" "$WS/out"for i in 1 2 3 4 5; do echo "# a$i" > "$WS/content/articles/ja/tips/a$i.mdx"; donemkdir -p "$BASE/sandbox" && cd "$BASE/sandbox"
Five .mdx files. Now touch them without quoting $WS.
Command form
Exit code
Returned
Correct
find $WS -name "*.mdx" | wc -l
0
0
5
ls $WS | wc -l
0
0
4
grep -rl "a1" $WS | wc -l
0
0
1
[ -d $WS ] && echo yes || echo no
0
no
yes
mkdir -p $WS/out/gen
0
created
created in 2 wrong places
for f in $(find "$WS" -name "*.mdx")
0
10 iterations
5 iterations
find "$WS" ... | xargs wc -l
0
0 total
5 total
python3 script.py $WS (argv count)
0
2
1
du -sh $WS
0
12K (of something else)
real size
cat $WS/.../a1.mdx
1
—
# a1
cp $WS/.../a1.mdx ./c.mdx
1
—
copied
Two forms failed out loud: cat and cp. The other nine — 82% of them — returned exit code 0 while handing back the wrong answer.
The failure modes aren't uniform, and the differences matter.
find and grepsucceed with zero results. $WS splits into /path/to/Dolice and Labs, neither exists, so there's nothing to search and the walk finishes normally. The complaint goes to stderr, but pipe it into wc -l and the exit code belongs to wc. The scream never reaches the caller.
[ -d $WS ]says a directory that exists doesn't. Split into two words, [ miscounts its arguments, the test never forms, and it returns false. Any branch keyed on an existence check quietly takes the other road.
for f in $(find "$WS" ...)turns 5 files into 10 iterations. find is quoted here and returns the right 5 paths — then command substitution word-splits each one at the space. The loop runs ten times and grabs a broken path every time. This is the sneakiest form of all: you quoted it, and it broke anyway.
python3 script.py $WSlands 2 items in argv. If the script reads sys.argv[1] and gets on with it, it starts working on /path/to/Dolice in good faith.
And mkdir -p $WS/out/gen returned 0 while creating directories in two places that weren't the workspace:
$ ls -A "$BASE/sandbox" # current directoryLabs$ ls -A "$BASE"Dolice Dolice Labs sandbox
mkdir -p /path/to/Dolice Labs/out/gen parses as two arguments: /path/to/Dolice and Labs/out/gen. The first is absolute, so it lands next to the workspace. The second is relative, so it lands wherever you happened to be standing.
Reads come back empty. Writes land somewhere you weren't looking. That asymmetry defines the whole problem.
An empty read is at worst nothing happening. A write that succeeded in the wrong place means the agent reports "done" and the output exists nowhere you'll think to check. That's the one I hit first.
The absence of an error proved nothing at all.
✦
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
✦Measured across 11 unquoted-path forms: 9 return exit 0 with wrong results (find says 0, for loops 10 times, argv holds 2)
✦Full implementation of ws.sh, an entry point that closes the boundary at a single cd (write probe, exit code 78, exec handoff)
✦Where the symlink alias breaks down: find refuses to follow it, and readlink -f hands the space right back
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.
My first instinct was to make the agent quote things. Put "always double-quote paths" in AGENTS.md, fail SC2086 in CI with shellcheck.
That gets you halfway. Look back at the table, though, and there are places quoting can't reach.
for f in $(find "$WS" ...) broke with the path properly quoted. What's broken there isn't the quoting of $WS — it's the word-splitting applied to the substitution's output. xargs has the same problem: whitespace is its default delimiter.
-print0, -0, and IFS= read -r -d ''. Correct, all three.
The question I kept circling was whether an agent will actually do this every time. Rules work fine for code I type myself. When something is writing dozens of shell snippets a day, rule compliance is your failure rate — and here, non-compliance exits 0. Miss one gate and there's no second chance to notice.
Designs that depend on rules being followed break when rules aren't followed. I don't much like building that way. I wanted to know whether the thing could be made unbreakable by non-compliance instead.
A symlink alias only covers the doorway
The next attempt was a space-free alias:
ln -sfn "$WS" "$BASE/ws"ALIAS="$BASE/ws"
Forget the quotes around $ALIAS and nothing splits. Several forms did come good:
[ -d $ALIAS ] && echo yes || echo no # yes (correct)mkdir -p $ALIAS/out/gen # rc=0, lands in the real workspace
Writes reached the real directory, nothing leaked into the current one. So far so good.
find won't follow a symlink unless you add a trailing slash or -L. Which means teaching the agent to write find -L — a brand new rule to comply with. Right back where I started.
And then this settled it:
readlink -f $ALIAS# /path/to/Dolice Labs
The alias is a doorway, nothing more. Any step that resolves the real path pours the space straight back downstream. Tools that call realpath. Scripts that walk up from __file__. Anything that prints an absolute path in an error message. All of them live outside the alias.
A symlink doesn't remove the space. It drapes one layer over it. Underneath, nothing changed.
Relative paths never had a space to begin with
Stuck, I went back to the first measurement — the one that split 100% and 0.3%.
The space is only in the prefix. It's essentially never in the filenames.
Strip the prefix and there's no space left. Stand inside the workspace with cd, and every relative path from there is clean.
cd "$WS"find . -name '*.mdx' | wc -l # 5for f in $(find . -name '*.mdx'); do echo x; done | wc -l # 5find . -name '*.mdx' | xargs wc -l | tail -1 # 5 total
The three forms that returned 0, 10, and 0 now all return 5. No quoting. No -print0.
The reason is unremarkable: ./content/articles/ja/tips/a1.mdx contains no space. There's nothing to split on.
So instead of demanding the agent quote correctly, give it a world with no spaces in it. That's the version that survives non-compliance.
Exactly one line touches a path with a space in it — the cd. And that line lives in one file I wrote.
ws.sh, the one way in
Here's the script that closes the boundary. It lives at bin/ws.sh in the workspace.
#!/usr/bin/env bash# ws.sh — the only entry point into a workspace whose path contains a space.# This file is the only thing that touches an absolute path.# Everything it calls lives in a world of relative paths.set -euo pipefail# 1) Resolve the real location exactly once. Never rebuild WS_ROOT downstream.WS_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"# 2) Decide writability by actually writing, not by testing existence._probe() { local t="${WS_ROOT}/.ws-probe.$$" if ( : >"$t" ) 2>/dev/null; then rm -f -- "$t"; return 0; fi return 1}if ! _probe; then printf 'ws.sh: cannot write to: %s\n' "$WS_ROOT" >&2 exit 78 # EX_CONFIG — a dedicated code so "blocked" never reads as "zero results"fi# 3) Close the boundary here. Relative paths from now on contain no spaces.cd -- "$WS_ROOT"# 4) Hand it to children via the environment, never through argument splitting.export WS_ROOTexport LC_ALL="${LC_ALL:-C.UTF-8}"if [ "$#" -eq 0 ]; then printf 'usage: bin/ws.sh <command> [args...]\n' >&2 exit 64fiexec -- "$@"
A few lines are worth explaining.
WS_ROOT derives from the script's own location so that callers never pass a path in. Accept a path as an argument and you've reopened the word-splitting hole. Deriving it from where the file sits means no space-bearing string can enter from outside.
cd -- "$(dirname ...)" && pwd -P stands in for readlink -f. pwd -P resolves symlinks to the physical directory, so even if bin/ws.sh gets invoked through an alias, it lands on the real thing.
The -- markers keep a leading-hyphen path from being read as an option. Different problem from spaces, but an entry point that handles paths may as well close both.
Then exec -- "$@". The argument array passes through intact — no splitting here either. exec replaces the process so exit codes and signals pass through cleanly; a lingering wrapper changes how Ctrl+C behaves and how exit codes propagate.
Testing writability instead of existence
One note on _probe.
[ -d "$WS_ROOT" ] looks sufficient. But readable-yet-unwritable is an ordinary state — permissions shift on a synced folder, ownership changes to another user. The existence check returns true and the write fails one step later.
Exit code 78 is deliberate. Exit 1 is indistinguishable from "the command failed." A dedicated config-error code lets the caller tell "zero results" apart from "never got in the door."
Verified against a read-only workspace:
$ chmod 555 "$RO_WS"$ "$RO_WS/bin/ws.sh" bash -c 'echo should_not_run'ws.sh: cannot write to: /path/to/Read Only WS$ echo $?78
should_not_run never ran. Stopped at 78.
Three lines of rules for the agent
This is the whole of what went into AGENTS.md:
## Running commands inside the workspace- Always run shell commands through `bin/ws.sh <command>`- Write every path as a relative path (never assemble an absolute path)- Output paths are relative too (e.g. `out/report/`)
Shorter rules get followed more often. Compared to "quote everything, use -print0 and -0, and never feed command substitution to a for loop," there's one thing to remember now.
The behaviour on non-compliance changed too. If the agent forgets bin/ws.sh and types a bare find . -name '*.mdx', the working directory is wrong and it gets 0 results — the same quiet failure as before. What it no longer does is assemble an absolute path and write into somewhere else entirely.
Put precisely: I moved the failure mode from "quietly wrong" to "quietly does nothing." The accidents got a grade cheaper.
Rules and implementation weren't enough. You get runs that look like they went through the entry point and didn't.
So there's a suite, written entirely in relative paths:
#!/usr/bin/env bash# tests/path_contract.sh — verify the entry point the same way every timeset -uo pipefailfail=0chk() { if [ "$2" = "$3" ]; then printf ' ok %-28s %s\n' "$1" "$3" else printf ' FAIL %-28s expected=%s actual=%s\n' "$1" "$2" "$3"; fail=1; fi}EXPECT_MDX="$1"chk "cwd is WS_ROOT" "$(pwd -P)" "${WS_ROOT:-unset}"chk "relative find" "$EXPECT_MDX" "$(find . -name '*.mdx' | wc -l | tr -d ' ')"chk "unquoted for" "$EXPECT_MDX" "$(for f in $(find . -name '*.mdx'); do echo x; done | wc -l | tr -d ' ')"chk "xargs (whitespace)" "$EXPECT_MDX" "$(find . -name '*.mdx' | xargs -r wc -l 2>/dev/null | tail -1 | awk '{print $1}')"chk "no debris in cwd" "0" "$(ls -A | grep -cx 'Labs' || true)"exit "$fail"
The unquoted for and the bare xargs are in there on purpose. The entire point of this entry point is that the sloppy forms work. Testing only the correct forms tells you nothing about whether the doorway is doing anything.
The cwd is WS_ROOT line fails whenever something runs without going through ws.sh.
$ cd /somewhere/else$ "$WS/bin/ws.sh" bash tests/path_contract.sh 5 ok cwd is WS_ROOT /path/to/Dolice Labs ok relative find 5 ok unquoted for 5 ok xargs (whitespace) 5 ok no debris in cwd 0$ echo $?0
All green. The unquoted for returns 5.
The suite runs inside my unattended schedules too, so a broken entry point announces itself before the run rather than at three in the morning.
What this design does not cover
Being straight about the edges.
Nothing outside the workspace.cd closes the inside only. If the agent needs to touch something like /Users/name/Library/Application Support/..., that's an absolute path with a space again. My own excursions outside were few — an indie project touches fewer machines than a team one — so those paths go into environment variables inside ws.sh — which at least makes the references countable. Countable means I can check the quoting at each one.
Error messages still print absolute paths. No way around it. Tools that echo pwd -P, runtimes that put file paths in stack traces. If the agent copies that message and builds its next command from it, the space walks back in. This happened to me once. The fix was a rule — "don't reuse paths lifted from error output" — which is squarely a compliance-dependent design. That's the one place I've compromised.
Spaces in filenames are a separate problem. 5 of 1,816 in my measurement. Small, not zero. Relative paths don't save ./out/report dir/x.txt. That's what -print0 and -0 are for, and no entry point fixes it. I settled on a naming convention for output paths that sticks to hyphens.
Windows is untested. These measurements are from a Linux environment. Drive letters, backslashes, and crossing the WSL2 boundary all behave differently. File watching under WSL2 has its own traps, which I touched on in what to do when file watching slows to a crawl under WSL2.
Renaming the folder would also just work. Rename Dolice Labs to dolice-labs and most of this article evaporates. I didn't. Chasing down every reference scattered across Dropbox share links, iOS app bookmarks, and four sites' worth of config seemed like more work than writing one ws.sh. That trade goes the other way in plenty of setups. With few references so far, renaming is the surer bet.
The one thing worth checking
If your workspace path has a space in it, run one line:
[ -d $WS ] && echo yes || echo no
If that says no, you're living in this article. Your agent's branches are treating something that exists as missing.
Seeing the same files split into 100% and 0.3% depending on how I counted is what finally showed me the shape of it. Hunting for errors turned up nothing, because there weren't any. Doubting the returned values themselves is what exposed nine forms lying to me.
I hope some of this proves useful in your own setup.
Share
Thank You for Reading
Antigravity 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.