Flow
The setup check, step by step
1Look for the name
Finds the program on the PATH. Any copy passes, even an old one.
2Install globally
npm install with a spinner and a long timeout.
3Failure
Prints the manual command and two tips. npm's own error is dropped.
4Success
Setup goes on. Nothing looks for the program again first.
5Write hooks
Agent config now runs the program by name.
Slowest way it can run is how I was telling people to start my CLI.
The CLI belongs to a small tool I'm building that watches coding agents. Setup adds hooks to each agent. A hook is a command the agent runs when something happens, like a tool call, so my program gets started many times in one session. Nobody sees those runs. They just feel a slow agent, and a slow hook gets deleted.
My onboarding page told people to run everything through npx. That's fine for trying a package once. But in a hook, every single call has to find the package and start it up before my code even begins. The change's notes call that a noticeable fraction of a second each time. I never timed it myself, but it adds up fast.
One check before setup starts
Now both setup commands start with the same check: is my program already on the PATH? If yes, keep going. If not, run a global npm install with a spinner and a long timeout. If the install works, setup continues. If it fails, setup prints the command to install by hand, the command to rerun, and two ways around a permissions error, then stops before writing any config.
The onboarding page changed too. It now says to install globally first, and the per-agent commands skip npx.
I'd guess that without this check, setup could look fine and still leave hooks that break later, when nobody is watching.
Where the reason goes missing
The global install is the step most likely to fail. And my install helper throws away npm's error. Its output goes into memory, and any failure just becomes false. A comment says the output is shown to the user, but it isn't. So the person is told it failed and gets two generic tips, never the real cause. What I want is closer to this:
return { ok: false, reason: lastLinesOf(npmError) };
The check also only looks for the name, so an old copy passes. And nothing looks again after installing, so a success message can still leave hooks pointing at a command that isn't there.