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
{{ message }}
Repository navigation
fix(framework): Cursor's turn-end hook never fires headless #680
Under cursor-agent -p, the framework's turn-end hook receives nothing.
Measured on a real invocation (cursor-agent -p "…" --force --trust --output-format text) against a project that declared stop, beforeSubmitPrompt, preToolUse, postToolUse, beforeReadFile, sessionStart and sessionEnd in its .cursor/hooks.json:
stop and beforeSubmitPrompt never fired. sessionEnd did. Five other events from the same file fired, so this is neither a load failure nor a scope problem.
Why it matters
CURSOR_EVENT_MAP in cli/src/domain/formats/flat-hooks-merge.ts:32-38 maps Stop -> stop and has no entry for sessionEnd. The run journal's turn-end line is driven by Stop (plugins/aidd-telemetry/hooks/hooks.json), so on Cursor headless no turn is ever recorded, and the one event that does mark the end of the work is not mapped to anything.
Bounds
Observed on one invocation, headless. Whether an interactive Cursor session fires stop is not established; the same probe run without -p settles it. Do not change the mapping before that is known - sessionEnd and stop are not interchangeable if both can fire.
Done when
It is established, by probe, whether stop fires in an interactive Cursor session.
A turn boundary is recorded on Cursor in both modes, or the mode where it cannot be is named in the docs rather than left silent.
A regression test fails if the event Cursor actually delivers stops being mapped.
Also found
A single unknown event name in .cursor/hooks.json invalidates the entire file, silently: the validator rejects the whole config and the loader stores nothing for that source. CURSOR_EVENT_MAP emits only names from Cursor's 21-name enum today, but nothing in the build asserts that.
While verifying #698 I ran two headless cursor-agent -p probes against a plugin-scope hook — ~/.cursor/plugins/local/<plugin>/hooks.json — declaring beforeSubmitPrompt, preToolUse, stop and a Claude-shaped SessionStart, with a script that appends to three separate paths and exits 0. Nothing fired. No file, on any of the four events, on either run.
That is a different failure from this ticket. The probe recorded here fired five of seven events from .cursor/hooks.json, project scope. Plugin scope fired zero. So before stop versus sessionEnd matters, there is a prior question: whether a plugin's own hooks.json is loaded at all, and what registers it — nothing in ~/.cursor/cli-config.json, argv.json or ide_state.json names the plugins sitting in ~/.cursor/plugins/local/.
Bounds: two invocations, headless, one machine, one plugin. Not established whether an interactive session loads them, which is the same probe this ticket already needs without -p. Both questions are answered by one interactive run, so they are worth doing together.
Worth knowing because the journal's Cursor path assumes plugin-scope hooks run. If they do not, mapping sessionEnd correctly changes nothing on that route.
Three probes settle it, one of them interactive through a real pty:
Where the hooks live
What fired
~/.cursor/plugins/local/<plugin>/hooks.json — where the framework installs them
nothing, 0 of 7 events, across auto-discovery and explicit --plugin-dir with a manifest matching Cursor's own validated schema, headless and interactive
.cursor/hooks.json in the project — where aidd framework build --target cursor --flat puts them
stop fired twice in one interactive session, and the journal wrote a real run file
The run file carries Cursor's own conversation id, a session_start, and two turn_end lines from two genuine stop firings (status: "error" then "aborted" in the captured payloads — two events, not one delivered twice).
So Cursor does not refuse our hooks. We install them into a directory nothing reads. cursor-agent plugin --help exposes only marketplace subcommands, and the CLI binary's own strings contain no reference to scanning plugins/local at all.
Two things found before spending a paid session
Both by dry-running journal.js against real-shaped payloads, at no cost:
Cursor resolves the repository root from payload.workspace_roots, not cwd. Every other host uses cwd, and the hook reads cwd. Without this the journal resolves by accident or not at all — it is the reason the first attempts wrote nothing.
Commands in .cursor/hooks.json resolve relative to the project root, the directory holding .cursor/. Confirmed against flat-build-strategy.ts's own path construction and then empirically.
What this ticket asked, answered
It is established, by probe, whether stop fires in an interactive Cursor session.
It does. Every prior probe was headless, which is why it had never been seen. The original headless probe fired sessionEnd and not stop, and that difference is real rather than a loading failure — so whichever event closes a turn has to be chosen per mode, from what each one actually fires. CURSOR_EVENT_MAP still has no SessionEnd key, so a headless install would journal a start and never a boundary.
CURSOR_EVENT_MAP was deliberately left untouched by the probe: mapping the right event changes nothing while nothing reads the map.
What follows
Work is in flight to install Cursor's hooks where Cursor reads them, read the root the way Cursor names it, and close a turn in both modes from what each fires. Plan: aidd_docs/tasks/2026_08/2026_08_22_telemetry-every-tool/phase-6.md.
One correction to the record: a probe flipped Cursor's journalAttributable to false in the plugin's declaration, and a new conformance test caught that the CLI computed true for the same tool. The CLI was right and the flag was reverted. That test — registry-conformance.unit.test.ts, "agrees with the plugin's own cost-report declaration on journalAttributable" — now pins the two implementations together per tool, which is the second time two copies of one fact have drifted here and the first time anyone would have seen it.
Done and awaiting review in #706, which closes this on merge.
The work is on claude/aidd-telemetry-layer-e403uf, eleven commits, targeting next. Gate at the time of push: 365 plugin specs, 1,931 CLI unit, 577 integration, 178 e2e, tsc and biome clean, no broken markdown links.
Read the eleven commits rather than the pull request's file count — the branch carries its own copy of the CLI migration that next has since received, so the diff counts it twice. The range and the real figures are in the first comment on #706: 142 files, +10,518 −418.
Every claim about a tool in this work rests on a session that was actually run. The negative results are kept in full in aidd_docs/tasks/2026_08/2026_08_22_telemetry-every-tool/measurements.md, including the two probes that disagreed about OpenCode and the measurement that reconciled them.
Activity
blafourcade commentedon Aug 21, 2026
A second Cursor gap, one level up from this one
While verifying #698 I ran two headless
cursor-agent -pprobes against a plugin-scope hook —~/.cursor/plugins/local/<plugin>/hooks.json— declaringbeforeSubmitPrompt,preToolUse,stopand a Claude-shapedSessionStart, with a script that appends to three separate paths and exits 0. Nothing fired. No file, on any of the four events, on either run.That is a different failure from this ticket. The probe recorded here fired five of seven events from
.cursor/hooks.json, project scope. Plugin scope fired zero. So beforestopversussessionEndmatters, there is a prior question: whether a plugin's ownhooks.jsonis loaded at all, and what registers it — nothing in~/.cursor/cli-config.json,argv.jsonoride_state.jsonnames the plugins sitting in~/.cursor/plugins/local/.Bounds: two invocations, headless, one machine, one plugin. Not established whether an interactive session loads them, which is the same probe this ticket already needs without
-p. Both questions are answered by one interactive run, so they are worth doing together.Worth knowing because the journal's Cursor path assumes plugin-scope hooks run. If they do not, mapping
sessionEndcorrectly changes nothing on that route.blafourcade commentedon Aug 22, 2026
Cursor journals. The obstacle was never Cursor.
Three probes settle it, one of them interactive through a real pty:
~/.cursor/plugins/local/<plugin>/hooks.json— where the framework installs them--plugin-dirwith a manifest matching Cursor's own validated schema, headless and interactive.cursor/hooks.jsonin the project — whereaidd framework build --target cursor --flatputs themstopfired twice in one interactive session, and the journal wrote a real run fileThe run file carries Cursor's own conversation id, a
session_start, and twoturn_endlines from two genuinestopfirings (status: "error"then"aborted"in the captured payloads — two events, not one delivered twice).So Cursor does not refuse our hooks. We install them into a directory nothing reads.
cursor-agent plugin --helpexposes only marketplace subcommands, and the CLI binary's own strings contain no reference to scanningplugins/localat all.Two things found before spending a paid session
Both by dry-running
journal.jsagainst real-shaped payloads, at no cost:payload.workspace_roots, notcwd. Every other host usescwd, and the hook readscwd. Without this the journal resolves by accident or not at all — it is the reason the first attempts wrote nothing..cursor/hooks.jsonresolve relative to the project root, the directory holding.cursor/. Confirmed againstflat-build-strategy.ts's own path construction and then empirically.What this ticket asked, answered
It does. Every prior probe was headless, which is why it had never been seen. The original headless probe fired
sessionEndand notstop, and that difference is real rather than a loading failure — so whichever event closes a turn has to be chosen per mode, from what each one actually fires.CURSOR_EVENT_MAPstill has noSessionEndkey, so a headless install would journal a start and never a boundary.CURSOR_EVENT_MAPwas deliberately left untouched by the probe: mapping the right event changes nothing while nothing reads the map.What follows
Work is in flight to install Cursor's hooks where Cursor reads them, read the root the way Cursor names it, and close a turn in both modes from what each fires. Plan:
aidd_docs/tasks/2026_08/2026_08_22_telemetry-every-tool/phase-6.md.One correction to the record: a probe flipped Cursor's
journalAttributabletofalsein the plugin's declaration, and a new conformance test caught that the CLI computedtruefor the same tool. The CLI was right and the flag was reverted. That test —registry-conformance.unit.test.ts, "agrees with the plugin's own cost-report declaration on journalAttributable" — now pins the two implementations together per tool, which is the second time two copies of one fact have drifted here and the first time anyone would have seen it.feat(telemetry): the journal records which step was running, on four …
blafourcade commentedon Aug 22, 2026
Done and awaiting review in #706, which closes this on merge.
The work is on
claude/aidd-telemetry-layer-e403uf, eleven commits, targetingnext. Gate at the time of push: 365 plugin specs, 1,931 CLI unit, 577 integration, 178 e2e,tscand biome clean, no broken markdown links.Read the eleven commits rather than the pull request's file count — the branch carries its own copy of the CLI migration that
nexthas since received, so the diff counts it twice. The range and the real figures are in the first comment on #706: 142 files, +10,518 −418.Every claim about a tool in this work rests on a session that was actually run. The negative results are kept in full in
aidd_docs/tasks/2026_08/2026_08_22_telemetry-every-tool/measurements.md, including the two probes that disagreed about OpenCode and the measurement that reconciled them.feat(telemetry): the journal records which step was running, on four …
test(cli): the mapping that makes Cursor close a turn headlessly is p…
fix(framework): Codex closes a turn on SessionEnd, because it has no …
docs(framework): three documents catch up with the code they describe
feat(framework): measurement that works on five tools, each proven by…
blafourcade commentedon Sep 2, 2026
Delivered by #706, squash-merged into
nextas 627408f.