[BUG] macOS native build: shell-snapshot grep/find/rg shadowing kills bash mid-command (silent truncation); false 'temp filesystem is full (0MB free)' ENOSPC banner from broken statfs (bsize=0)
Everything below was reproduced live on 2026-06-11 on this machine; decompiled snippets are from the installed 2.1.173 binary. Two distinct bugs are reported together because they compound into one extremely misleading failure mode; happy to split them if preferred.
Environment
- Claude Code 2.1.173 (native build, official installer,
~/.local/share/claude/versions/2.1.173); the symptom appears in transcripts across 30+ sessions / 9 projects since 2026-05-29 - macOS Darwin 24.6.0, APFS, 469 GB free (1.8 TB volume), no quotas, inodes plentiful
- Shell: bash;
CLAUDE_CODE_TMPDIR=$HOME/.claude/tmp(set while chasing this; identical with the default/tmproot)
TL;DR
- Bug A — false ENOSPC banner. Any Bash call with empty stdout + non-zero exit is reported as
Command output was lost: the temp filesystem at …/tasks is full (0MB free)… ENOSPC. The "0MB" comes from a brokenfs.statfson macOS (bsize = 0), so the message fires on a volume with 469 GB free.sh -c 'exit 7'reproduces it 100%. - Bug B — shell-snapshot grep/find/rg shadowing kills bash mid-command. The snapshot's
grep/find/rgfunctions re-exec the Claude binary as embedded ugrep/bfs/ripgrep; when such a call appears mid-command, the parent bash dies at (or immediately after) that call — later statements never execute, and partial output is rendered as success. A no-matchgrepthen yields empty stdout + exit 1, which triggers Bug A's banner — so users (and Claude itself) chase phantom disk-space problems while the real issue is silent command truncation.
---
Bug A: false "temp filesystem is full (0MB free)" ENOSPC banner
Repro (100% deterministic)
Ask Claude to run any command with no stdout and a non-zero exit:
sh -c 'exit 7'
Result:
Exit code 7 Command output was lost: the temp filesystem at /Users/josh/.claude/tmp/claude-501/-Users-josh-Desktop-workspace-Conjuguer/<session>/tasks is full (0MB free). The child process's stdout/stderr writes failed with ENOSPC. Free up space or set CLAUDE_CODE_TMPDIR to a directory on a filesystem with room.
A bare no-match grep pattern file (exit 1, no stdout) does the same. df -h reports 469 GB free; the tasks dir exists, is writable, and contains kilobytes.
Why it says 0MB: the runtime's statfs is broken on macOS
Decompiled check from the 2.1.173 binary (runs when stdout-to-file capture is empty and exit ∉ {0, 137}):
let _ = dirname(H), A = await fs.statfs(_, {bigint:true}),
q = A.bavail * A.bsize / (1024n*1024n); // "free MB"
if (q < 0n) return null; // (the #63877 guard)
if (q < 10n) return `Command output was lost: the temp filesystem at ${_} is full (${q}MB free)...`;
if (A.files > 0n && A.ffree < 1000n) return `...out of inodes...`;
Bun's fs.statfsSync on macOS returns a field-shifted struct — observed with standalone bun 1.3.9 on this machine (and the always-"0MB" wording implies the bundled runtime matches):
$ bun -e 'console.log(require("fs").statfsSync(process.env.HOME + "/.claude/tmp", {bigint:true}))'
{ type: 1n, bsize: 0n, blocks: 4096n, bfree: 1048576n, bavail: 488475719n,
files: 122871777n, ffree: 122871777n }
bsize is 0 (the real bsize, 4096, appears in blocks), so bavail * bsize = 0 → every macOS machine computes "0MB free" whenever this code path runs. The inode branch never fires because the shifted fields dodge it. The harness thus asserts "writes failed with ENOSPC" without ever having observed an ENOSPC errno.
Impact
- Every quiet non-zero command (no-match grep being the everyday case) renders as a scary disk-full error.
- Users and Claude itself chase disk space. On this machine, weeks of cargo-cult workarounds accumulated (CLAUDE.md instructions to
rm -rfcapture dirs — which can delete live capture files of concurrent sessions and make things worse).
Related issues
- #63877 — same preflight, 32-bit-truncation negative variant on >17.6 TB filesystems (now guarded by
q < 0n; theq == 0bsize-bug variant remains) - #65086 — exact symptom, closed as duplicate
- #63909 — macOS "ENOSPC despite free space", open; likely this family
Suggested fix
Don't infer ENOSPC from arithmetic on statfs: fix the runtime's macOS statfs binding (field offsets), and/or treat bsize == 0 / inconsistent structs as "unknown" the same way q < 0n is treated. Ideally only claim ENOSPC when an actual ENOSPC errno was observed; "no stdout + non-zero exit" is normal behavior for many commands.
---
Bug B: snapshot-shadowed grep/find/rg can kill bash mid-command
Background
Since ~v2.1.117 the shell snapshot (sourced before every Bash tool call) shadows tools with functions that re-exec the Claude binary as embedded tools, e.g.:
function grep {
...
elif [[ $BASHPID != $$ ]]; then
exec -a ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@"
else
(exec -a ugrep "$_cc_bin" -G --ignore-files --hidden -I --exclude-dir=.git ... "$@")
fi
}
(rg is shadowed only when no real ripgrep is on PATH; grep/find are shadowed whenever the native-build gate is on and search tools aren't opted in.)
Repro
In a Claude Code session on this machine (3/3 deterministic that session):
echo S1; grep zzz_no_match_zzz /etc/hosts; echo S2_alive
→ output S1, exit 1, S2_alive never printed. Proof the shell died (not mere output loss): a variant writing to a real file —
grep zzz_no_match_zzz /etc/hosts; echo ALIVE >> ~/probe.txt; echo TAIL; true
→ ~/probe.txt was never created (the >> append never executed), and the call reported exit 1 even though it ends in true. Bug A's banner decorated the result.
The success path kills too, one statement later:
grep localhost /etc/hosts > /dev/null; echo stdout_probe; echo rc=$? >> ~/probe2.txt; true
→ rendered "(Bash completed with no output)"; probe2.txt never created.
Control experiments (same session)
| Command shape | Outcome |
|---|---|
| command grep <no-match>; echo X | X prints, rc=1 visible — fine |
| unset -f grep find rg; grep <no-match>; echo X | fine |
| shadowed grep with a match, nothing after | fine (matches print) |
| (exec -a ugrep "$CLAUDE_CODE_EXECPATH" -G ... <no-match> /etc/hosts); echo X directly | survived — so the embedded tool's exit alone isn't sufficient; function context / timing matters |
Real-session field data (one documentation-only session, ~25 Bash calls)
- 6 calls lost output; every truncated call contained a no-match/non-zero shadowed invocation; every fully-intact call had all-zero exits. Read/Write/Edit unaffected.
- A compound command redirecting all work to
~/out.txt(12 sections) left the file truncated after section 1 — execution stopped; not just capture loss. - Most dangerous presentation: truncation rendered as success — missing grep output is indistinguishable from "no matches", which silently corrupts the model's conclusions downstream.
- Parallel batches of grep-census commands were hit hardest (more no-match greps), which reads as "intermittent" until you notice the exit-code correlation.
Interaction with Bug A
Shell killed at a no-match grep → empty stdout + exit 1 → Bug A's false banner. The two bugs combined send users hunting disk-space ghosts while their commands are being half-executed.
Related issues
- #54394 — embedded ugrep wrapper memory amplification
- #59517 — wrapper forces
-G(BRE), silently breaking-Ecallers - #56751 (mine; closed as stale) — "long-line grep matches silently disappear" is plausibly this same shadowing: the wrapper passes
-I(skip binary) to ugrep, whose binary-detection heuristics differ from BSD grep's, so files with very long lines can be silently skipped instead of reportingBinary file matches.
Workarounds verified on this machine
- Install real ripgrep (
brew install ripgrep) — the snapshot'srgshadow has a runtimecommand -v rgguard, so this disables it even for existing sessions. - Launch with
--allowedTools Grep,Glob— sets the internal search-tools opt-in, which the snapshot builder's gate checks before emitting the grep/find shadows. - Global PreToolUse hook on Bash that prefixes commands with
unset -f grep find rg 2>/dev/null;.
Suggested fix
Investigate why a shadowed-tool child (which is the Claude binary itself, inheriting CLAUDE_CODE_* session env) terminates the parent bash — PID/process-group confusion in the task monitor seems plausible given the exit code reported is the embedded tool's exit, not the chain's. Until then, consider defaulting the shadowing off; per #59517 it also changes grep semantics silently.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗