← All posts

The Glob That Ate the Rest of My File

A zero-match zsh glob silently aborted half my shell-init.sh — and a lint pass would never have caught it.

  • zsh
  • shell
  • agents
  • claude-code
  • testing

An auto-review agent flagged a one-line change on a claude-config PR, and I almost dismissed it. The PR refactored how my agent fleet discovers which accounts it can use — a roster of credential files sitting on disk, so each box knows which Claude Code logins it’s allowed to rotate through. The loop that reads them looked completely fine:

for f in "$BASE"/credentials-*.json; do
  [[ -f "$f" ]] || continue
  # ...load the account
done

Here’s the setup for anyone who doesn’t live in a terminal: shell-init.sh is a file that runs every time a shell starts, defining functions and shortcuts I use all day. A glob is a wildcard — credentials-*.json means “every file that looks like this.” And the loop above has a guard, [[ -f "$f" ]] || continue, that’s supposed to skip anything that isn’t a real file. So if there are zero credential files, the loop just does nothing. Right?

Wrong, under zsh, and only on some machines.

zsh has a default called NOMATCH. When a glob matches no files at all, zsh doesn’t hand the loop an empty list — it throws an error at the moment it tries to expand the wildcard. That’s before the loop body runs. So my clever [[ -f "$f" ]] guard never even got a chance to fire. And because the file was being sourced — loaded into the running shell — that error aborted the entire source. Every function and alias defined after that loop silently never loaded.

The reason I’d never seen it: on my machines, credential files existed, so the glob matched and everything worked. bash also hides the bug — when a bash glob matches nothing, it leaves the literal string credentials-*.json sitting there, the guard catches it, life goes on. It only bites on a box running zsh with no matching files. Which is exactly the fresh-provisioned state a new fleet node starts in.

The fix is one line: tell the shell an empty glob is fine.

The fix and the test that actually proves it give me the detail

Set null-glob behavior locally so it doesn’t leak into the rest of the sourced environment:

# zsh
setopt local_options null_glob
for f in "$BASE"/credentials-*.json; do
  [[ -f "$f" ]] || continue
  load_account "$f"
done
# bash equivalent
shopt -s nullglob

The part I want to underline: shellcheck passed this file the whole time. A linter reads syntax; it does not know your runtime’s NOMATCH policy or which box has zero files. The only thing that catches this is sourcing the file in a real subshell against a zero-file fixture and asserting a function defined after the loop still exists:

BASE=$(mktemp -d)  # empty on purpose
zsh -c "source ./shell-init.sh; typeset -f my_late_function >/dev/null" \
  && echo PASS || echo "FAIL: tail of file never loaded"

Test the empty directory, not the populated one.

What still bugs me is how quiet it was. No error surfaced to me — the shell just came up missing half its tools, and I’d have blamed something else entirely. The general lesson I took: when a file’s job is to discover things that might not be there yet, the zero-case is the primary case, not the edge case. Write the test for nothing before you write the test for something.

And keep the review agent that reads your happy-path code and asks about the empty directory. Mine earned its keep on one line.