You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
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.
Hook flight log written by the plugin's tool/before route (hook-events.jsonl) over ~2 days of heavy multi-session use: codex 53 events, claude fires via PreToolUse, cursor 2 events — both manual echo '{...}' | node hooks/event-route-tool-before.cursor.mjs smoke tests. Zero from real Cursor sessions, before and after several reinstalls (loadUserLocalPlugin logged 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.json declaring afterShellExecution, postToolUse, stop, sessionStart, …): its analytics show Cursor delivered only sessionStart (2 invocations); every other declared event: 0. Manifest hooks appear limited to session lifecycle on this build.
Ran cargo --version from a live Cursor agent session: no preToolUse event; only the PATH shim caught it. Cursor's Cursor Agent Exec.log shows hookForcesPrompt:false on every shell evaluation and no hook execution lines.
Control: a throwaway ~/.cursor/hooks.json with preToolUse/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 real event-route-tool-before.cursor.mjs: the very next cargo --version was rewritten to node …/scripts/hauler.mjs exec --session <conversation_id> --host cursor -- cargo --version and tool/after recorded exitCode:0. (Reverted afterwards — this must come from the pack, not hand edits.)
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", plus matcher and timeout as 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 hooks from the installed .cursor-plugin/plugin.json when user-level registration succeeds, or document that tool/before routes 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 a sessionStart/preToolUse canary 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 applied
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-declaredpreToolUse/postToolUse/stophooks 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.jsonfire immediately forpreToolUse,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(matcherShell) withupdated_inputis the only Cursor event that can rewrite a command;beforeShellExecutionis allow/deny/ask only.Evidence (cargo-hauler 0.3.2,
~/.cursor/plugins/local/cargo-hauler)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)..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.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.~/.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.)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 …, privateCARGO_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:~/.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/."_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).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.INSTALL.md.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
Environment
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).