Skip to content

fix: hooks were dead on a freshly installed plugin (0.50.2) - #63

Merged
Shahinyanm merged 1 commit into
masterfrom
fix/plugin-dist-bootstrap
Jul 26, 2026
Merged

fix: hooks were dead on a freshly installed plugin (0.50.2)#63
Shahinyanm merged 1 commit into
masterfrom
fix/plugin-dist-bootstrap

Conversation

@Shahinyanm

Copy link
Copy Markdown
Member

What

Hooks did not run at all on a freshly installed or freshly updated plugin, until something happened to trigger a build.

Why

dist/ is a build artifact and is not in git — but the marketplace installs this plugin from git. start.sh builds it, except start.sh only runs when the MCP server starts, and hooks are invoked directly:

node ${CLAUDE_PLUGIN_ROOT}/dist/index.js hook-read

So every hook fired before the first MCP server start hit a missing entry point and died.

It died quietly, which is the damaging part. Claude Code reads a failed hook as no decision, so enforcement did not warn or error — it simply was not there.

Found on a real machine while cleaning the plugin cache: the freshly updated 0.50.1 had no dist at all, while a months-old 0.47.1 had one purely because past sessions had built it. This is the most plausible explanation for the long-standing "it doesn't always work" complaint.

How

Hooks now go through hooks/run.sh, which:

  • builds dist when missing — ~3s, and node_modules ships with the plugin so no install is needed
  • guards that build with a lock, because a session start fires several hooks at once and they would otherwise all run tsc into the same directory
  • exits 0 if the build fails anyway, so a hook that cannot decide never becomes a hook that refuses
  • deliberately does not cd — the hooks read process.cwd() as the project root, so entering the plugin directory would break every relative path

Risk

The failure mode this replaces is silent non-enforcement, so almost any behaviour is an improvement. The one new cost is ~3s on the very first hook after an install.

Verified against a dist-less copy of the plugin:

check result
single hook on missing dist builds, returns deny
six concurrent hooks 6/6 decide, no stale lock
relative path from caller dir resolves correctly

Full suite 1450 green.

Notes for reviewer

The npm install path is untouched: installer.ts writes its own absolute-path hook commands and never reads hooks/hooks.json. hooks/run.sh was added to the files list so it ships in the npm tarball too.

Alternatives considered and rejected: committing dist (3.2 MB and 115 files churning on every release, plus src/dist duplication), and routing hooks through the existing start.sh (it cds into the plugin directory, which would make every hook resolve the project root to the plugin).

dist/ is a build artifact and is not in git, but the marketplace installs
this plugin from git. start.sh builds it — except start.sh only runs when
the MCP server starts, while hooks are invoked directly as
`node ${CLAUDE_PLUGIN_ROOT}/dist/index.js hook-*`. On a freshly installed
or updated plugin, every hook fired before the first MCP server start hit
a missing entry point and died.

It died quietly, which is the damaging part: Claude Code reads a failed
hook as no decision, so enforcement did not warn or error, it just was not
there. Found on a real machine — an updated 0.50.1 had no dist at all,
while a months-old 0.47.1 had one purely because past sessions had built
it. This is the most plausible explanation for "it doesn't always work".

Route hooks through hooks/run.sh, which:
  - builds dist when missing (~3s; node_modules ships with the plugin)
  - guards the build with a lock, because a session start fires several
    hooks at once and they would otherwise all run tsc into one directory
  - exits 0 if the build fails, so a hook that cannot decide never turns
    into a hook that refuses
  - does not cd, because the hooks read process.cwd() as the project root

Verified on a dist-less copy: one hook builds and decides; six concurrent
hooks all decide and leave no stale lock; a relative path still resolves
from the caller's directory.

The npm install path is untouched — installer.ts writes its own
absolute-path commands and never reads hooks/hooks.json.
@Shahinyanm
Shahinyanm merged commit dbe3073 into master Jul 26, 2026
3 checks passed
@Shahinyanm
Shahinyanm deleted the fix/plugin-dist-bootstrap branch July 26, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant