v1.10.8 ·
The hook that could not say it was broken: fixing a silent failure without opening a security hole
The problem
The previous release described a cost-capture hook that could not run. This one fixes it, and the fix turned out to be more interesting than the defect.
The hook command was two absolute paths, written once when the hook was installed: one to the node binary, one to the installed CLI. Both were correct on the day they were written. A renamed account, a new laptop, or an ordinary runtime upgrade invalidated either of them, and from that moment the hook could not start. Nothing ran. Nothing was logged, because the thing that would have written the log was the thing that failed to launch. Cost reporting stayed empty, and every phase of every instance read as free work.
What makes this worth a post is that the failure was already understood. A repair diagnostic existed, found the broken hook correctly, named the missing home directory and the outdated node version in its own source comments, and printed a warning on every update. It was deliberately report-only, on sound reasoning: a diagnostic has no business silently rewriting a developer's editor configuration. That reasoning held. The data was lost anyway, because the last link in the chain was a person reading a line of output and choosing to act on it.
So the interesting question was not how to detect a stale path. It was how to stop having one.
How it could be solved
The obvious fix is one word: instead of a frozen path to the node binary, look node up on PATH at
run time. It resolves correctly on every machine, forever, and it is wrong.
This command executes automatically at the start and end of every coding session. Resolving an
executable from an inherited PATH means that whoever controls that variable controls what runs, with
the developer's own privileges. The frozen absolute path — the thing being removed as defective — was
the only reason that was not already possible. Fixing a silent-data-loss bug by introducing an
arbitrary-code-execution surface is a poor trade, and it is an easy one to make, because the security
property had no name and nothing tested it.
Three approaches were weighed. An npx one-liner resolves at run time and needs no generated files,
but has nowhere to write when it fails, which reproduces the original sin: a hook that cannot start
cannot report that it did not start. A small checked-in launcher shares that problem in a different
shape. The third option, a generated shell shim at a fixed location inside the project, was chosen for
exactly one reason — it is the only one that can append a line to the capture log explaining why it
could not run. Not elegance. The ability to describe its own failure.
That shim never consults PATH for the runtime. It tries an explicit override, then the path recorded
at install time, then a short list of standard locations, then version-manager directories by glob —
which is what lets it survive the runtime upgrade that broke the old command. Every candidate must be
an absolute path, must be executable, and must not sit in a world-writable directory. It sets its own
PATH rather than inheriting one, so that even the small utilities it uses to make those checks cannot
be substituted. And it locates the CLI relative to the node it just resolved, rather than to the node
that existed at install time.
The second decision was what to do with the diagnostic that had reported this failure faithfully and uselessly for days. With no absolute path left in the command, there is no stale path to detect, so it was deleted — along with the helper that powered it — and replaced by a migration that repairs the old form instead of describing it. That migration applies automatically, which reverses its predecessor's stance. The narrower rule it follows is that it only ever rewrites the hook entries carrying this project's own marker, so nothing a developer wrote by hand is touched.
The last decision was to leave the recovery alone. Restoring capture does not retroactively attribute usage that was never recorded, and the thousands of orphaned records already on disk are a separate problem with a separate risk profile. This release fixes the leak. It does not claim to refill the tank.
How AIDLC solves it
- Cost hooks no longer embed absolute paths. Both the claude-code and kiro installers now generate a capture shim inside the project and invoke that instead; it resolves node and the CLI at run time. A node upgrade, machine move, or username change no longer silently kills capture. (The shim's own path is deliberately not named here: it is generated per-install and gitignored, because its body embeds the install-time paths as hints, so the blog content guard correctly rejects it as a dangling path.)
- Existing installations self-repair.
aidlc doctor(and the doctor pass insideaidlc update) detects a pre-shim hook and re-installs it. No manualaidlc cost hooks installrequired. aidlc statusno longer prints$0.00for instances with no cost data, matchingaidlc costandaidlc metrics. In this repository that changed four instances from a confident$0.00to-.aidlc costexplains an absent total — "no data means capture has never run (<cause>) — not that the work was free" — instead of leaving the reader to guess..aidlc/cost/unattributed.ndjsongainsmerge=union, so concurrent worktrees stop conflicting on the ledger whose purpose is deferred recovery.- Removed: the
stale-cost-hooksdoctor migration and thecommandResolvablehook-status field. Nothing detects a stale path because no stale path can exist.HookStatus.commandResolvableis replaced byHookStatus.legacy— a breaking change to an internal export, not a public API.