/** * Which FILES a shell command hands to a program to execute β€” the parser-backed * answer to "is the guard this hook names even on disk?". * * πŸ”΄ WHY THIS EXISTS, and it is the loudest lie the sweep could tell. A hook * registered as `python3 .claude/hooks/guard.py` whose script is not there does * not fail in a way anyone can see: `python3` exits **2** on a file it cannot * open, and 2 is Claude Code's DENY code. So `decideHook` reads a block, and * `experimental_verifyPluginGuards` reports the hook as blocking every disaster * in the battery β€” a perfect score for a guard that does not exist. Measured on * this repo's own build: * * ``` * hook `python3 .claude/hooks/guard.py`, script absent * β†’ MEASURED blocks=7/7 exits=2,2,2,2,2,2,2 * ``` * * No malformed config is needed to reach it β€” a relative script path is the * commonest shape in the wild β€” which makes it strictly worse than the * uncompilable-matcher lie fixed alongside it, and it is the same false * confidence the product sells against, produced by the product. * * THE EXIT CODE CANNOT DECIDE IT, and that is not a guess: `run-script.ts` * already writes down the same fact for its own coverage attribution β€” "`sh * ` exits **2** under dash, which is Claude Code's BLOCK code β€” * indistinguishable from a gate legitimately denying, so it cannot be encoded". * The shell's own 126/127 catch a program that never launched; nothing catches a * program that launched and could not find its script. That is what this module * is for, and why the two halves are both needed rather than either alone. * * ## What counts as a file reference (MEASURED, not assumed) * * A word is only reported when it is UNAMBIGUOUSLY a script the command runs: * fully resolved (no unexpanded `$`, no substitution, no glob, no whitespace), * not a flag, not a `NAME=value`, not a URL, not the inline program text of a * `-c`/`-e`, and either the head is a known interpreter or the word carries a * script extension UNDER A HEAD THAT COULD EXECUTE IT. The OR is what keeps * either list from being load-bearing on its own: an interpreter missing from * the table is still caught by the extension, and an extensionless script is * still caught by the interpreter. * * The `UNDER A HEAD` clause is the correction: an extension is evidence about * the FILE, not about the command, so on its own it read `rm -f /tmp/stale.sh` * β€” an ordinary cleanup hook whose file is meant to be absent β€” as a missing * script. {@link DATA_ONLY_HEADS} is the set of heads that never execute an * operand, and it silences the extension branch for them. * * The narrowing is measured, not taste. Over the 107 hook registrations in * `davila7/claude-code-templates` (the corpus the defect was found in): * * | rule | hits | wrong | * | ------------------------------------------- | ---: | ----: | * | any path-shaped operand | 31 | 8 | * | interpreter head OR script extension | 23 | 0 | * * The eight the wide rule got wrong are files a hook WRITES or later reads * (`rm ~/.claude/session_start.tmp`, `mv ~/.claude/performance.csv`) and one * outright absurdity (`echo N/A`, which contains a slash). Reporting those would * be crying wolf on hooks that are perfectly fine, and a check read once and * distrusted is a check that is off. * * ⚠️ THE `wrong: 0` ROW IS WHAT THE `DATA_ONLY_HEADS` CLAUSE CORRECTS, and the * two measurements are of different corpora β€” say so rather than quoting one * number. `rm -f /tmp/stale.sh` is exactly the eighth shape above wearing a * script extension, and it was not in the pinned 107. Re-measured 2026-09-08 * against today's `davila7/claude-code-templates` (134 registrations, 120 * distinct commands β€” the repo has moved since the pin, so this is a second * sample, not a reproduction of the first): * * | rule | hits | lost to the clause | * | ------------------------------------------- | ---: | -----------------: | * | interpreter head OR script extension | 5 | 0 | * | …with the `DATA_ONLY_HEADS` clause | 5 | 0 | * * All five are true positives reached by an INTERPRETER head or a head that is * itself a path, so the extension-under-an-unknown-head branch contributes zero * hits on this sample and narrowing it costs nothing measurable. That is the * evidence the clause does not trade a real detection for a false-positive fix; * it is not evidence the branch is dead, which is why it is kept. * * CONSERVATIVE IN THE OTHER DIRECTION THAN {@link shellVarReads}, on purpose. * Over-reporting a missing FILE costs a real guard its measurement AND accuses * its author of shipping a broken hook, so here silence is the safe error: a * word this module cannot fully resolve is skipped, never guessed at. The * missing-program half of the same question is answered after the fact by the * shell's 126/127, so a false negative here is not the last line of defence. */ /** What a command would execute, as far as this module can resolve it. */ export interface CommandFileRefs { /** * Files the command hands to a program to run, in first-seen order, exactly * as the shell would spell them (relative stays relative β€” the caller resolves * against the cwd the hook will run in). */ readonly refs: readonly string[]; /** Whether the shell parser accepted the command. `false` β‡’ `refs` is empty. */ readonly parsed: boolean; } /** * The files `command` would hand to a program to execute. * * @param command - the shell command, exactly as the hook registers it. * @param values - variables whose value is known at run time, for expansion. A * word naming anything absent from this map is skipped, not guessed. */ export declare function commandFileRefs(command: string, values?: Readonly>): CommandFileRefs; //# sourceMappingURL=command-files.d.ts.map