Skip to content

feat(capture): Cursor harness via hook receiver#111

Open
Coldaine wants to merge 8 commits into
mainfrom
cursor/cursor-harness-driver-5f4c
Open

feat(capture): Cursor harness via hook receiver#111
Coldaine wants to merge 8 commits into
mainfrom
cursor/cursor-harness-driver-5f4c

Conversation

@Coldaine

@Coldaine Coldaine commented Jul 21, 2026

Copy link
Copy Markdown
Owner

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

  • cursor harness driver + hook-receiver (127.0.0.1:9477) + default auto transport
  • Committed .cursor/hooks.json + forwarders
  • Mid-session reload helper (reload-exec-daemon-hooks.sh) — needed when hooks are added after exec-daemon start

CI gates (this push)

Gate When Proves
npm run test:cursor-hooks Always required Real forward-to-shadow.sh → receiver → derive
Job cursor-cli-hook-selftest When CURSOR_API_KEY secret is set Headless Cursor CLI loads project hooks and POSTs into the receiver

How Cursor-in-CI works (from official docs):

  1. curl https://cursor.com/install -fsS \| bash (GitHub Actions)
  2. CURSOR_API_KEY + agent -p --force "…" (headless CLI)
  3. Project .cursor/hooks.json only (user hooks unavailable in CI/cloud VMs) (hooks; deployment patterns state hooks work for CLI)

Enable the full CLI gate:

gh secret set CURSOR_API_KEY --repo Coldaine/AgentVisualCrazy --body "$CURSOR_API_KEY"

Live dogfood

Self-hooked this Background Agent after ReloadAgentSkills; capture showed sessionId=bc-019f84ed-…, harnessId=cursor.

Context7: no Context7 MCP in this environment — used live Cursor docs (hooks, CLI GitHub Actions, headless, deployment patterns).

Open in Web Open in Cursor 

Summary

Adds Cursor as a second supported agent harness alongside Claude Code.

Highlights

  • Adds Cursor harness driver, normalization, discovery, capabilities, and event-source support.
  • Introduces loopback hook receiver on 127.0.0.1:9477 with optional Unix socket and token authentication.
  • Adds automatic capture transport combining Claude transcript tailing with Cursor hooks.
  • Provides Bash and PowerShell fail-open hook forwarders plus Cursor hook configuration.
  • Preserves subagent identity and improves source attribution across transports.
  • Adds exec-daemon hook reloading for active-session dogfooding.
  • Updates documentation, examples, fixtures, and architecture guidance.

Validation

  • Adds Cursor driver, transport, discovery, authorization, Unix socket, and state-derivation coverage.
  • Adds required npm run test:cursor-hooks CI gate.
  • Adds optional headless Cursor CLI self-test when CURSOR_API_KEY is configured.
  • Adds live forwarder-to-receiver-to-derived-state end-to-end testing.

CodeAnt-AI Description

Add Cursor hook capture alongside the existing Claude transcript feed

What Changed

  • The app now watches Cursor agent hooks on a local receiver, so Cursor turns appear in the live graph without manual transcript files.
  • A new default “auto” mode runs Cursor hooks and Claude file-tailing together, and falls back to file-tailing if the hook receiver is unavailable.
  • Cursor sessions now keep their own identity, tool names, subagents, and source labels, so the transcript, file attention, and session state stay correct.
  • Added Cursor hook forwarders, a mid-session hook reload helper, example hook setup, and CI checks that prove the Cursor path works end to end.

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:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

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:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

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.

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>
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@cursor[bot], you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 9007fdc4-76f4-4ab7-b00a-858105ec131c

📥 Commits

Reviewing files that changed from the base of the PR and between 3c031a5 and 3da721d.

📒 Files selected for processing (4)
  • src/capture/drivers/cursor/normalizer.ts
  • src/capture/session-manager.ts
  • tests/capture/drivers/cursor.test.ts
  • tests/capture/session-manager.test.ts
📝 Walkthrough

Walkthrough

Cursor 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.

Changes

Cursor capture pipeline

Layer / File(s) Summary
Cursor driver and event normalization
src/capture/drivers/cursor/*, src/capture/drivers/index.ts, src/shared/schema.ts, src/shared/derive.ts, tests/capture/drivers/*, tests/integration/e2e-cursor-hooks-to-state.test.ts, tests/fixtures/transcripts/cursor-hooks.jsonl
Cursor hooks and agent traces are normalized into canonical events, registered by source, discovered from trace files, and verified through derived-state tests.
Hook receiver and automatic capture
src/capture/capture-transport.ts, src/capture/capture-transports.ts, src/capture/hook-receiver-transport.ts, src/capture/auto-capture-transport.ts, src/capture/*transport.ts, src/capture/session-manager.ts, src/capture/transcript-watcher.ts, tests/capture/*transport*.test.ts, tests/capture/file-tail-source.test.ts
Adds hook-receiver and auto transports, source propagation, token authorization, HTTP/Unix ingestion, and file-tail fallback behavior.
Hook forwarding and validation tooling
.cursor/hooks.json, scripts/hooks/*, .github/workflows/ci.yml, package.json, tests/live/cursor-hooks-live.test.ts
Adds hook configurations, Bash and PowerShell forwarders, live capture tooling, daemon reload support, package commands, and required/optional CI self-tests.
Documentation and project records
README.md, docs/*, scripts/hooks/README.md, tests/fixtures/README.md
Documents Cursor capture, transport configuration, hook installation, CI/live verification, root-level setup, and the updated harness architecture.

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
Loading

Possibly related PRs

Suggested labels: size:XXL

Poem

I’m a bunny with hooks in a loopback burrow,
Sending each event in a neat little furrow.
Claude and Cursor now hop side by side,
Canonical events in a stream they can ride.
Tests blink green as the dashboards grow—
Thump, thump, forward, and off we go!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.65% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: adding Cursor capture support through a hook receiver.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/cursor-harness-driver-5f4c

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@charliecreates charliecreates Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found two blocking correctness issues in the Cursor capture path:

  1. 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 lack tool_use_id and 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.

  2. subagentStart/subagentStop events use actor: 'system' while deriveState() keys agent nodes by event.actor; their agentId is only stored in payload. All Cursor subagents therefore collapse into one system node instead of being represented individually. Use the subagent identity consistently (or update derivation to key these lifecycle events by payload.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.

Comment thread src/capture/drivers/cursor/normalizer.ts
Comment thread src/capture/drivers/cursor/normalizer.ts Outdated
cursoragent and others added 5 commits July 21, 2026 13:58
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>
@cursor
cursor Bot marked this pull request as ready for review July 22, 2026 03:30
@coderabbitai coderabbitai Bot added the size:XXL This PR changes 1000+ lines, ignoring generated files label Jul 22, 2026
Comment thread src/capture/session-manager.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (3 files)
  • src/capture/drivers/cursor/normalizer.ts
  • src/capture/session-manager.ts
  • tests/capture/drivers/cursor.test.ts
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)
  • src/capture/session-manager.ts
  • tests/capture/session-manager.test.ts

Previous review (commit 3c031a5)

Status: 1 Warning Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
src/capture/session-manager.ts 153 overrideSource dropped when rewiring auto transport for an overridePath, causing discovered Cursor agent-trace files to lose harness attribution
Files Reviewed (4 key source files)
  • src/capture/session-manager.ts — 1 warning
  • src/capture/drivers/cursor/normalizer.ts — no new issues (existing comments addressed by commits in this PR)
  • src/shared/derive.ts — no issues
  • src/capture/hook-receiver-transport.ts — no issues

Also reviewed: docs (docs/domain-events.md, docs/north-star.md, docs/plans/roadmap.md, docs/plans/plan-multi-harness-mvp.md), tests (tests/live/cursor-hooks-live.test.ts, tests/capture/drivers/cursor.test.ts), CI (.github/workflows/ci.yml), scripts (scripts/hooks/forward-to-shadow.sh, scripts/hooks/reload-exec-daemon-hooks.sh). No new issues found in those.

Note on docs/plans/plan-multi-harness-mvp.md: ~33 references still point to removed shadow-agent/ paths (e.g., shadow-agent/src/capture/drivers/). The file is already marked as historical context from the pre-reset era, but any contributor reading it to understand architecture decisions will reach non-existent paths. Including for awareness.

Fix these issues in Kilo Cloud


Reviewed by step-3.7-flash · Input: 41.9K · Output: 3K · Cached: 181K

@cursor

cursor Bot commented Jul 22, 2026

Copy link
Copy Markdown

@charliecreates please re-review — both blocking items are fixed in 3c031a5 (and threads resolved):

  1. Stock hooks config is generic-only; specialized hooks without tool_use_id are skipped.
  2. Subagent lifecycle uses Cursor subagent_id as actor / payload.agentId; deriveState prefers that id.

CI (test + Cursor CLI hook self-test) is green. Ready to merge once the CHANGES_REQUESTED review is cleared.

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>
Copilot AI review requested due to automatic review settings July 22, 2026 03:38
@codeant-ai

codeant-ai Bot commented Jul 22, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR 03078a4 Jul 22, 2026 · 03:38 03:44

Updated in place by CodeAnt AI · last 5 reviews

@codeant-ai

codeant-ai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Thanks for using CodeAnt! 🎉

We're free for open-source projects. if you're enjoying it, help us grow by sharing.

Share on X ·
Reddit ·
LinkedIn

@codeant-ai codeant-ai Bot added size:XXL This PR changes 1000+ lines, ignoring generated files and removed size:XXL This PR changes 1000+ lines, ignoring generated files labels Jul 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 + optional cursor-agent-trace) via a new driver and hook-receiver transport (HTTP + optional Unix socket).
  • Make auto the default capture transport (Claude file-tail + hook receiver concurrently) and propagate source correctly 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.

Comment on lines +46 to +54
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'
}
});
Comment on lines +95 to +101
return {
sessionId,
label: options.sessionLabel ?? `Live Cursor: ${sessionId.slice(0, 12)}`,
source,
path: pathHint ?? `hook-receiver://${source}/${sessionId}`,
transportId: 'hook-receiver'
};
Comment on lines +107 to +110
const host = options.host ?? DEFAULT_HOOK_RECEIVER_HOST;
const port = options.port ?? DEFAULT_HOOK_RECEIVER_PORT;
const fallbackSessionId = options.sessionId ?? 'cursor-live';

Comment on lines +27 to +36
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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in VSCode Claude

(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
👍 | 👎

Comment on lines +375 to +383
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()))
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in VSCode Claude

(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
👍 | 👎

Comment thread src/capture/session-manager.ts Outdated
Comment on lines +157 to +162
overridePath && (transport.kind === 'file-tail' || transport.kind === 'auto')
? createCaptureTransport({
...configuredTransportOptions,
kind: transport.kind,
overridePath
} as CaptureTransportOptions)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in VSCode Claude

(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
👍 | 👎

Comment on lines +332 to +338
if (source === 'cursor-agent-trace' && !name) {
return normalizeAgentTraceEntry(entry, sessionId, source, timestamp);
}

if (name) {
return normalizeHookEntry(entry, sessionId, source, timestamp, name);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in VSCode Claude

(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
👍 | 👎

Comment on lines +81 to +87
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');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in VSCode Claude

(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
👍 | 👎

Comment on lines +115 to +145
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in VSCode Claude

(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 fix
👍 | 👎

@cursor
cursor Bot enabled auto-merge (squash) July 22, 2026 03:42
Only 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>
Comment on lines +54 to +56
const port = Number.parseInt(raw, 10);
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
throw new Error(`Invalid SHADOW_HOOK_RECEIVER_PORT: ${raw}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in VSCode Claude

(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
👍 | 👎

Comment on lines +73 to +82
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in VSCode Claude

(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 fix
👍 | 👎

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Preserve the existing auto transport settings when applying overridePath. 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

📥 Commits

Reviewing files that changed from the base of the PR and between 8db8a56 and 3c031a5.

📒 Files selected for processing (47)
  • .cursor/hooks.json
  • .github/workflows/ci.yml
  • README.md
  • docs/architecture.md
  • docs/domain-events.md
  • docs/getting-started.md
  • docs/history/log.md
  • docs/north-star.md
  • docs/plans/plan-multi-harness-mvp.md
  • docs/plans/roadmap.md
  • docs/research/harness-ingestion-matrix.md
  • package.json
  • scripts/hooks/README.md
  • scripts/hooks/ci-cursor-cli-self-test.sh
  • scripts/hooks/cursor-hooks.example.json
  • scripts/hooks/forward-to-shadow-debug.sh
  • scripts/hooks/forward-to-shadow.ps1
  • scripts/hooks/forward-to-shadow.sh
  • scripts/hooks/live-capture-server.mjs
  • scripts/hooks/reload-exec-daemon-hooks.sh
  • src/capture/auto-capture-transport.ts
  • src/capture/capture-transport.ts
  • src/capture/capture-transports.ts
  • src/capture/drivers/cursor/capabilities.ts
  • src/capture/drivers/cursor/discovery.ts
  • src/capture/drivers/cursor/index.ts
  • src/capture/drivers/cursor/normalizer.ts
  • src/capture/drivers/index.ts
  • src/capture/hook-receiver-transport.ts
  • src/capture/http-stream-transport.ts
  • src/capture/session-manager.ts
  • src/capture/socket-transport.ts
  • src/capture/transcript-watcher.ts
  • src/capture/websocket-transport.ts
  • src/shared/derive.ts
  • src/shared/schema.ts
  • tests/capture/auto-capture-transport.test.ts
  • tests/capture/capture-transport-env.test.ts
  • tests/capture/drivers/cursor.test.ts
  • tests/capture/drivers/harness-driver.test.ts
  • tests/capture/file-tail-source.test.ts
  • tests/capture/hook-receiver-unix.test.ts
  • tests/fixtures/README.md
  • tests/fixtures/transcripts/cursor-hooks.jsonl
  • tests/integration/e2e-cursor-hooks-to-state.test.ts
  • tests/live/cursor-hooks-live.test.ts
  • tests/schema.test.ts

Comment thread .cursor/hooks.json
Comment on lines +4 to +14
"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" }]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

git ls-files .cursor scripts/hooks cursor-hooks.example.json

Repository: 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.md

Repository: 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:


🌐 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.sh

Repository: 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.

Comment thread .github/workflows/ci.yml
Comment on lines +41 to +53
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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.

Suggested change
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

Comment thread .github/workflows/ci.yml
Comment on lines +46 to +48
- uses: actions/checkout@v4

- uses: actions/setup-node@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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

Comment thread docs/domain-events.md
Comment on lines +42 to +60
### 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment thread docs/getting-started.md
Comment on lines +84 to +94
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) |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment on lines +206 to +223
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();
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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/**' || true

Repository: 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.

Comment on lines +298 to +300
const discovered = await discoverActiveSession(options.overridePath, {
overrideSource: options.overrideSource
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment on lines +486 to +511
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('{}');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Comment thread tests/fixtures/README.md
Comment on lines +20 to +29
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 |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Comment on lines +49 to +61
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL This PR changes 1000+ lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants