feat(capture): Cursor harness via hook receiver#111
Conversation
Wire a second HarnessDriver for Cursor that ingests command-hook JSON through a local loopback receiver, with stock forwarder scripts and an auto transport that keeps Claude file-tail working alongside it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
Warning Review limit reached
Next review available in: 43 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughCursor support is added through a registered harness driver, canonical event normalization, loopback HTTP and Unix-socket capture, automatic Claude-plus-Cursor transport, fail-open hook forwarders, CI smoke tests, and updated documentation. ChangesCursor capture pipeline
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Cursor
participant HookForwarder
participant HookReceiver
participant CursorDriver
participant DerivedState
Cursor->>HookForwarder: emit hook JSON
HookForwarder->>HookReceiver: POST hook payload
HookReceiver->>CursorDriver: provide JSONL entry
CursorDriver->>DerivedState: emit canonical events
DerivedState->>DerivedState: update transcript and agent state
Possibly related PRs
Suggested labels: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
I found two blocking correctness issues in the Cursor capture path:
-
The supplied hook configuration registers generic tool hooks (
preToolUse/postToolUse) together with the specialized shell/MCP/file hooks, but the normalizer emits a separate canonical event for every delivery and has no correlation/deduplication step. A single tool call can therefore become duplicate starts/completions (specialized events often lacktool_use_idand receive random IDs), inflating tool counts, file attention, and risk signals. Please either choose one hook family in the example configuration or deduplicate the overlapping events using a stable Cursor correlation key before emitting canonical events. -
subagentStart/subagentStopevents useactor: 'system'whilederiveState()keys agent nodes byevent.actor; theiragentIdis only stored in payload. All Cursor subagents therefore collapse into onesystemnode instead of being represented individually. Use the subagent identity consistently (or update derivation to key these lifecycle events bypayload.agentId).
The PR's existing CI test, CodeRabbit, and GitGuardian checks are green for this head. I did not rerun tests/build locally during this read-only review; the findings are based on the current diff, repository consumers, tests, and docs.
See the inline comments for the affected code paths.
Add env/auto/unix/source-attribution tests, expand cursor normalizer and toolNameMap coverage, document the harness driver contract and hook install path, and make stream transports accept a configurable EventSource. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a live vitest that drives the real shell forwarder into the real hook-receiver (async spawn to avoid event-loop deadlock), a standalone live-capture server, and committed project hooks for dogfooding. Record that this Background Agent VM does not invoke .cursor/hooks.json itself. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Project hooks added after daemon startup were invisible until ControlService.ReloadAgentSkills. Add a helper script and document the self-hook proof on this Background Agent VM. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Add a required npm run test:cursor-hooks step to CI, plus an optional job that installs the Cursor CLI and proves project hooks POST into the receiver when CURSOR_API_KEY is configured. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Stock hooks.json now registers only the generic tool family. Specialized shell/MCP/file hooks without tool_use_id are skipped so one tool call does not inflate derive counts. Subagent Start/Stop use Cursor's subagent_id as actor (and payload.agentId); derive prefers that id. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Files Reviewed (3 files)
Previous Review Summaries (2 snapshots, latest commit 03078a4)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 03078a4)Status: No Issues Found | Recommendation: Merge Files Reviewed (2 files)
Previous review (commit 3c031a5)Status: 1 Warning Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Files Reviewed (4 key source files)
Also reviewed: docs ( Note on Reviewed by step-3.7-flash · Input: 41.9K · Output: 3K · Cached: 181K |
|
@charliecreates please re-review — both blocking items are fixed in 3c031a5 (and threads resolved):
CI ( |
sessionManager.start(overridePath) now spreads the configured transport options so cursor-agent-trace (and hook settings) survive path overrides. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
🤖 CodeAnt AI — Review Status
Updated in place by CodeAnt AI · last 5 reviews |
Thanks for using CodeAnt! 🎉We're free for open-source projects. if you're enjoying it, help us grow by sharing. Share on X · |
There was a problem hiding this comment.
Pull request overview
This PR adds a first-class Cursor observation path alongside Claude Code by introducing a local hook-receiver transport, a Cursor harness driver/normalizer, and CI gates that validate the end-to-end hook forwarding pipeline.
Changes:
- Add Cursor harness support (
cursor-hook+ optionalcursor-agent-trace) via a new driver and hook-receiver transport (HTTP + optional Unix socket). - Make
autothe default capture transport (Claude file-tail + hook receiver concurrently) and propagatesourcecorrectly across transports/discovery/overrides. - Add fixtures, tests, scripts, docs, and CI gates proving forwarder → receiver → derive, plus an optional Cursor CLI self-test.
Reviewed changes
Copilot reviewed 48 out of 48 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/schema.test.ts | Extends runtime schema contract coverage for new Cursor event sources. |
| tests/live/cursor-hooks-live.test.ts | Live CI contract test for real forwarder → receiver → cursor normalize → derive. |
| tests/integration/e2e-cursor-hooks-to-state.test.ts | E2E fixture→buffer→derive coverage for Cursor hook events. |
| tests/fixtures/transcripts/cursor-hooks.jsonl | Adds Cursor hook JSON fixture lines used across tests. |
| tests/fixtures/README.md | Documents transcript fixture formats across Claude + Cursor. |
| tests/capture/session-manager.test.ts | Ensures auto transport recreation preserves overrideSource and Cursor attribution. |
| tests/capture/hook-receiver-unix.test.ts | Verifies hook-receiver accepts JSON payloads via Unix domain socket. |
| tests/capture/file-tail-source.test.ts | Ensures file-tail preserves discovered/override source for non-Claude JSONL. |
| tests/capture/drivers/harness-driver.test.ts | Updates registry assertions to include Cursor sources and preseeded drivers. |
| tests/capture/drivers/cursor.test.ts | Adds comprehensive Cursor driver + hook-receiver + forwarder contract coverage. |
| tests/capture/capture-transport-env.test.ts | Validates env→transport resolution for auto + hook-receiver + source overrides. |
| tests/capture/auto-capture-transport.test.ts | Tests composite auto transport behavior and fail-open when hook port is busy. |
| src/shared/schema.ts | Adds Cursor sources to KnownEventSources and keeps EventSource extensible. |
| src/shared/derive.ts | Improves subagent node identity by preferring payload.agentId (Cursor compatibility). |
| src/capture/websocket-transport.ts | Allows configurable session source for websocket transport. |
| src/capture/transcript-watcher.ts | Preserves discovered source and forwards overrideSource into discovery. |
| src/capture/socket-transport.ts | Allows configurable session source for socket transport. |
| src/capture/session-manager.ts | Preserves configured transport options when recreating transports with overridePath. |
| src/capture/http-stream-transport.ts | Allows configurable session source for http-stream transport. |
| src/capture/hook-receiver-transport.ts | Implements local HTTP/Unix hook receiver with optional token auth and source stamping. |
| src/capture/drivers/index.ts | Seeds singleton driver registry with Cursor after Claude to keep Claude as default. |
| src/capture/drivers/cursor/normalizer.ts | Normalizes Cursor hooks + agent-trace entries into CanonicalEvents. |
| src/capture/drivers/cursor/index.ts | Registers Cursor harness driver, sources, capabilities, discovery, and exports helpers. |
| src/capture/drivers/cursor/discovery.ts | Adds discovery for optional .agent-trace/traces.jsonl sessions. |
| src/capture/drivers/cursor/capabilities.ts | Adds Cursor capabilities and tool-name mapping for derive heuristics. |
| src/capture/capture-transports.ts | Adds hook-receiver + auto transports; makes auto the default env transport. |
| src/capture/capture-transport.ts | Extends transport option types (hook-receiver/auto) and adds overrideSource/source fields. |
| src/capture/auto-capture-transport.ts | Adds composite transport that runs file-tail + hook receiver concurrently (fail-open). |
| scripts/hooks/reload-exec-daemon-hooks.sh | Adds helper to reload Cursor exec-daemon hooks mid-session in cloud VMs. |
| scripts/hooks/README.md | Documents hook forwarders, environment vars, CI gates, and live verification flows. |
| scripts/hooks/live-capture-server.mjs | Adds standalone hook capture server for smoke tests and CI self-test support. |
| scripts/hooks/forward-to-shadow.sh | Adds POSIX hook forwarder that fail-opens and posts to the local receiver. |
| scripts/hooks/forward-to-shadow.ps1 | Adds Windows PowerShell hook forwarder with fail-open behavior. |
| scripts/hooks/forward-to-shadow-debug.sh | Adds a debug wrapper to prove forwarder invocation during dogfooding. |
| scripts/hooks/cursor-hooks.example.json | Provides example Cursor hooks config pointing at the forwarder script. |
| scripts/hooks/ci-cursor-cli-self-test.sh | Adds optional CI script validating real Cursor CLI hooks with CURSOR_API_KEY. |
| README.md | Updates top-level docs to include Cursor capture via auto transport and hooks. |
| package.json | Adds Cursor hook test scripts and live capture helper script. |
| docs/research/harness-ingestion-matrix.md | Updates research matrix with implemented Cursor ingestion details and dates. |
| docs/plans/roadmap.md | Notes Cursor harness landing under Reach milestone. |
| docs/plans/plan-multi-harness-mvp.md | Updates plan status and marks Cursor driver/hook receiver as landed. |
| docs/north-star.md | Updates “Watch” pillar text to include Cursor via hook receiver. |
| docs/history/log.md | Records Cursor harness + CI gates additions in project history. |
| docs/getting-started.md | Updates setup/test/run instructions and adds Cursor hook usage + env var docs. |
| docs/domain-events.md | Documents harness driver contract and Cursor hook→CanonicalEvent mapping. |
| docs/architecture.md | Updates capture architecture to reflect auto transport and Cursor support. |
| .github/workflows/ci.yml | Adds required Cursor hook contract gate and optional Cursor CLI hook self-test job. |
| .cursor/hooks.json | Commits project Cursor hooks config for dogfooding and CI/CLI compatibility. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| function runForwarder(port: number, body: string): Promise<{ status: number | null; stdout: string; stderr: string }> { | ||
| return new Promise((resolve, reject) => { | ||
| const child = spawn(FORWARDER, [], { | ||
| env: { | ||
| ...process.env, | ||
| SHADOW_HOOK_URL: `http://127.0.0.1:${port}/hook`, | ||
| SHADOW_HOOK_SOURCE: 'cursor-hook' | ||
| } | ||
| }); |
| return { | ||
| sessionId, | ||
| label: options.sessionLabel ?? `Live Cursor: ${sessionId.slice(0, 12)}`, | ||
| source, | ||
| path: pathHint ?? `hook-receiver://${source}/${sessionId}`, | ||
| transportId: 'hook-receiver' | ||
| }; |
| const host = options.host ?? DEFAULT_HOOK_RECEIVER_HOST; | ||
| const port = options.port ?? DEFAULT_HOOK_RECEIVER_PORT; | ||
| const fallbackSessionId = options.sessionId ?? 'cursor-live'; | ||
|
|
| async function freePort(): Promise<number> { | ||
| const probe = createServer(); | ||
| await new Promise<void>((resolve) => probe.listen(0, '127.0.0.1', () => resolve())); | ||
| const address = probe.address(); | ||
| if (!address || typeof address === 'string') throw new Error('no port'); | ||
| const port = address.port; | ||
| await new Promise<void>((resolve, reject) => | ||
| probe.close((error) => (error ? reject(error) : resolve())) | ||
| ); | ||
| return port; |
There was a problem hiding this comment.
Suggestion: The freePort probe introduces a time-of-check/time-of-use race: the test closes the probe server and only later binds the real receiver to that same port, so another process (or parallel test) can claim it first and cause intermittent EADDRINUSE failures. Bind once and keep the socket open until the transport is ready, or let the transport pick port 0 and read back the assigned port. [race condition]
Severity Level: Major ⚠️
- ❌ Auto capture transport tests flake due to port collisions.
- ⚠️ CI stability degraded validating Cursor hook receiver.Steps of Reproduction ✅
1. Run the Vitest suite so `tests/capture/auto-capture-transport.test.ts` executes
`describe('auto capture transport', ...)` starting at line 39.
2. In the test `it('starts file-tail and hook-receiver, forwarding both session sources',
...)` at lines 52–111, observe it calls `const port = await freePort();` at line 65, which
uses `freePort()` defined at lines 27–36 to bind a probe server on port 0, read the
assigned port, then close the probe before returning the port.
3. Concurrently, another test such as `tests/live/cursor-hooks-live.test.ts` (freePort at
line 34 per Grep output) or `tests/capture/hook-receiver-unix.test.ts` (freePort at line
25) can perform the same probe-close pattern and bind its real server to the same
ephemeral port in the window after `freePort()` closes the probe and before the auto
transport actually binds, taking the port first.
4. Back in `createAutoCaptureTransport()` implementation at
`src/capture/auto-capture-transport.ts:25–78`, the `start()` method constructs
`hookOptions` with `port: options.hookPort` (line 48) and calls
`createHookReceiverCaptureTransport(hookOptions)` then `hookReceiver.start(context)`
inside a `try` (lines 56–58); `createHookReceiverCaptureTransport`’s `start()` binds
`server.listen(port, host, ...)` at `src/capture/hook-receiver-transport.ts:196–202`. If
another process/test has claimed the port, `server.once('error', reject)` (line 197)
rejects with `EADDRINUSE`, causing the hook receiver to fail to start while the test still
expects a reachable `http://127.0.0.1:${port}/hook` at lines 97–105, making the test
intermittently fail or time out depending on whether the race occurs in that run.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** tests/capture/auto-capture-transport.test.ts
**Line:** 27:36
**Comment:**
*Race Condition: The `freePort` probe introduces a time-of-check/time-of-use race: the test closes the probe server and only later binds the real receiver to that same port, so another process (or parallel test) can claim it first and cause intermittent `EADDRINUSE` failures. Bind once and keep the socket open until the transport is ready, or let the transport pick port `0` and read back the assigned port.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| it('accepts POSTed Cursor hooks and feeds JSONL chunks', async () => { | ||
| const probe = createServer(); | ||
| await new Promise<void>((resolve) => probe.listen(0, '127.0.0.1', () => resolve())); | ||
| const address = probe.address(); | ||
| if (!address || typeof address === 'string') throw new Error('no port'); | ||
| const port = address.port; | ||
| await new Promise<void>((resolve, reject) => | ||
| probe.close((error) => (error ? reject(error) : resolve())) | ||
| ); |
There was a problem hiding this comment.
Suggestion: This test repeats the same free-port probe pattern (bind random port, close it, then reuse it), which is racy and can fail nondeterministically when another process takes the port between close and transport startup. Use a single listener lifecycle or dynamic port assignment from the transport/server itself. [race condition]
Severity Level: Major ⚠️
- ❌ Hook-receiver contract tests intermittently fail with EADDRINUSE.
- ⚠️ Overall capture pipeline verification becomes unreliable over time.Steps of Reproduction ✅
1. Run Vitest so `tests/capture/drivers/cursor.test.ts` executes `describe('hook-receiver
transport', ...)` starting at line 362, which contains the hook-receiver contract tests.
2. In the test `it('accepts POSTed Cursor hooks and feeds JSONL chunks', ...)` at lines
375–435, note the port probe sequence: `const probe = createServer();` (line 376),
`probe.listen(0, '127.0.0.1', ...)` (line 377), reading `const port = address.port;` (line
380), then closing the probe with `await new Promise<void>(... probe.close(...))` at lines
381–382 before using that port for the actual server.
3. Immediately afterward, the test constructs a hook-receiver transport via
`createHookReceiverCaptureTransport({ kind: 'hook-receiver', host: '127.0.0.1', port,
defaultSource: 'cursor-hook' })` (lines 391–396) and calls `transport.start(...)` (lines
397–413); this in turn calls `server.listen(port, host, ...)` in
`createHookReceiverCaptureTransport` at `src/capture/hook-receiver-transport.ts:196–202`.
At the same time, other tests using the same probe-close pattern — including the second
test in this block `it('rejects unauthorized requests when a shared token is configured',
...)` at lines 437–483, and freePort-based tests like
`tests/live/cursor-hooks-live.test.ts:34` and
`tests/capture/hook-receiver-unix.test.ts:25` (from Grep) — can bind their own server to
the probed port after it is closed but before this `server.listen` call.
4. When another process/test has taken the port, `server.once('error', reject)` at
`hook-receiver-transport.ts:197` causes `transport.start` to reject with `EADDRINUSE`,
leaving no server listening on `http://127.0.0.1:${port}/hook`. The test then attempts
`fetch('http://127.0.0.1:${port}/hook', ...)` at lines 422–426 (and similar calls in the
unauthorized test at lines 467–481), which will fail (connection refused or timeout) and
cause nondeterministic test failures depending on whether the race occurred during that
run.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** tests/capture/drivers/cursor.test.ts
**Line:** 375:383
**Comment:**
*Race Condition: This test repeats the same free-port probe pattern (bind random port, close it, then reuse it), which is racy and can fail nondeterministically when another process takes the port between close and transport startup. Use a single listener lifecycle or dynamic port assignment from the transport/server itself.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| overridePath && (transport.kind === 'file-tail' || transport.kind === 'auto') | ||
| ? createCaptureTransport({ | ||
| ...configuredTransportOptions, | ||
| kind: transport.kind, | ||
| overridePath | ||
| } as CaptureTransportOptions) |
There was a problem hiding this comment.
Suggestion: When an injected custom transport is provided (options.transport with its own start), calling start(overridePath) now bypasses that transport and instantiates a new built-in transport based only on kind. This breaks the session-manager contract for pluggable transports and can start unintended listeners/watchers with missing custom settings. Only recreate a transport for override paths when the original input was CaptureTransportOptions; otherwise keep using the injected transport instance. [api mismatch]
Severity Level: Major ⚠️
- ⚠️ Injected auto/file-tail transports bypassed when overridePath used.
- ⚠️ Session-manager pluggable transport contract violated for custom instances.
- ⚠️ Custom hook/file-tail configuration silently replaced by defaults.Steps of Reproduction ✅
1. Implement a custom `CaptureTransport` with `kind: 'auto'` (following the
`CaptureTransport` interface at `src/capture/capture-transport.ts:36-40`) that has its own
`start()` behavior, e.g. a composite transport similar to `createAutoCaptureTransport()`
in `src/capture/auto-capture-transport.ts:25-79`.
2. Create a session manager using this custom transport instance: `createSessionManager(()
=> null, { transport: customAutoTransport })` as in the pattern used for injecting a
transport at `tests/capture/session-manager.test.ts:7-35`, but with `kind: 'auto'` instead
of `'socket'`.
3. Call `manager.start('/tmp/override.jsonl')` so `overridePath` is truthy; in
`src/capture/session-manager.ts:68-77` `transport` is set to your injected instance, and
in `src/capture/session-manager.ts:156-163` `effectiveTransport` is computed.
4. Because `overridePath` is set and `transport.kind === 'auto'`, the condition at
`src/capture/session-manager.ts:157` is true and `effectiveTransport` becomes
`createCaptureTransport({ ...configuredTransportOptions, kind: transport.kind,
overridePath })` rather than the injected instance; `configuredTransportOptions` for an
injected transport was initialized to `{ kind: 'file-tail' }` at
`src/capture/session-manager.ts:70-73`, so your custom transport’s settings and behavior
are bypassed and a new built-in `auto` transport with default options is started instead.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/capture/session-manager.ts
**Line:** 157:162
**Comment:**
*Api Mismatch: When an injected custom transport is provided (`options.transport` with its own `start`), calling `start(overridePath)` now bypasses that transport and instantiates a new built-in transport based only on `kind`. This breaks the session-manager contract for pluggable transports and can start unintended listeners/watchers with missing custom settings. Only recreate a transport for override paths when the original input was `CaptureTransportOptions`; otherwise keep using the injected transport instance.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| if (source === 'cursor-agent-trace' && !name) { | ||
| return normalizeAgentTraceEntry(entry, sessionId, source, timestamp); | ||
| } | ||
|
|
||
| if (name) { | ||
| return normalizeHookEntry(entry, sessionId, source, timestamp, name); | ||
| } |
There was a problem hiding this comment.
Suggestion: The cursor-agent-trace path only uses normalizeAgentTraceEntry when eventName is empty, but eventName currently falls back to entry.type. Many agent-trace records include a non-hook type, so they get routed to normalizeHookEntry, dropped as unknown, and never parsed as messages. Route cursor-agent-trace entries through normalizeAgentTraceEntry first (or only treat known hook names as hook events) to avoid silently losing valid trace events. [logic error]
Severity Level: Critical 🚨
- ❌ Cursor agent-trace sessions lose non-hook message events.
- ⚠️ Agent-trace fallback unusable for reconstructing full conversations.Steps of Reproduction ✅
1. Start the capture pipeline via `createSessionManager()` in
`src/capture/session-manager.ts:48-51`, letting it build a transport with
`createCaptureTransport()` in `src/capture/capture-transports.ts:141-155` using the
default `'file-tail'` or `'auto'` mode.
2. Ensure Cursor agent-trace JSONL exists under a search root (e.g.
`~/.agent-trace/traces.jsonl`); `createCursorDiscovery()` in
`src/capture/drivers/cursor/discovery.ts:68-101` discovers it and returns a
`DriverDiscoveredSession` with `source: 'cursor-agent-trace'` (line 90).
3. The file-tail transport in `src/capture/transcript-watcher.ts:133-331` calls
`discoverActiveSession()` (`session-discovery.ts:48-95`), then `activateSession()` (lines
279-295) which builds a `CaptureSession` with `source: discovered.source`
(`cursor-agent-trace`) and invokes `context.onSessionStarted(nextSession)` (line 293);
`createSessionManager()` then calls `startSession()` (session-manager.ts:116-143) and
chooses `cursorDriver` via `driverRegistry.getForSource(session.source)`
(`drivers/index.ts:17-19`, `drivers/harness-driver.ts:117-119`).
4. For each JSONL line in the agent-trace file with a non-hook `type` (e.g. `type: "log"`
plus `role` and `content` fields), `createIncrementalParser()` in
`src/capture/incremental-parser.ts:17-44` parses it and passes it to
`cursorDriver.normalizeEntry()` (`src/capture/drivers/cursor/index.ts:6-12`) with `source:
'cursor-agent-trace'`. Inside `normalizeEntry()`
(`src/capture/drivers/cursor/normalizer.ts:324-341`), `eventName()` (lines 37-45) returns
the `type` string as `name`, so the `if (source === 'cursor-agent-trace' && !name)` branch
at line 332 is skipped and the `if (name)` branch at lines 336-338 calls
`normalizeHookEntry()` with a non-hook `name`. `normalizeHookEntry()` falls through to the
`default` case at lines 277-282, logs `cursor_driver.unknown_hook_event`, returns an empty
array, and `normalizeAgentTraceEntry()` (lines 290-322) is never invoked, so these
agent-trace messages are silently dropped and never reach the event buffer.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/capture/drivers/cursor/normalizer.ts
**Line:** 332:338
**Comment:**
*Logic Error: The `cursor-agent-trace` path only uses `normalizeAgentTraceEntry` when `eventName` is empty, but `eventName` currently falls back to `entry.type`. Many agent-trace records include a non-hook `type`, so they get routed to `normalizeHookEntry`, dropped as unknown, and never parsed as messages. Route `cursor-agent-trace` entries through `normalizeAgentTraceEntry` first (or only treat known hook names as hook events) to avoid silently losing valid trace events.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| async function readBody(req: http.IncomingMessage): Promise<string> { | ||
| const chunks: Buffer[] = []; | ||
| for await (const chunk of req) { | ||
| chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); | ||
| } | ||
| return Buffer.concat(chunks).toString('utf8'); | ||
| } |
There was a problem hiding this comment.
Suggestion: The request body reader has no size limit and buffers the entire payload in memory before parsing; a large POST can exhaust memory and destabilize the process. Enforce a maximum body size while streaming (for example reject with 413 once exceeded) instead of unbounded accumulation. [security]
Severity Level: Major ⚠️
- ❌ Oversized POST bodies can crash the capture process.
- ⚠️ Hook receiver susceptible to memory exhaustion from large inputs.Steps of Reproduction ✅
1. Start a hook-receiver transport either in the Electron app (via
`createCaptureTransport()` in `src/capture/capture-transports.ts:141-155` resolving to
`kind: 'hook-receiver'`) or using the standalone `scripts/hooks/live-capture-server.mjs`
(lines 64-71), which calls `createHookReceiverCaptureTransport()`
(`src/capture/hook-receiver-transport.ts:104-260`) with `host: '127.0.0.1'` and a chosen
port.
2. The HTTP server created in `createHookReceiverCaptureTransport.start()` (lines 153-194)
handles POST requests by first checking authorization (lines 171-174), then calling
`resolveSource()` (41-59) and `readBody(req)` (line 177) to obtain the request payload
before passing it to `handlePayload()` (lines 118-151).
3. `readBody()` (`src/capture/hook-receiver-transport.ts:81-87`) iterates `for await
(const chunk of req)` over the entire request, pushing each chunk into an in-memory
`chunks: Buffer[]` array, and finally calls `Buffer.concat(chunks)` to form a single
buffer for the full body. There is no size limit or early rejection; any client that sends
a very large POST (hundreds of MB or more) to the hook endpoint will cause `chunks` and
the concatenated buffer to grow without bound.
4. As the oversized body is buffered, the Node process’s heap grows until garbage
collection and memory limits are exceeded, at which point the capture process (Electron
app or `live-capture-server.mjs`) becomes sluggish or crashes, dropping all ongoing
sessions. Because the hook listener can be exposed beyond loopback by setting
`SHADOW_HOOK_RECEIVER_HOST` (capture-transports.ts:110-121), a misconfigured or malicious
client can reliably trigger this unbounded buffering path and exhaust memory.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/capture/hook-receiver-transport.ts
**Line:** 81:87
**Comment:**
*Security: The request body reader has no size limit and buffers the entire payload in memory before parsing; a large POST can exhaust memory and destabilize the process. Enforce a maximum body size while streaming (for example reject with 413 once exceeded) instead of unbounded accumulation.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| const sessionsStarted = new Set<string>(); | ||
| let stopped = false; | ||
|
|
||
| const handlePayload = async ( | ||
| rawBody: string, | ||
| source: EventSource | ||
| ): Promise<{ ok: true; sessionId: string } | { ok: false; error: string }> => { | ||
| const trimmed = rawBody.trim(); | ||
| if (!trimmed) { | ||
| return { ok: false, error: 'empty body' }; | ||
| } | ||
|
|
||
| let parsed: Record<string, unknown>; | ||
| try { | ||
| parsed = JSON.parse(trimmed) as Record<string, unknown>; | ||
| } catch { | ||
| return { ok: false, error: 'invalid JSON' }; | ||
| } | ||
|
|
||
| const sessionId = resolveSessionId(parsed, fallbackSessionId); | ||
| const session = buildSession( | ||
| sessionId, | ||
| source, | ||
| options, | ||
| typeof parsed.transcript_path === 'string' ? parsed.transcript_path : undefined | ||
| ); | ||
|
|
||
| if (!sessionsStarted.has(sessionId)) { | ||
| sessionsStarted.add(sessionId); | ||
| await context.onSessionStarted(session); | ||
| } |
There was a problem hiding this comment.
Suggestion: Session deduplication only keys on sessionId, so a new stream with the same id but different source (or updated path metadata) will not trigger onSessionStarted, leaving the session manager on the old driver/session metadata. Include source (and preferably path) in the dedupe key so cross-harness or metadata-changing streams cannot be merged incorrectly. [api mismatch]
Severity Level: Major ⚠️
- ❌ Cross-harness streams normalized with incorrect driver implementation.
- ⚠️ Session metadata stale when hook source or path changes.Steps of Reproduction ✅
1. Configure capture to use the hook receiver (e.g.
`SHADOW_CAPTURE_TRANSPORT=hook-receiver` or `auto`) so
`resolveCaptureTransportOptionsFromEnv()` in `src/capture/capture-transports.ts:61-139`
returns a `HookReceiverCaptureTransportOptions`, and `createCaptureTransport()` at lines
141-155 constructs `createHookReceiverCaptureTransport()`
(`src/capture/hook-receiver-transport.ts:104-260`).
2. Start the session manager via `createSessionManager()`
(`src/capture/session-manager.ts:48-51`), whose `start()` method (lines 145-205) calls
`transport.start()` with `onSessionStarted` and `onChunk` callbacks. On the first HTTP
POST from a Cursor hook script, `createHookReceiverCaptureTransport.start()` builds a
`CaptureSession` (lines 134-140 via `buildSession()` at 89-101) and, because
`sessionsStarted` (line 115) does not yet contain `sessionId`, calls
`context.onSessionStarted(session)` (lines 142-145).
3. In `onSessionStarted` inside `session-manager.ts:165-184`, the manager logs the new
session and calls `startSession(session)` (lines 116-143), which sets `activeSession` and
picks the harness driver via `driverRegistry.getForSource(session.source)`
(`src/capture/drivers/index.ts:17-19`), typically `cursorDriver` for `source:
'cursor-hook'`. Subsequent POSTs with the same `conversation_id` and source `cursor-hook`
reuse this driver and session metadata.
4. Now send a new stream to the same hook receiver using the same `conversation_id` but a
different harness source (e.g. `x-shadow-source: claude-hook` or a different
transcript_path) so `resolveSource()` (`hook-receiver-transport.ts:41-59`) returns a
different `source` and `buildSession()` constructs a session with changed `source` or
`path`. In `handlePayload()` (lines 118-151), `resolveSessionId()` (31-39) still produces
the same `sessionId`, so `sessionsStarted.has(sessionId)` is true (line 142) and
`context.onSessionStarted(session)` is not called. The session manager’s `onChunk` handler
(lines 198-203) sees `activeSession.sessionId === session.sessionId` and also skips
`startSession(session)`, leaving `activeSession.source` and `activeSession.path` pointing
at the old driver/metadata. As a result, chunks from the new harness are normalized by the
wrong driver (e.g. Cursor normalizer for Claude-format JSONL), and snapshot metadata
(`buildSnapshot()` in `session-manager.ts:79-108`) continues to show the stale path and
source.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/capture/hook-receiver-transport.ts
**Line:** 115:145
**Comment:**
*Api Mismatch: Session deduplication only keys on `sessionId`, so a new stream with the same id but different `source` (or updated path metadata) will not trigger `onSessionStarted`, leaving the session manager on the old driver/session metadata. Include `source` (and preferably path) in the dedupe key so cross-harness or metadata-changing streams cannot be merged incorrectly.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fixOnly recreate file-tail/auto transports from configured options (never replace an injected CaptureTransport). Agent-trace sources always go through the agent-trace normalizer so non-hook `type` fields are kept. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| const port = Number.parseInt(raw, 10); | ||
| if (!Number.isInteger(port) || port < 1 || port > 65_535) { | ||
| throw new Error(`Invalid SHADOW_HOOK_RECEIVER_PORT: ${raw}`); |
There was a problem hiding this comment.
Suggestion: Port parsing is too permissive because Number.parseInt accepts partial numeric strings (for example 9477abc becomes 9477), so malformed environment values are silently accepted instead of rejected. Use strict numeric validation before parsing so invalid port values fail fast. [incorrect condition logic]
Severity Level: Major ⚠️
❌ Hook receiver silently accepts malformed SHADOW_HOOK_RECEIVER_PORT values.
⚠️ Misconfigured hook ports can bind on unintended TCP ports.
⚠️ Socket transport similarly accepts partial SHADOW_CAPTURE_SOCKET_PORT.
⚠️ Operators get no clear error on malformed port env.Steps of Reproduction ✅
1. In `src/electron/start-main-process.ts:138-152`, the Electron main process calls
`resolveCaptureTransportOptionsFromEnv()` with no arguments to configure live capture, and
passes its result as the `transport` option into `createSessionManager()` (see
`start-main-process.ts:148-152`).
2. Because `SHADOW_CAPTURE_TRANSPORT` is unset, `resolveCaptureTransportOptionsFromEnv()`
in `src/capture/capture-transports.ts:61-66` uses `DEFAULT_CAPTURE_TRANSPORT = 'auto'`
(line 22) and enters the `'auto'` case at lines 122-135, which calls
`parseOptionalPort(env.SHADOW_HOOK_RECEIVER_PORT, DEFAULT_HOOK_RECEIVER_PORT)` at line
129.
3. Configure the environment before starting the app with a malformed port value such as
`SHADOW_HOOK_RECEIVER_PORT=9477abc` (or `5000-port`) so that
`env.SHADOW_HOOK_RECEIVER_PORT` contains a partial number followed by non-digits;
`parseOptionalPort` (lines 50-58) executes `const port = Number.parseInt(raw, 10);` (line
54) and only checks `Number.isInteger(port) || port < 1 || port > 65_535` (line 55), which
all pass for `9477abc` because `Number.parseInt('9477abc', 10)` returns `9477`.
4. Observe that no error is thrown even though `SHADOW_HOOK_RECEIVER_PORT` is malformed:
`resolveCaptureTransportOptionsFromEnv()` returns `hookPort: 9477` (lines 125-133),
`createCaptureTransport()` (lines 141-154) builds an `auto` transport, and
`createAutoCaptureTransport()` in `src/capture/auto-capture-transport.ts:45-62` starts a
hook-receiver on port `9477`, silently accepting the invalid environment value instead of
failing fast, which can mislead operators about configuration correctness and cause
unexpected binding or connection behaviour.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/capture/capture-transports.ts
**Line:** 54:56
**Comment:**
*Incorrect Condition Logic: Port parsing is too permissive because `Number.parseInt` accepts partial numeric strings (for example `9477abc` becomes `9477`), so malformed environment values are silently accepted instead of rejected. Use strict numeric validation before parsing so invalid port values fail fast.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix| const roots = options.searchRoots ?? [process.cwd(), os.homedir()]; | ||
| const sessions: DriverDiscoveredSession[] = []; | ||
|
|
||
| for (const root of roots) { | ||
| const hooksPath = path.join(root, '.cursor', 'hooks.json'); | ||
| if (await pathExists(hooksPath)) { | ||
| logger.debug('capture', 'cursor_discovery.hooks_present', { hooksPath }); | ||
| } | ||
|
|
||
| const traces = await findAgentTraceFiles(root); |
There was a problem hiding this comment.
Suggestion: The default discovery path scans both cwd and the entire home directory root and then stats every first-level entry each discovery run, which creates repeated high I/O overhead in long-running capture mode. This should be narrowed (or made opt-in/cached) to avoid periodic performance degradation on machines with large home directories. [performance]
Severity Level: Major ⚠️
⚠️ Auto capture runs discovery every 30 seconds by default.
⚠️ Cursor discovery scans process cwd and full home directory.
⚠️ Large or network home directories incur repeated stat operations.
⚠️ Periodic filesystem scans can degrade live capture responsiveness.Steps of Reproduction ✅
1. Start the main Electron app so `startMainProcess()` in
`src/electron/start-main-process.ts:138-201` runs; it constructs a `SessionManager` via
`createSessionManager()` with `transport: resolveCaptureTransportOptionsFromEnv()` (lines
148-152), which by default returns an `'auto'` transport (verified by
`tests/capture/capture-transport-env.test.ts:11-26`).
2. `createSessionManager()` in `src/capture/session-manager.ts:70-77` calls
`createCaptureTransport(configuredTransportOptions)` when `options.transport` is not
already a started transport; for the `'auto'` kind, `createCaptureTransport()` in
`src/capture/capture-transports.ts:141-155` returns an auto transport that internally
creates a file-tail transport via `createFileTailCaptureTransport()` (see
`src/capture/auto-capture-transport.ts:34-42`).
3. Inside `createFileTailCaptureTransport()` in
`src/capture/transcript-watcher.ts:133-141`, the `start()` implementation defines
`runDiscovery()` (lines 38-46) which calls `discoverActiveSession(options.overridePath, {
overrideSource: options.overrideSource })` from `src/capture/session-discovery.ts:48-51`,
then schedules `runDiscovery()` to be invoked every `discoveryIntervalMs` via
`setInterval` (lines 50-52), defaulting to `DEFAULT_DISCOVERY_INTERVAL_MS = 30_000` ms
(line 43).
4. `discoverActiveSession()` in `src/capture/session-discovery.ts:72-80` iterates all
registered harness drivers; for the Cursor driver, `cursorDriver` in
`src/capture/drivers/cursor/index.ts:6-12` exposes `discovery: cursorDiscovery`, and
`cursorDiscovery` is created by `createCursorDiscovery()` in
`src/capture/drivers/cursor/discovery.ts:68-103` with default `searchRoots ??
[process.cwd(), os.homedir()]` (line 73). On every discovery tick, `discoverSessions()`
loops these roots (lines 73-76), and `findAgentTraceFiles()` (lines 30-60) performs
`readdir(root)` (lines 43-47) and then a `stat()` call for `path.join(root, entry,
'.agent-trace', 'traces.jsonl')` for every first-level entry under each root (lines
49-55). When `root` is `os.homedir()`, long-running capture on machines with large or
network-mounted home directories will repeatedly scan the entire first-level of the home
directory every 30 seconds, creating ongoing filesystem I/O overhead even when no Cursor
traces are present.(Use Cmd/Ctrl + Click for best experience)
Prompt for AI Agent 🤖
This is a comment left during a code review.
**Path:** src/capture/drivers/cursor/discovery.ts
**Line:** 73:82
**Comment:**
*Performance: The default discovery path scans both `cwd` and the entire home directory root and then stats every first-level entry each discovery run, which creates repeated high I/O overhead in long-running capture mode. This should be narrowed (or made opt-in/cached) to avoid periodic performance degradation on machines with large home directories.
Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fixThere was a problem hiding this comment.
Actionable comments posted: 19
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/capture/session-manager.ts (1)
68-71: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winPreserve the existing
autotransport settings when applyingoverridePath. Rebuilding with{ kind: 'auto', overridePath }drops the original hook-receiver config (hookHost,hookPort,unixSocketPath,sharedToken, etc.), which resets the receiver to defaults and can remove token enforcement. Merge the current transport options instead of starting from scratch.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/capture/session-manager.ts` around lines 68 - 71, Update the transport construction in the session-manager flow to preserve all existing auto transport settings when applying overridePath. When rebuilding the transport, merge the current options.transport configuration with the override rather than creating a new `{ kind: 'auto', overridePath }` object, retaining hookHost, hookPort, unixSocketPath, sharedToken, and related receiver settings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.cursor/hooks.json:
- Around line 4-14: Update the hook definitions in the Cursor hooks
configuration so Windows invokes the PowerShell forwarder via pwsh and Unix
continues using scripts/hooks/forward-to-shadow.sh. Ensure every affected hook,
including sessionStart, sessionEnd, beforeSubmitPrompt, and tool/agent lifecycle
hooks, is platform-aware or scope the configuration by platform rather than
leaving Windows pointed at the shell script.
In @.github/workflows/ci.yml:
- Around line 46-48: Pin the actions in the new job to immutable commit SHAs
instead of version tags, updating both actions/checkout and actions/setup-node
while preserving their current major-version behavior. Use the repository’s
established SHA-pinning convention if one exists.
- Around line 41-53: Update the actions/checkout step in the
cursor-cli-hook-selftest job to set persist-credentials to false, ensuring the
job’s third-party install script cannot access the persisted GITHUB_TOKEN while
leaving the rest of the workflow unchanged.
In `@docs/domain-events.md`:
- Around line 42-60: The Cursor hook mapping table incorrectly groups
afterFileEdit with ID-gated specialized hooks. Update the afterFileEdit entry to
state that it requires file_path and emits the synthetic Write
tool_started/tool_completed pair, deriving edit:<path> when tool_use_id is
absent; limit the tool_use_id requirement to the shell, MCP, and read-file
events.
In `@docs/getting-started.md`:
- Around line 84-94: Update the SHADOW_CAPTURE_TRANSPORT entry to list only
auto, file-tail, and hook-receiver; document Cursor under the
SHADOW_CAPTURE_SOURCE or related source/harness setting instead, without
presenting it as a transport value.
In `@scripts/hooks/ci-cursor-cli-self-test.sh`:
- Around line 52-53: Add tsx as a pinned devDependency in package.json and
update the lockfile accordingly, then change the command launching
live-capture-server.mjs to use the project-installed dependency rather than npx
fetching tsx at runtime. Keep the existing port, output, logging, and
background-process behavior unchanged.
In `@scripts/hooks/live-capture-server.mjs`:
- Around line 45-62: Extract the shared conversation_id/session_id fallback and
cursorDriver.normalizeEntry(entry, sessionId, 'cursor-hook') behavior into a
helper under src/capture/drivers/cursor, then reuse it in
scripts/hooks/live-capture-server.mjs lines 45-62 and
tests/live/cursor-hooks-live.test.ts lines 89-94, removing the duplicated inline
logic at both sites.
In `@scripts/hooks/README.md`:
- Around line 60-62: Update the `gh secret set` example in the README to omit
the `--body` option and pipe or otherwise provide `CURSOR_API_KEY` through
standard input, preserving the existing repository target and secret name.
In `@scripts/hooks/reload-exec-daemon-hooks.sh`:
- Around line 18-30: No code change is required; retain the existing token
discovery behavior and treat the /proc/<pid>/cmdline exposure as an acknowledged
upstream exec-daemon characteristic.
- Around line 32-44: Replace predictable temporary paths with mktemp-generated
paths in reload-exec-daemon-hooks.sh lines 32-44 for the request and response
files, and register trap-based cleanup on exit. In ci-cursor-cli-self-test.sh
lines 26-29, generate unique default capture and log paths instead of fixed /tmp
names; in lines 73-96, likewise replace /tmp/cursor-ci-agent-output.txt with an
mktemp-generated path.
- Around line 21-25: Update the CMDLINE assignment in the TOKEN fallback block
to quote the pgrep/head process-ID substitution when constructing the /proc
cmdline path, eliminating word-splitting while preserving the existing graceful
failure behavior.
In `@src/capture/capture-transports.ts`:
- Around line 50-59: Update parseOptionalPort to accept a variableName parameter
like parsePort and use it in the invalid-port error message instead of
hardcoding SHADOW_HOOK_RECEIVER_PORT. Update both existing call sites to pass
their corresponding environment variable name.
- Around line 109-135: Extract the duplicated hook-receiver environment mapping
from the transport-selection logic into a shared helper, then reuse it in both
the `hook-receiver`/`hooks`/`cursor` branch and the `auto`/`both` branch.
Preserve the existing defaults and trimming behavior for host, port,
unixSocketPath, sharedToken, defaultSource, sessionId, and sessionLabel, while
retaining each branch’s transport-specific fields.
In `@src/capture/hook-receiver-transport.ts`:
- Around line 81-87: Cap payload accumulation in readBody and the analogous
Unix-socket read loop with a shared maximum payload size. Track bytes before
appending each chunk, reject or terminate the read when the limit is exceeded,
and ensure oversized input never reaches handlePayload or remains fully buffered
in memory.
- Around line 206-223: The Unix-socket handler in the net.createServer callback
bypasses sharedToken authorization by calling handlePayload directly. Apply the
existing authorize() guard to Unix-socket requests before invoking
handlePayload, preserving the current payload handling only for authorized
clients; alternatively, explicitly document and enforce that unixSocketPath
intentionally relies solely on filesystem permissions.
In `@src/capture/transcript-watcher.ts`:
- Around line 298-300: Update SessionManager.start and its replacement transport
flow to preserve options.overrideSource when recreating file-tail/auto
transports for an overridePath, so Cursor traces retain their original source
and driver normalization. Propagate the source from discoverActiveSession
through the SessionManager override path, and add a SessionManager-level
regression test covering this behavior.
In `@tests/capture/drivers/cursor.test.ts`:
- Around line 486-511: Remove the chmodSync call from the forward-to-shadow.sh
test and ensure the script’s executable mode is committed in the repository.
Preserve the existing executable and fail-open assertions without mutating
tracked files or shared filesystem state.
In `@tests/fixtures/README.md`:
- Around line 20-29: Update the earlier transcript-format guidance in
tests/fixtures/README.md to state that each fixture uses its harness’s raw
observed-agent wire format, including Cursor hook JSON where applicable. Ensure
the guidance no longer implies all fixtures must use Claude Code JSONL, while
preserving the one-JSON-object-per-line requirement where applicable.
In `@tests/integration/e2e-cursor-hooks-to-state.test.ts`:
- Around line 49-61: Replace the fixed 50 ms delay in the incremental parser
test with the existing deterministic waitFor-style polling used by sibling
cursor tests, waiting until buffer.getAll() contains the expected events. Await
each buffer.push operation or otherwise propagate push failures so asynchronous
persistence errors are not silently discarded, while preserving the final
event-count assertion.
---
Outside diff comments:
In `@src/capture/session-manager.ts`:
- Around line 68-71: Update the transport construction in the session-manager
flow to preserve all existing auto transport settings when applying
overridePath. When rebuilding the transport, merge the current options.transport
configuration with the override rather than creating a new `{ kind: 'auto',
overridePath }` object, retaining hookHost, hookPort, unixSocketPath,
sharedToken, and related receiver settings.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1a137e00-7c17-44bb-971e-965a5c854d5a
📒 Files selected for processing (47)
.cursor/hooks.json.github/workflows/ci.ymlREADME.mddocs/architecture.mddocs/domain-events.mddocs/getting-started.mddocs/history/log.mddocs/north-star.mddocs/plans/plan-multi-harness-mvp.mddocs/plans/roadmap.mddocs/research/harness-ingestion-matrix.mdpackage.jsonscripts/hooks/README.mdscripts/hooks/ci-cursor-cli-self-test.shscripts/hooks/cursor-hooks.example.jsonscripts/hooks/forward-to-shadow-debug.shscripts/hooks/forward-to-shadow.ps1scripts/hooks/forward-to-shadow.shscripts/hooks/live-capture-server.mjsscripts/hooks/reload-exec-daemon-hooks.shsrc/capture/auto-capture-transport.tssrc/capture/capture-transport.tssrc/capture/capture-transports.tssrc/capture/drivers/cursor/capabilities.tssrc/capture/drivers/cursor/discovery.tssrc/capture/drivers/cursor/index.tssrc/capture/drivers/cursor/normalizer.tssrc/capture/drivers/index.tssrc/capture/hook-receiver-transport.tssrc/capture/http-stream-transport.tssrc/capture/session-manager.tssrc/capture/socket-transport.tssrc/capture/transcript-watcher.tssrc/capture/websocket-transport.tssrc/shared/derive.tssrc/shared/schema.tstests/capture/auto-capture-transport.test.tstests/capture/capture-transport-env.test.tstests/capture/drivers/cursor.test.tstests/capture/drivers/harness-driver.test.tstests/capture/file-tail-source.test.tstests/capture/hook-receiver-unix.test.tstests/fixtures/README.mdtests/fixtures/transcripts/cursor-hooks.jsonltests/integration/e2e-cursor-hooks-to-state.test.tstests/live/cursor-hooks-live.test.tstests/schema.test.ts
| "sessionStart": [{ "command": "scripts/hooks/forward-to-shadow.sh" }], | ||
| "sessionEnd": [{ "command": "scripts/hooks/forward-to-shadow.sh" }], | ||
| "beforeSubmitPrompt": [{ "command": "scripts/hooks/forward-to-shadow.sh" }], | ||
| "preToolUse": [{ "command": "scripts/hooks/forward-to-shadow.sh" }], | ||
| "postToolUse": [{ "command": "scripts/hooks/forward-to-shadow.sh" }], | ||
| "postToolUseFailure": [{ "command": "scripts/hooks/forward-to-shadow.sh" }], | ||
| "subagentStart": [{ "command": "scripts/hooks/forward-to-shadow.sh" }], | ||
| "subagentStop": [{ "command": "scripts/hooks/forward-to-shadow.sh" }], | ||
| "afterAgentResponse": [{ "command": "scripts/hooks/forward-to-shadow.sh" }], | ||
| "afterAgentThought": [{ "command": "scripts/hooks/forward-to-shadow.sh" }], | ||
| "stop": [{ "command": "scripts/hooks/forward-to-shadow.sh" }] |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
git ls-files .cursor scripts/hooks cursor-hooks.example.jsonRepository: Coldaine/AgentVisualCrazy
Length of output: 479
🏁 Script executed:
sed -n '1,220p' .cursor/hooks.json && printf '\n--- cursor-hooks.example.json ---\n' && sed -n '1,260p' scripts/hooks/cursor-hooks.example.json && printf '\n--- README.md ---\n' && sed -n '1,260p' scripts/hooks/README.mdRepository: Coldaine/AgentVisualCrazy
Length of output: 6556
🏁 Script executed:
ast-grep outline scripts/hooks/forward-to-shadow.sh && printf '\n---\n' && ast-grep outline scripts/hooks/forward-to-shadow.ps1 && printf '\n--- refs ---\n' && rg -n "forward-to-shadow\.ps1|forward-to-shadow\.sh|hooks.json" scripts/hooks .cursor -g '!**/node_modules/**'Repository: Coldaine/AgentVisualCrazy
Length of output: 4996
🌐 Web query:
Cursor hooks Windows command execution .sh shell interpreter wrapper documentation
💡 Result:
On Windows, Cursor’s command execution and hook systems heavily rely on PowerShell, often using internal wrapper scripts that can behave inconsistently due to environment or configuration constraints [1][2][3][4][5]. Key characteristics of Windows command execution in Cursor include: 1. PowerShell Dependency: Cursor’s agent and hook execution layers on Windows frequently default to PowerShell, often ignoring user-configured terminal profiles (such as Nushell or Git Bash) [1][2][4]. This can cause issues in environments with restricted PowerShell policies, such as Constrained Language Mode [4]. 2. Wrapper Scripts: Commands executed through the agent are often wrapped in temporary PowerShell scripts (e.g., ps-script-*.ps1) [3][4][5]. These scripts are susceptible to errors, such as parsing failures or environment variable conflicts, which do not occur when running commands directly in the standard integrated terminal [3][5]. 3. Hook Execution: Cursor hooks (defined in hooks.json) are intended to run as command-based scripts [6]. On Windows, however, users frequently report that these hooks fail at the launcher level [7][8]. To maximize success, it is recommended to explicitly invoke PowerShell within the hook command to bypass potential launcher issues: "command": "powershell -NoProfile -ExecutionPolicy Bypass -Command "Write-Output '{\"continue\":true}'"" [7] 4. Troubleshooting and Workarounds: - Legacy Terminal Tool: If agent shell commands or hooks fail on Windows, enabling the "Legacy Terminal Tool" (found in Settings > Agents > Inline Editing & Terminal) often resolves issues related to the modern, sandboxed wrapper implementations [9][3][4][5]. - Explicit Shell Invocation: When using scripts (like MCP wrappers or hooks), avoid relying on implicit execution. Instead, explicitly call the shell (e.g., "command": "bash", "args": ["-c", "./script.sh"]) to ensure cross-platform compatibility and correct path handling [10]. - Debugging: If hooks or commands fail, inspect the specific "Hooks" output panel (View > Output > Hooks) to see if the launcher is failing before the script even executes [7][11][8]. There is currently no native flag or configuration option to force a non-PowerShell shell for all agent-level command execution on Windows [1]. Users are generally advised to use the Legacy Terminal Tool as the primary workaround for these platform-specific integration issues [9][3][4][5].
Citations:
- 1: https://forum.cursor.com/t/agent-cli-on-windows-no-way-to-configure-shell-hardcoded-to-powershell-no-shell-flag-or-config-option/151858
- 2: https://forum.cursor.com/t/ide-agent-ignores-terminal-integrated-defaultprofile-windows-always-uses-powershell-even-with-nushell-bash-configured/151859/1
- 3: https://forum.cursor.com/t/agent-shell-broken-after-update-powershell-wrapper-script-parsing-error/151887/1
- 4: https://forum.cursor.com/t/agent-mode-fails-on-windows-with-powershell-constrained-language-mode/138398
- 5: https://forum.cursor.com/t/agent-terminal-wrapper-an-item-with-the-same-key-has-already-been-added-when-capturing-env/151079
- 6: https://cursor.com/docs/hooks.md
- 7: https://forum.cursor.com/t/hooks-in-windows/150377
- 8: https://forum.cursor.com/t/cursor-hooks-on-windows/140293/1
- 9: https://forum.cursor.com/t/windows-agent-shell-tool-returns-no-exit-status-and-produces-no-terminal-output-files-integrated-terminal-works-perfectly/163565
- 10: https://forum.cursor.com/t/when-running-mcp-wrapper-script-in-windows-gives-error-no-such-file/150207
- 11: https://forum.cursor.com/t/project-level-hooks-fail-to-execute-on-windows/144940
🌐 Web query:
site:cursor.com/docs/hooks Windows .sh command hooks PowerShell explicit interpreter
💡 Result:
To execute a.sh script on Windows using PowerShell as an explicit interpreter within a command-based hook, you must modify the command string to invoke the PowerShell executable directly, passing the script file as an argument. Because standard command hooks (such as those used in Cursor) interpret the command property as a shell string, you can override the default execution behavior by prefixing your script path with pwsh or powershell.exe. For example, update your hook configuration as follows: { "command": "pwsh -File./scripts/your-script.sh" } In this configuration, pwsh (PowerShell Core) or powershell.exe (Windows PowerShell) is invoked as the primary process, which then interprets and executes the specified.sh file. Ensure that the script itself is written in PowerShell syntax if it is intended to be executed by the PowerShell engine, as a.sh file containing Bash or POSIX shell syntax will not be natively executable by PowerShell. If the script requires a bash environment on Windows, ensure that a bash interpreter (like Git Bash or WSL) is available in your system PATH and call it explicitly instead: { "command": "bash./scripts/your-script.sh" }
Citations:
🏁 Script executed:
sed -n '1,220p' scripts/hooks/forward-to-shadow.sh && printf '\n--- ps1 ---\n' && sed -n '1,220p' scripts/hooks/forward-to-shadow.ps1 && printf '\n--- debug ---\n' && sed -n '1,80p' scripts/hooks/forward-to-shadow-debug.shRepository: Coldaine/AgentVisualCrazy
Length of output: 3203
Wire the Windows forwarder or scope this config to Unix
.cursor/hooks.json still points every hook at scripts/hooks/forward-to-shadow.sh, so Windows Cursor installs won’t use the new forward-to-shadow.ps1. Add an explicit pwsh -File scripts/hooks/forward-to-shadow.ps1 path for Windows, or split the install/config by platform; otherwise the committed hooks remain Unix-only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.cursor/hooks.json around lines 4 - 14, Update the hook definitions in the
Cursor hooks configuration so Windows invokes the PowerShell forwarder via pwsh
and Unix continues using scripts/hooks/forward-to-shadow.sh. Ensure every
affected hook, including sessionStart, sessionEnd, beforeSubmitPrompt, and
tool/agent lifecycle hooks, is platform-aware or scope the configuration by
platform rather than leaving Windows pointed at the shell script.
| cursor-cli-hook-selftest: | ||
| name: Cursor CLI hook self-test | ||
| runs-on: ubuntu-latest | ||
| needs: test | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: 20 | ||
| cache: npm | ||
| cache-dependency-path: package-lock.json | ||
|
|
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
Add persist-credentials: false to checkout in the new job.
This job runs a third-party install script (curl https://cursor.com/install -fsS | bash) and uses CURSOR_API_KEY. Leaving the default actions/checkout@v4 persists the ephemeral GITHUB_TOKEN in the local git config for the rest of the job, which is unnecessary exposure if that untrusted code were to read it.
🔒 Suggested fix
- uses: actions/checkout@v4
+ with:
+ persist-credentials: false📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| cursor-cli-hook-selftest: | |
| name: Cursor CLI hook self-test | |
| runs-on: ubuntu-latest | |
| needs: test | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: package-lock.json | |
| cursor-cli-hook-selftest: | |
| name: Cursor CLI hook self-test | |
| runs-on: ubuntu-latest | |
| needs: test | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: 20 | |
| cache: npm | |
| cache-dependency-path: package-lock.json |
🧰 Tools
🪛 zizmor (1.26.1)
[warning] 46-46: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[warning] 41-81: overly broad permissions (excessive-permissions): default permissions used due to no permissions: block
(excessive-permissions)
[error] 46-46: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
[error] 48-48: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml around lines 41 - 53, Update the actions/checkout
step in the cursor-cli-hook-selftest job to set persist-credentials to false,
ensuring the job’s third-party install script cannot access the persisted
GITHUB_TOKEN while leaving the rest of the workflow unchanged.
Source: Linters/SAST tools
| - uses: actions/checkout@v4 | ||
|
|
||
| - uses: actions/setup-node@v4 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win
Actions not pinned to a commit SHA.
Static analysis flags actions/checkout@v4 and actions/setup-node@v4 here as unpinned, "required by blanket policy." The pre-existing test job (unchanged lines 14-20) uses the same unpinned convention, so this may just mirror existing repo practice rather than a new regression — but worth confirming whether the policy is meant to apply going forward to new jobs like this one, especially since it now runs an unauthenticated curl | bash install.
🧰 Tools
🪛 zizmor (1.26.1)
[warning] 46-46: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[error] 46-46: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
[error] 48-48: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml around lines 46 - 48, Pin the actions in the new
job to immutable commit SHAs instead of version tags, updating both
actions/checkout and actions/setup-node while preserving their current
major-version behavior. Use the repository’s established SHA-pinning convention
if one exists.
Source: Linters/SAST tools
| ### Cursor hook → CanonicalEvent map | ||
|
|
||
| | Cursor `hook_event_name` | Canonical `kind` | | ||
| |--------------------------|------------------| | ||
| | `sessionStart` | `session_started` | | ||
| | `sessionEnd` | `session_ended` | | ||
| | `beforeSubmitPrompt` | `message` (`actor: user`) | | ||
| | `afterAgentResponse` / `afterAgentThought` | `message` (`actor: assistant`; thought sets `thinking: true`) | | ||
| | `preToolUse` | `tool_started` | | ||
| | `postToolUse` | `tool_completed` | | ||
| | `postToolUseFailure` | `tool_failed` | | ||
| | `afterFileEdit` | synthetic `tool_started` + `tool_completed` (Write; stable id `edit:<path>`) | | ||
| | `beforeShellExecution` / `beforeMCPExecution` / `beforeReadFile` / `afterShellExecution` / `afterMCPExecution` | same as pre/post **only when** `tool_use_id` is present (skipped otherwise to avoid double-counting with the generic family) | | ||
| | `subagentStart` / `subagentStop` | `agent_spawned` / `agent_completed` (`actor` + `payload.agentId` = Cursor `subagent_id`) | | ||
| | `stop` | `agent_idle` | | ||
|
|
||
| Stock `.cursor/hooks.json` registers the generic tool family only (`preToolUse` / | ||
| `postToolUse` / `postToolUseFailure`), not the specialized shell/MCP/file twins. | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Document afterFileEdit separately from ID-gated specialized hooks.
Line 54 says afterFileEdit is skipped without tool_use_id, but the Cursor normalizer emits a synthetic Write start/completion pair when file_path is present and derives edit:<path> if no ID exists. Update the table so only the shell/MCP/read specialized events require tool_use_id; afterFileEdit requires file_path instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/domain-events.md` around lines 42 - 60, The Cursor hook mapping table
incorrectly groups afterFileEdit with ID-gated specialized hooks. Update the
afterFileEdit entry to state that it requires file_path and emits the synthetic
Write tool_started/tool_completed pair, deriving edit:<path> when tool_use_id is
absent; limit the tool_use_id requirement to the shell, MCP, and read-file
events.
| Optional env vars: | ||
|
|
||
| | Variable | Default | Purpose | | ||
| |----------|---------|---------| | ||
| | `SHADOW_CAPTURE_TRANSPORT` | `auto` | `auto` / `file-tail` / `hook-receiver` (`cursor`) | | ||
| | `SHADOW_CAPTURE_SOURCE` | _(unset)_ | Override `EventSource` for file-tail / stream transports | | ||
| | `SHADOW_HOOK_RECEIVER_PORT` | `9477` | Loopback port for the hook receiver | | ||
| | `SHADOW_HOOK_RECEIVER_SOCKET` | _(unset)_ | Optional Unix socket path for the receiver | | ||
| | `SHADOW_HOOK_TOKEN` | _(unset)_ | Shared token required via `X-Shadow-Token` | | ||
| | `SHADOW_HOOK_URL` | `http://127.0.0.1:9477/hook` | Forwarder target (set in the Cursor environment if non-default) | | ||
|
|
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Remove cursor from the transport values.
SHADOW_CAPTURE_TRANSPORT is documented elsewhere as auto, file-tail, or hook-receiver; cursor is a harness/source, not a transport. Listing it here can cause users to configure an invalid value. Document Cursor under the source/harness setting instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/getting-started.md` around lines 84 - 94, Update the
SHADOW_CAPTURE_TRANSPORT entry to list only auto, file-tail, and hook-receiver;
document Cursor under the SHADOW_CAPTURE_SOURCE or related source/harness
setting instead, without presenting it as a transport value.
| let unixServer: net.Server | null = null; | ||
| if (options.unixSocketPath) { | ||
| try { | ||
| await unlink(options.unixSocketPath).catch(() => undefined); | ||
| unixServer = net.createServer((socket) => { | ||
| void (async () => { | ||
| const chunks: Buffer[] = []; | ||
| for await (const chunk of socket) { | ||
| chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); | ||
| } | ||
| const body = Buffer.concat(chunks).toString('utf8'); | ||
| await handlePayload(body, options.defaultSource ?? 'cursor-hook'); | ||
| socket.end('{}\n'); | ||
| })().catch((error) => { | ||
| logger.warn('capture', 'transport.hook_receiver.unix_error', { error }); | ||
| socket.destroy(); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== file outline ==\n'
ast-grep outline src/capture/hook-receiver-transport.ts --view expanded || true
printf '\n== relevant sections ==\n'
sed -n '1,280p' src/capture/hook-receiver-transport.ts | cat -n
printf '\n== sharedToken search ==\n'
rg -n "sharedToken|unix socket|unixSocketPath|X-Shadow-Token|Bearer" src . -g '!**/node_modules/**' || trueRepository: Coldaine/AgentVisualCrazy
Length of output: 16956
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo 'hello'Repository: Coldaine/AgentVisualCrazy
Length of output: 169
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,260p' src/capture/hook-receiver-transport.ts | nl -ba | sed -n '1,260p'Repository: Coldaine/AgentVisualCrazy
Length of output: 203
Unix-socket path bypasses sharedToken authorization.
authorize() is only used by the HTTP handler; the Unix-socket server calls handlePayload() directly. When sharedToken is set, this lets any local process that can access the socket inject payloads without the token. Apply the same guard here, or document that unixSocketPath relies only on filesystem permissions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/capture/hook-receiver-transport.ts` around lines 206 - 223, The
Unix-socket handler in the net.createServer callback bypasses sharedToken
authorization by calling handlePayload directly. Apply the existing authorize()
guard to Unix-socket requests before invoking handlePayload, preserving the
current payload handling only for authorized clients; alternatively, explicitly
document and enforce that unixSocketPath intentionally relies solely on
filesystem permissions.
| const discovered = await discoverActiveSession(options.overridePath, { | ||
| overrideSource: options.overrideSource | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
Preserve the source when SessionManager overrides the path.
This forwarding is bypassed by SessionManager.start(overridePath), which recreates file-tail/auto transports with only kind and overridePath. A Cursor trace then falls back to the default source and is normalized by the wrong driver. Thread the override source through that replacement flow and add a SessionManager-level regression test.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/capture/transcript-watcher.ts` around lines 298 - 300, Update
SessionManager.start and its replacement transport flow to preserve
options.overrideSource when recreating file-tail/auto transports for an
overridePath, so Cursor traces retain their original source and driver
normalization. Propagate the source from discoverActiveSession through the
SessionManager override path, and add a SessionManager-level regression test
covering this behavior.
| describe('forward-to-shadow.sh', () => { | ||
| it('is executable and fail-opens when the receiver is down', async () => { | ||
| const script = path.resolve( | ||
| path.dirname(fileURLToPath(import.meta.url)), | ||
| '../../../scripts/hooks/forward-to-shadow.sh' | ||
| ); | ||
| chmodSync(script, 0o755); | ||
| const { spawnSync } = await import('node:child_process'); | ||
| const result = spawnSync( | ||
| script, | ||
| [], | ||
| { | ||
| input: JSON.stringify({ | ||
| hook_event_name: 'stop', | ||
| conversation_id: 'offline-test', | ||
| }), | ||
| encoding: 'utf8', | ||
| env: { | ||
| ...process.env, | ||
| SHADOW_HOOK_URL: 'http://127.0.0.1:1/hook', | ||
| }, | ||
| } | ||
| ); | ||
| expect(result.status).toBe(0); | ||
| expect(result.stdout.trim()).toBe('{}'); | ||
| }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Test mutates a tracked file's permissions as a side effect.
chmodSync(script, 0o755) changes the mode of the committed forward-to-shadow.sh on disk, which leaves a dirty working tree locally and makes the test depend on/alter shared filesystem state. Prefer committing the script as executable and asserting it, or restoring the original mode in a finally.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/capture/drivers/cursor.test.ts` around lines 486 - 511, Remove the
chmodSync call from the forward-to-shadow.sh test and ensure the script’s
executable mode is committed in the repository. Preserve the existing executable
and fail-open assertions without mutating tracked files or shared filesystem
state.
| These use the raw observed-agent wire format (Claude Code JSONL or Cursor hook JSON), | ||
| one JSON object per line. | ||
|
|
||
| | File | Harness | Description | Key events | | ||
| |------|---------|-------------|------------| | ||
| | `happy-path.jsonl` | Claude Code | Small clean session (~6 transcript lines) | Read + Write + success; phase = implementation | | ||
| | `tool-heavy.jsonl` | Claude Code | Dense tool session | Many Bash + Read calls; triggers bash-churn risk | | ||
| | `risk-escalation.jsonl` | Claude Code | Session with failures | Multiple `tool_result` errors; triggers failed-tool risk | | ||
| | `subagent-flow.jsonl` | Claude Code | Sub-agent delegation pattern | user→assistant→tool delegation messages | | ||
| | `cursor-hooks.jsonl` | Cursor | Hook-receiver payloads (`hook_event_name`) | sessionStart, tools, afterFileEdit, subagent, stop | |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Align regeneration guidance with the Cursor fixture.
The new Cursor row conflicts with the earlier Claude Code-only transcript-format guidance. Update that guidance to say fixtures follow their harness’s raw wire format, so contributors do not author Cursor fixtures as Claude transcripts.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/fixtures/README.md` around lines 20 - 29, Update the earlier
transcript-format guidance in tests/fixtures/README.md to state that each
fixture uses its harness’s raw observed-agent wire format, including Cursor hook
JSON where applicable. Ensure the guidance no longer implies all fixtures must
use Claude Code JSONL, while preserving the one-JSON-object-per-line requirement
where applicable.
| const parser = createIncrementalParser((entry) => { | ||
| const events = cursorDriver.normalizeEntry(entry, 'conv-cursor-1', 'cursor-hook'); | ||
| if (events.length > 0) { | ||
| void buffer.push(events); | ||
| } | ||
| }); | ||
|
|
||
| parser.push(readFileSync(FIXTURE, 'utf8')); | ||
| // Allow async pushes to settle. | ||
| await new Promise((resolve) => setTimeout(resolve, 50)); | ||
|
|
||
| const events = await buffer.getAll(); | ||
| expect(events.length).toBeGreaterThan(5); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Replace the fixed 50 ms sleep with a deterministic wait to avoid CI flakiness.
buffer.push(events) is fire-and-forget async I/O (persistence is enabled via persistenceRoot), so the hard-coded setTimeout(resolve, 50) can race under load and any push rejection is silently swallowed. Poll until the expected events land (as the sibling cursor.test.ts does with its waitFor helper) rather than sleeping a fixed interval.
♻️ Suggested approach
- parser.push(readFileSync(FIXTURE, 'utf8'));
- // Allow async pushes to settle.
- await new Promise((resolve) => setTimeout(resolve, 50));
-
- const events = await buffer.getAll();
+ parser.push(readFileSync(FIXTURE, 'utf8'));
+ // Poll until the expected events have been persisted.
+ let events = await buffer.getAll();
+ const deadline = Date.now() + 4_000;
+ while (events.length <= 5 && Date.now() < deadline) {
+ await new Promise((resolve) => setTimeout(resolve, 20));
+ events = await buffer.getAll();
+ }
expect(events.length).toBeGreaterThan(5);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const parser = createIncrementalParser((entry) => { | |
| const events = cursorDriver.normalizeEntry(entry, 'conv-cursor-1', 'cursor-hook'); | |
| if (events.length > 0) { | |
| void buffer.push(events); | |
| } | |
| }); | |
| parser.push(readFileSync(FIXTURE, 'utf8')); | |
| // Allow async pushes to settle. | |
| await new Promise((resolve) => setTimeout(resolve, 50)); | |
| const events = await buffer.getAll(); | |
| expect(events.length).toBeGreaterThan(5); | |
| parser.push(readFileSync(FIXTURE, 'utf8')); | |
| // Poll until the expected events have been persisted. | |
| let events = await buffer.getAll(); | |
| const deadline = Date.now() + 4_000; | |
| while (events.length <= 5 && Date.now() < deadline) { | |
| await new Promise((resolve) => setTimeout(resolve, 20)); | |
| events = await buffer.getAll(); | |
| } | |
| expect(events.length).toBeGreaterThan(5); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/integration/e2e-cursor-hooks-to-state.test.ts` around lines 49 - 61,
Replace the fixed 50 ms delay in the incremental parser test with the existing
deterministic waitFor-style polling used by sibling cursor tests, waiting until
buffer.getAll() contains the expected events. Await each buffer.push operation
or otherwise propagate push failures so asynchronous persistence errors are not
silently discarded, while preserving the final event-count assertion.
User description
Summary
Makes AgentVisualCrazy work as a second screen for Cursor agents, not just Claude Code — plus CI gates that prove the hook path.
Capture
cursorharness driver + hook-receiver (127.0.0.1:9477) + defaultautotransport.cursor/hooks.json+ forwardersreload-exec-daemon-hooks.sh) — needed when hooks are added after exec-daemon startCI gates (this push)
npm run test:cursor-hooksforward-to-shadow.sh→ receiver → derivecursor-cli-hook-selftestCURSOR_API_KEYsecret is setHow Cursor-in-CI works (from official docs):
curl https://cursor.com/install -fsS \| bash(GitHub Actions)CURSOR_API_KEY+agent -p --force "…"(headless CLI).cursor/hooks.jsononly (user hooks unavailable in CI/cloud VMs) (hooks; deployment patterns state hooks work for CLI)Enable the full CLI gate:
Live dogfood
Self-hooked this Background Agent after
ReloadAgentSkills; capture showedsessionId=bc-019f84ed-…,harnessId=cursor.Context7: no Context7 MCP in this environment — used live Cursor docs (hooks, CLI GitHub Actions, headless, deployment patterns).
Summary
Adds Cursor as a second supported agent harness alongside Claude Code.
Highlights
127.0.0.1:9477with optional Unix socket and token authentication.Validation
npm run test:cursor-hooksCI gate.CURSOR_API_KEYis configured.CodeAnt-AI Description
Add Cursor hook capture alongside the existing Claude transcript feed
What Changed
Impact
✅ Live Cursor agent monitoring✅ Fewer missed sessions after hook setup✅ Clearer multi-agent transcripts💡 Usage Guide
Checking Your Pull Request
Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.
Talking to CodeAnt AI
Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:
This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.
Example
Preserve Org Learnings with CodeAnt
You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:
This helps CodeAnt AI learn and adapt to your team's coding style and standards.
Example
Retrigger review
Ask CodeAnt AI to review the PR again, by typing:
Check Your Repository Health
To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.