Skip to content

fix(hooks): stop Cursor plugin hooks failing silently on every event - #7

Merged
brunoazoulay merged 2 commits into
mainfrom
fix/cursor-hooks-inert
Sep 7, 2026
Merged

brunoazoulay merged 2 commits into
mainfrom
fix/cursor-hooks-inert

Conversation

@brunoazoulay

Copy link
Copy Markdown
Collaborator

Summary

  • Installed Cursor plugins were fully inert: hooks loaded and wired, but nothing ever executed. Three chained defects, each masking the next -- plugin-scanner.ts parsed the nested Claude Code hooks shape instead of Cursor's flat one, matchesFilter only tested tool_name instead of the per-event field Cursor actually sends, and ./scripts/hook.sh was never resolved with a cwd before spawn.
  • hooks-loader.ts now logs to stderr before failing open instead of exiting clean on error.
  • Rewrote the hooks-harness-exclusive anti-regression gate (was 22 assertions with zero on an actual command; now 106, two-layer invariant manifest -> wrapper -> harness), including two sniper-found fixes: a command regex that let shell injection through, and a throw during bun:test collection that silently dropped a whole plugin's checks.

Changes

  • .cursor-plugin/scripts/hooks-loader.ts
  • .cursor-plugin/scripts/src/interfaces/hooks.ts
  • .cursor-plugin/scripts/src/services/plugin-scanner.ts
  • .cursor-plugin/scripts/src/__tests__/plugin-scanner.test.ts
  • .cursor-plugin/scripts/src/__tests__/plugin-scanner-home-resolution.test.ts
  • .cursor-plugin/scripts/src/__tests__/plugin-scanner-matcher.test.ts (new)
  • .cursor-plugin/scripts/src/__tests__/plugin-scanner-script-path.test.ts (new)
  • .cursor-plugin/scripts/src/__tests__/hooks-harness-exclusive.test.ts
  • .cursor-plugin/scripts/src/__tests__/hooks-harness-exclusive.helpers.ts
  • CHANGELOG.md (0.1.3)

Test plan

  • bun test from .cursor-plugin/scripts: 278 pass / 0 fail / 548 expect across 30 files (was 194 pass / 464 expect)
  • bunx tsc --noEmit: exit 0
  • Targeted gate (hooks-harness-exclusive): 22 -> 106 assertions
  • Red path proven: 7 shell-injection payloads blocked / 0 leaks; 63 real manifest commands / 0 false rejects
  • CI green

Breaking changes

None

Installed Cursor plugins were fully inert -- loaded and wired, but no
hook ever executed anything. Three chained defects, each masking the
next:

- plugin-scanner.ts read entry.hooks[] (Claude Code's nested shape),
  but a Cursor plugin hooks.json is flat ({ command, matcher?, type? })
  -> TypeError on every manifest, swallowed by a catch that exited 0.
- matchesFilter always tested tool_name, while Cursor tests the shell
  command line on beforeShellExecution/afterShellExecution, a
  synthesized MCP:<tool_name> prefix on
  beforeMCPExecution/afterMCPExecution, subagent_type on
  subagentStart/subagentStop, and fixed constants on
  beforeSubmitPrompt/stop -- the MCP and shell guards could never
  match.
- ./scripts/hook.sh was never resolved to an absolute path: the spawn
  passes CURSOR_PLUGIN_ROOT via env but no cwd, so the script was
  never found and hooks failed open silently.

hooks-loader.ts now logs to stderr before failing open instead of
exiting clean on error.

Rewrote the hooks-harness-exclusive anti-regression gate: it produced
22 assertions with zero on an actual command (it iterated the nested
shape), and its criterion looked for the harness binary directly in
the manifest even though Cursor submission requires relative paths.
New two-layer invariant: manifest delegates to ./scripts/hook.sh, and
that wrapper invokes the harness. Two more defects found by sniper and
fixed in the gate itself: a command regex that let shell injection
through (&&, ;, $(...), backticks, redirections), and a throw during
bun:test collection that silently dropped every check for a plugin.

Verified before commit: bun test 278 pass / 0 fail / 548 expect across
30 files (was 194 pass / 464 expect); bunx tsc --noEmit exit 0; the
targeted gate went from 22 to 106 assertions, with a proven red path
(7 injection payloads blocked, 0 leaks; 63 real manifest commands, 0
false rejects).
@brunoazoulay
brunoazoulay merged commit 6879505 into main Sep 7, 2026
3 checks passed
@brunoazoulay
brunoazoulay deleted the fix/cursor-hooks-inert branch September 7, 2026 08:22
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