/** * Cost-capture shim: resolves the node runtime and the CLI at RUN time. * * Why this exists (cost-capture-goes-dark). Hook commands used to be written as * two absolute paths frozen at install time — `"" "" cost record …`. * A username change, a new machine, or an ordinary `nvm` upgrade invalidated * both, the hook could not execute, and **nothing reported that**: no ledger, no * capture.log, and `aidlc cost` read `$0.00` forever. A hook that cannot start * cannot report that it did not start. This repo's own hooks named * `/Users/rodrigo` and node v24.9.0 long after the account and runtime moved on. * * The shim is the only design considered that can report its own failure — an * `npx` one-liner resolves at run time but has nowhere to write when it fails. * That property, not elegance, is why a generated file won. * * SECURITY (cost-capture-goes-dark/AC-15). The naive fix — resolve `node` from * `PATH` — would be a regression. This command runs automatically on every * session start and end, so an attacker-influenced `PATH` would execute * arbitrary code with the developer's privileges. The frozen absolute path, * whatever else was wrong with it, did not have that weakness. So the shim: * * - never performs a bare `command -v node` / PATH lookup; * - only ever execs an ABSOLUTE candidate; * - rejects any candidate whose directory is world-writable (unless sticky); * - derives the global CLI location from the *resolved* node rather than the * install-time node, which is what lets it survive an nvm upgrade. * * Candidate order is deliberate: an explicit operator override first, then the * project-local install (deterministic and project-scoped), then the recorded * install-time hint, then a narrow set of standard absolute locations. */ /** Path of the generated shim. */ export declare function shimPath(projectRoot: string): string; /** Why the generated shim cannot run. */ export type ShimProblem = 'missing' | 'not-a-file' | 'not-executable'; /** * Whether the generated shim can actually execute — null when it can. * * This is the existence check that `cost-capture-goes-dark` deleted along with * `commandResolvable`, restored at the one place it still matters. That deletion * reasoned that once the hook invokes the shim "there is no path to go stale". * True of the *hook command*, and it does not follow for the *shim*: the shim is * generated, gitignored, and no tracked file can supply it, so a fresh clone or a * second machine has armed hooks pointing at nothing. That is exactly what * happened on 2026-08-28 — capture was dead for 41 hours while * `aidlc cost hooks status` reported `installed`. * * The lesson, recorded here because it is where it was paid for: **deleting a * detector needs its replacement verified to exist.** A hook that cannot start * cannot report that it did not start, so the check has to live outside it. */ export declare function shimProblem(projectRoot: string): ShimProblem | null; /** One-line, user-facing rendering of a shim problem. */ export declare function describeShimProblem(problem: ShimProblem): string; /** * The shim body. `nodeHint`/`cliHint` are the absolute paths known at install * time; they are *hints*, validated before use and never trusted to still exist. */ export declare function shimBody(nodeHint: string, cliHint: string): string; /** * Write (or rewrite) the shim and make it executable. Idempotent: the body is a * pure function of the two hints, so re-installing produces identical bytes * unless the environment genuinely moved. */ export declare function writeShim(projectRoot: string, nodeHint: string, cliHint: string): string; /** * How a hook command should invoke the shim. * * `$CLAUDE_PROJECT_DIR` is set by Claude Code; the `:-.` fallback covers hosts * that do not set it and run hooks from the project root. The path is relative * to a variable rather than absolute precisely so the command text carries no * machine-specific path (AC-1) — that is the whole point. */ export declare function shimInvocation(rootVar?: string): string; //# sourceMappingURL=shim.d.ts.map