The guard that guarded the guard
posttool/write/stale-import-detector runs after every Write and Edit and is named in this repo's own rules as the thing that catches a rename's stale importers. Measured on 2026-08-29 while wiring asyncRewake: it had never once found one. Type a pattern and watch the shell guard decide.
1. Why every call returned nothing
SAFE_ARG_RE = /^[a-zA-Z0-9._/~=:@-]+$/ is the right guard for a path handed to a shell string. A regex is not a path: it needs (, |, \, *. The assertion threw on every call, the catch returned [], and the hook answered {"continue":true,"suppressOutput":true} for every Write since the guard was added.
2. Why the tests were green
The unit test mocks child_process, so grep never runs and the assertion in front of it is never exercised with the real pattern. The mock returns the references the test wants; the block path is proven against a shell that does not exist. A second, quieter defect hid behind the first: CC hands hooks CLAUDE_PROJECT_DIR as a realpath (/private/var/... on macOS) while tool_input.file_path keeps the caller's spelling (/var/...), so the same-subtree check compared two spellings of one directory.
3. The fix, and the test that would have failed on day one
| piece | before | after |
|---|---|---|
| grep call | one shell string, regex through assertSafeShellArg | execFileSync('grep', [args]): an argument vector, no shell, the pattern is data |
| paths | project dir from env, file from the tool input, as given | both through realpathSync when they exist |
| tests | one file, shell mocked | plus stale-import-detector.real-grep.test.ts: real grep on a temp fixture with three same-subtree importers must block; one importer must stay silent |
| through CC | hook fires, 2 ms, silent | with #3800's asyncRewake wiring: the block reaches the next request as a system reminder |
Class: a fix present and never reached, and a test that measures a mock. The verdict a rewake can carry is only as good as the verifier that establishes it; this one established none.