Skip to content

v1.10.8 ·

The hook that could not say it was broken: fixing a silent failure without opening a security hole

Cost capture was dark, and nothing said so. The hook command was two absolute paths written once at install time, one to the node binary and one to the installed CLI. A renamed account, a new laptop or a runtime upgrade broke either of them, and from then on the hook could not start. 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.

The failure was already understood. A repair diagnostic found the broken hook, named the missing home directory and the outdated node version, and printed a warning on every update. It was deliberately report-only, because a diagnostic should not rewrite your editor configuration. That reasoning held. The data was lost anyway, because the last link was a person reading a line of output and acting on it.

So the question was not how to detect a stale path. It was how to stop having one.

Why not just use PATH

The obvious fix is to look node up on PATH at run time. It resolves on every machine, and it is wrong. This command runs automatically at the start and end of every coding session. Resolving an executable from an inherited PATH means whoever controls that variable controls what runs, with your privileges. The frozen absolute path was the only reason that was not already possible. Fixing silent data loss by opening a code execution hole is a poor trade, and an easy one to make, because the security property had no name and no test.

I weighed three approaches. An npx one-liner has nowhere to write when it fails. A checked-in launcher has the same problem. A generated shell shim at a fixed place in the project was the only one that can append a line to the capture log saying why it could not run. I picked it for that, not for elegance.

The shim never consults PATH for the runtime. It tries an explicit override, then the path recorded at install, then a short list of standard locations, then version-manager directories by glob, which is how it survives the upgrade that broke the old command. Each candidate must be absolute, executable and outside any world-writable directory. It sets its own PATH, so even the small utilities it uses for those checks cannot be swapped. It finds the CLI relative to the node it just resolved.

What changed around it

Both the claude-code and kiro installers now generate the shim. aidlc doctor, and the doctor pass inside aidlc update, detect the old form and reinstall it. That reverses the old diagnostic's report-only stance, so I deleted the diagnostic and its helper. The repair only touches hook entries carrying this project's own marker, so nothing you wrote by hand is touched. Removing the helper breaks an internal export, HookStatus.commandResolvable, now HookStatus.legacy. It is not public API.

aidlc status no longer prints $0.00 for an instance with no cost data. Here that changed four instances from a confident $0.00 to -. aidlc cost now says an absent total means capture never ran, not that the work was free. And .aidlc/cost/unattributed.ndjson gets merge=union, so concurrent worktrees stop conflicting on it.

What it does not fix

This stops the leak. It does not refill the tank. Restoring capture does not attribute usage that was never recorded, and the thousands of orphaned records already on disk are a separate problem with a separate risk.