Repository navigation
Cursor pack: manifest-declared hooks never fire for tool events (Cursor 3.16) — emitted installer must register hooks in ~/.cursor/hooks.json so the plugin works out of the box #407
Description
Summary
A Cursor pack emitted by agent-bundle declares its hooks only through .cursor-plugin/plugin.json → "hooks": "./hooks/hooks-cursor.json". On the current Cursor build (3.16.21, ~/.cursor-server), Cursor loads the local plugin (loadUserLocalPlugin cargo-hauler loaded in 130ms, 0 failures) but never invokes the manifest-declared preToolUse / postToolUse / stop hooks for tool events. The plugin's shell-rewrite hook — its primary reason to exist — was dead for two days while every framework gate (validate, build, contract matrix, install proof) was green.
The same hook scripts registered through the documented user-level ~/.cursor/hooks.json fire immediately for preToolUse, beforeShellExecution, postToolUse, afterShellExecution, hot-reload without a restart, and apply across every live agent session. So the plumbing gap is in how the installer wires hooks, not in the projection.
Framework mapping itself is correct: per https://cursor.com/docs/agent/hooks, preToolUse (matcher Shell) with updated_input is the only Cursor event that can rewrite a command; beforeShellExecution is allow/deny/ask only.
Evidence (cargo-hauler 0.3.2, ~/.cursor/plugins/local/cargo-hauler)
- Hook flight log written by the plugin's
tool/beforeroute (hook-events.jsonl) over ~2 days of heavy multi-session use:codex53 events,claudefires via PreToolUse,cursor2 events — both manualecho '{...}' | node hooks/event-route-tool-before.cursor.mjssmoke tests. Zero from real Cursor sessions, before and after several reinstalls (loadUserLocalPluginlogged at 03:47, 04:39, 05:28, 05:34, 06:05, 06:23, 06:42). - Independent plugin on the same box (tracedecay,
.cursor-plugin/plugin.json→hooks/hooks.jsondeclaringafterShellExecution,postToolUse,stop,sessionStart, …): its analytics show Cursor delivered onlysessionStart(2 invocations); every other declared event: 0. Manifest hooks appear limited to session lifecycle on this build. - Ran
cargo --versionfrom a live Cursor agent session: nopreToolUseevent; only the PATH shim caught it. Cursor'sCursor Agent Exec.logshowshookForcesPrompt:falseon every shell evaluation and no hook execution lines. - Control: a throwaway
~/.cursor/hooks.jsonwithpreToolUse/beforeShellExecution/postToolUse/afterShellExecution→ logging script. Within seconds it logged all four events from six concurrent Cursor conversations,tool_name:"Shell"included. Swapping the file to the pack's realevent-route-tool-before.cursor.mjs: the very nextcargo --versionwas rewritten tonode …/scripts/hauler.mjs exec --session <conversation_id> --host cursor -- cargo --versionandtool/afterrecordedexitCode:0. (Reverted afterwards — this must come from the pack, not hand edits.) - The hook script itself is fine:
echo '{"tool_name":"Shell","tool_input":{"command":"cargo check -p demo"},...}' | node hooks/event-route-tool-before.cursor.mjs→{"permission":"allow","updated_input":{"command":"… hauler.mjs exec … -- cargo check -p demo"}}.
Consequences observed
With the hook dead, agents on this machine learned to route around the PATH-shim backstop (absolute ~/.rustup/toolchains/*/bin/cargo, env -u …, private CARGO_TARGET_DIR), which is exactly what the host hook exists to prevent. A pack that validates and installs clean but whose hooks never run is the worst failure mode: silent.
Requested behavior
The emitted Cursor install.mjs (and matching uninstall path, see #400 / #101) should produce a fully working plugin out of the box:
- Register hooks at the user level. Merge the pack's hook entries into
~/.cursor/hooks.json(create if absent; preserve other owners' entries;version: 1). Entries must be self-contained because user-level hooks do not receive${CURSOR_PLUGIN_ROOT}:
"command": "CURSOR_PLUGIN_ROOT=<dest> node <dest>/hooks/event-route-tool-before.cursor.mjs", plusmatcherandtimeoutas compiled. Absolute paths, since user hooks run from~/.cursor/. - Own the entries. Tag each entry (e.g.
"_agentBundle": {"plugin":"cargo-hauler","version":"0.3.2"}or a stable comment/marker the framework already uses) so reinstall replaces exactly its own entries and uninstall removes only them, leaving the file valid (or deleting it when it becomes empty and the installer created it). - Avoid double delivery. If/when Cursor starts honoring manifest hooks, both channels would fire. Either strip
hooksfrom the installed.cursor-plugin/plugin.jsonwhen user-level registration succeeds, or document thattool/beforeroutes must be idempotent — but pick one and make the emitted pack consistent. - Keep the manifest declaration for marketplace installs (where the installer does not run), and note the limitation in
INSTALL.md. - Prove liveness, not just shape. Post-install (or
agent-bundle doctor --host cursor), verify Cursor is actually delivering events — e.g. write the merged file, wait for the documented hot-reload, and check for asessionStart/preToolUsecanary or the pack's hook flight log — and print a loud warning when nothing arrives. The contract matrix (runPackedContractMatrix: coverage check requires a fixture for app routes, contradicting the docs #401) proves the route contract in-process; nothing today proves the host calls it.
Repro
# any pack with an events/tool/before route, e.g. cargo-hauler 0.3.2
node artifact/plugin/install.mjs # Cursor: loadUserLocalPlugin <name> loaded
# in a Cursor agent chat, run any Shell command matched by the hook
# → no hook process spawned, no updated_input applied
cat > ~/.cursor/hooks.json <<'J'
{"version":1,"hooks":{"preToolUse":[{"command":"CURSOR_PLUGIN_ROOT=$HOME/.cursor/plugins/local/<name> node $HOME/.cursor/plugins/local/<name>/hooks/event-route-tool-before.cursor.mjs","matcher":"Shell","timeout":10}]}}
J
# run the same command again → hook fires, updated_input appliedEnvironment
Cursor 3.16.21 (remote server, linux-x64), agent-bundle preview 105c65d8f, pack: https://github.com/ScriptedAlchemy/cargo-hauler @ 0.3.2.
Related: #189 (Cursor contract refresh), #400 (installer replace path), #401 (contract matrix coverage), #387 (routed CLI not in host artifacts).
Activity
feat(install): cursor --mode local|marketplace and hooks-registration…
feat(install): cursor --mode local|marketplace and hooks-registration…
feat(install): cursor --mode local|marketplace, marketplace staging, …
feat(install): cursor --mode local|marketplace, marketplace staging, …
ScriptedAlchemy commentedon Sep 3, 2026
Resolved by #414 (merged as
9c91a8697c). Full evidence note:docs/audits/2026-09-03-cursor-plugin-hooks-registration.md(URL + quote for every doc claim).Per-item evidence
.cursor-plugin/plugin.json,hooks/or manifest path) and are not part of the Agent Plugins rootplugin.jsonformat. No plugin/marketplace install deeplink exists;cursor-agent plugin marketplace addneeds a hosted Git URL.cursortarget on 3.16.21 in~/.cursor-serverand observed the same gap on tracedecay, which fires today. Cursor's CLI changelog (2026-08-11) records "Plugin hooks run from installed plugins … now execute"; in 3.18.25 plugin hooks are gated bythirdPartyExtensibilityEnabled/enable_cc_plugin_import. Vendor-side build gap, not an emitted-shape defect../hooks/hooks.jsonvshooks/hooks.json(same resolution),${CURSOR_PLUGIN_ROOT}commands vs absolute binary,^Shell$matchers. Plugin hook commands run withcwd= plugin root and${CURSOR_PLUGIN_ROOT}substituted (observed);.mjsscripts need no exec bit/shebang becausenodeis the command. Emitter unchanged: both shapes fire.preToolUse/postToolUse(^Shell$) andstopfired from the plugin manifest with no~/.cursor/hooks.json, identical to the hand-written probe, user and project hooks. Portable (Agent Plugins) has no hooks component by spec. Agent CLI not observed (auth); vendor changelog states plugin hooks run there since 2026-08-11.~/.cursor/plugins/marketplaces,plugins/cache, copiedstate.vscdb)plugins/cache/<marketplace>/<plugin>/<commit-or-version>/…+.cache-completereceipt; enabled-plugin identity is server-side numeric ids → replicating state by hand (strategy 3) is not viable.agent-bundle install cursor --mode marketplace, the package-relative installer bin, and emittedinstall.mjs --mode marketplacestage a committed local marketplace repo (~/.cursor/agent-bundle/marketplaces/<name>,.cursor-plugin/marketplace.jsonper pinned schema) and print the exact Customize → Plugins → "Add Plugins from Local Repository" step (the one UI step Cursor requires; native folder picker could not be driven under Xvfb). Byte-faithful commit (.git/info/attributes,core.autocrlf=false, blob-id proof,AB7004on drift), nested-.gitand non-Cursor-Plugin bundles refused (AB7003), idempotent reruns,AB7005collisions incl. dirty/ignored trees.--mode localfallbackdocs/framework-mode.md, package README, emittedINSTALL.md.~/.cursor/hooks.jsonregistrationAB7322registered/stale/missing (schema-validated hooks doc, resolves the executed script incl.env,NAME=value,bun run,pwsh -File,${CURSOR_PLUGIN_ROOT}\),AB7323duplicate user-level delivery / unparsable~/.cursor/hooks.json(component-boundary, Windows case-folded),AB7324staged marketplace registered/unregistered/corrupt (gitcat-file/statusverified,.cache-completefor the exact commit). Codes renumbered from AB7321–23 after #420 tookAB7321.cursor-2026-08-28.jsonhooks row carries the dated 2026-09-03 evidence;install.mjschanged → cursor 1.11.0, plugin 1.27.0, portable 1.8.0 with pins intests/adapter-metadata.test.ts.tests/install.test.ts(staging, schema-valid manifest, idempotency, version/manifest/dirty/ignored collisions, byte drift, nested git, missing git, mode rejection, CLI pass-through),tests/doctor.test.ts(AB7322–AB7324 incl. FIFO documents, interpreter forms, win32 case),tests/install-surface.test.ts, host-install proofs asserting hooks registration + marketplace staging (pnpm test:host-install:build19/19).docs/framework-mode.md, package README, emittedINSTALL.md,docs/diagnostics.md,.changeset/cursor-install-mode-hooks-registration.md(patch).Evidence matrix (condensed; full table in audit §3)
~/.cursor/hooks.json<ws>/.cursor/hooks.jsoncursortarget)node "${CURSOR_PLUGIN_ROOT}/hooks/*.mjs"^Shell$) / postToolUse (^Shell$) / stop~/.cursor/hooks.jsonportable, rootplugin.json)Delta that "fixed" it: none in the emitted shape — the 3.16.21 server build did not deliver plugin hooks; 3.18.25 does. Doctor now makes that silent state visible (
AB7322stale/missing,AB7323duplicates).Review note: Codex reviewed through
8788311b12(all threads fixed);807299ab8andf113002a7were not reviewed (usage limits) and are listed in the PR body for a follow-up review.ScriptedAlchemy commentedon Sep 4, 2026
2026-09-04 follow-up (evidence, no install-path change): the record was re-read against the question "do marketplace-installed plugins fire hooks while user-local installs do not?", and a non-interactive probe was run on this machine's live Cursor. Written up as §7 of
docs/audits/2026-09-03-cursor-plugin-hooks-registration.md(PR #500).Desktop windows (3.18.25): user-local plugin hooks fire. Beyond the isolated-instance matrix in §3, Cursor's own hooks log for the maintainer's daily instance (
~/.config/Cursor/logs/20260903T041607/window1_wb5/…/cursor.hooks.workspaceId-19017dde…log, 2026-09-03 20:43–20:56Z) records 220Executing hook … from claude-plugin configlines, 121 withRunning script in directory: /home/zack/.cursor/plugins/local/tracedecayand 77 with…/plugins/local/cargo-hauler(an agent-bundle emittedcursorpack), acrosspreToolUse(124 steps),postToolUse(78),afterShellExecution(40),sessionStart,stop,workspaceOpen, withNo user hooks configuration found. Marketplace hooks were never observed instead of local ones, and §4–§5 still stand: the marketplace-installed state is server-assigned and cannot be produced by an installer.Remote-SSH windows (3.19.3): this issue reproduces on a current build. #407 was filed from
~/.cursor-server; the same configuration was live here today (client and server3.19.3, server commit3b4eb8dd53). The remote extension host loads local plugins from the remote home (Cursor Plugins.log:loadUserLocalPlugin cargo-hauler loaded,… tracedecay loaded, 17 marketplace plugins from~/.cursor/plugins/cache,total=20 plugins, failures=0). A throwaway~/.cursor/plugins/local/ab-hooks-probe(hand-written.cursor-plugin/plugin.json+hooks/hooks.json,preToolUse/postToolUse^Shell$,stop,sessionStart→sh "${CURSOR_PLUGIN_ROOT}/hooks/log.sh") was loaded within 10 s (loadUserLocalPlugin ab-hooks-probe loaded in 327.2ms,21 plugins loaded … 0 failures) and then produced 0 events over two minutes of Shell traffic from seven concurrent conversations. The identical commands placed in a remote~/.cursor/hooks.jsonproduced 28 events (15preToolUse, 13postToolUse) from 7 conversations within 20 s, executed on the remote host (cwd/home/zack/.cursor, payloadcursor_version: "3.19.3",workspace_roots: ["/fast/projects/agent-bundle"]). Both probes were removed afterwards (loadUserLocalPlugins completed … (3 plugins loaded)). tracedecay's and cargo-hauler's own telemetry agree: lastcursorhook delivery 2026-09-03 20:56Z, the desktop instance's final minute, none during the Remote-SSH hours since.Not observed: whether hooks of marketplace plugins execute in a Remote-SSH window, and on which machine. A marketplace plugin exists in the client's
~/.cursor/plugins/cacheas well as the remote's (the enabled set is account state), whereas a local install exists only on the host where the installer ran; if the client-side hooks service resolves plugin hooks against the client's copies, that asymmetry alone would make marketplace installs appear to fire and local ones not in Remote-SSH windows. That is a hypothesis; §7.3 lists the two-host, three-placement matrix (marker files on both filesystems,Cursor Plugins.logon the remote,cursor.hooks.*.logon the client) that would settle it.Install default stays
--mode local;--mode marketplacestays the staged Customize hand-off; DoctorAB7322keeps proving registration rather than assuming it.docs(audits): record the 2026-09-04 re-read of local vs marketplace h…
docs(audits): record the 2026-09-04 re-read of local vs marketplace h…
docs(audits): record the 2026-09-04 re-read of local vs marketplace h…