fix: eliminate 0-byte ghost files from shell-redirect misinterpretation#8
Conversation
Root cause: c3's output filter wrote its savings header as "raw->Ntok". The literal "->" was re-read by a shell as "> Ntok", creating an empty file named after the token count (e.g. 110tok). The same class produced "main" (git's "ref -> ref"), "str"/"dict" (Python "-> Type" hints), and "=x.y.z" (pip ">="). It was NOT caused by "&". - cli/tools/filter.py: header now uses the Unicode arrow instead of "->", so c3 never emits a shell-redirect metacharacter in its own output. - cli/hook_ghost_files.py: cleanup now triggers after c3_shell / c3_read / Read, not just Bash, so ghosts from any tool's output get swept. - cli/c3.py (install-mcp): register hook_ghost_files for those matchers and add a mcp__c3__c3_shell block. Verified: the broadened hook deletes a synthetic ghost on a c3_shell payload, and the filter header no longer contains ">". Re-run `c3 install-mcp` and restart the MCP server to activate the hook + filter changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Eliminates creation of 0-byte “ghost” files in the project root caused by shell redirection metacharacters appearing in tool output and being misinterpreted downstream. This change reduces accidental filesystem pollution and broadens the existing cleanup hook so it runs after more tool types (not just native shell runs).
Changes:
- Update the output-filter savings header to use
→instead of->, avoiding>redirection metacharacters in emitted output. - Expand the ghost-file cleanup hook trigger set to also run after
c3_shell,c3_read, and native read tools. - Register the ghost-file hook for additional matchers in
install-mcp, including a newmcp__c3__c3_shellmatcher, and document the fix in the changelog.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
cli/tools/filter.py |
Replaces raw->Ntok style header formatting with a → glyph to prevent shell redirect misinterpretation. |
cli/hook_ghost_files.py |
Adds a shared trigger-tool allowlist so ghost cleanup runs after more tool invocations. |
cli/c3.py |
Extends install-mcp hook registration to run ghost cleanup after Read, c3_read, and c3_shell. |
CHANGELOG.md |
Records the ghost-file fix and notes the need to re-run c3 install-mcp. |
| # Tools whose output can carry shell-meta text that leaks into 0-byte files: | ||
| # native shells, c3_shell (its `N->Mtok` filter header), and file reads whose | ||
| # content has `-> Type` hints. A downstream shell sees `> word` and creates an | ||
| # empty file named `word`. |
|
Added commit b345f80 — a more impactful fix uncovered while investigating: no c3 hook was launching on Windows at all. The generated hook commands used a bare `cmd /c` prefix, but Git Bash (Claude Code's Windows hook executor) doesn't resolve bare `cmd` on PATH, so enforcement / c3-signal / output-filter / ghost-cleanup all silently failed to start (that's also why `.c3/last_c3_call.json` was never written). Fix: `cmd.exe /c`. Verified under bash — the hook then runs and writes its signal file. NB: this is what made the ghost cleanup dormant; the filter `->`→`→` change in e861345 is the part that stops c3 creating ghosts regardless. Re-run `c3 install-mcp` + restart to activate. |
fix(windows): c3 hooks never launched (cmd to cmd.exe) — completes PR #8
The bug
0-byte "ghost" files (e.g.
110tok,69tok,main,str) kept appearing in the project root. Despite the suspicion, it is not caused by&.Root cause: text containing
-> wordreaches a shell, which parses> wordas a redirection and creates an empty file namedword. The biggest source was c3's own output filter, which formats its savings header asraw->Ntok— the literal->becomes> Ntok.Reproduced live: a
git logcommand (no->in its own output) was filtered with header818->69tok, and a 0-byte69tokimmediately appeared. Other instances of the same mechanism: git'sref -> ref→main, Python-> Typehints in read files →str/dict, pip>=x→=x.The fix
cli/tools/filter.py— the filter header now uses→instead of->, so c3 never emits a shell-redirect metacharacter in its own output (eliminates theNtokfamily entirely).cli/hook_ghost_files.py— the cleanup hook previously only ran afterBash. It now also runs aftermcp__c3__c3_shell,mcp__c3__c3_read, andRead, so ghosts from any tool's output (git refs, type hints, pip specifiers) are swept regardless of source.cli/c3.py(install-mcp) — registershook_ghost_fileson theReadandc3_readmatchers and adds a newc3_shellmatcher block.Verified
c3_shellPostToolUse payload (clean UTF-8 stdin):[c3:ghost-cleanup] Deleted 2 ghost file(s)….>.ruffclean;test_ghost_files.py+test_cli_smoke.pypass (19).Activation
Logged under
[Unreleased]. After merge, re-runc3 install-mcp(to register the new hook matchers) and restart the MCP server (to pick up the filter change). Happy to cut a2.32.3release once merged.🤖 Generated with Claude Code