Skip to content

Fix activity-ingest empty stdout blocking Cursor gate hooks - #166

Merged
Minitour merged 10 commits into
developfrom
fix/activity-ingest-gate-allow
Aug 3, 2026
Merged

Fix activity-ingest empty stdout blocking Cursor gate hooks#166
Minitour merged 10 commits into
developfrom
fix/activity-ingest-gate-allow

Conversation

@Minitour

@Minitour Minitour commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

  • capa activity-ingest is an observer but was installed on Cursor gate events (beforeFileRead, subagentStart, userPromptSubmit) without writing stdout.
  • Cursor treats empty stdout as invalid JSON on gate hooks and blocks the action for safety (e.g. file reads).
  • Always emit {"permission":"allow"} / {"continue":true} in a finally block so ingest stays fail-open and gates never fail closed.

Test plan

  • bun test src/cli/commands/__tests__/activity-ingest.test.ts
  • Manual: pipe JSON into capa activity-ingest ... --event beforeFileRead and confirm stdout is {"permission":"allow"}
  • Rebuild/link local capa and confirm Cursor Read is no longer blocked by the activity hook

beforeFileRead and other gate events require valid allow/continue JSON; emit it in a finally so ingest stays fail-open without failing closed.

Co-authored-by: Cursor <cursoragent@cursor.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Prevent Cursor gate hooks from blocking by always emitting allow/continue JSON

🐞 Bug fix 🧪 Tests 🕐 10-20 Minutes

Grey Divider

AI Description

• Ensure capa activity-ingest always returns valid JSON for Cursor gate hook events.
• Add event-to-stdout mapping (allow vs continue) and emit it in a finally block.
• Add Bun tests to prevent regressions when ingest exits early or fails.
Diagram

graph TD
  A{{"Cursor gate hook"}} --> B["capa activity-ingest"] --> C{"Gate event?"}
  C --> D["stdout allow/continue JSON"] --> A
  B --> E["Local capa server"]
  F["stdin hook payload"] --> B

  subgraph Legend
    direction LR
    _ext{{"External"}} ~~~ _proc["Process"] ~~~ _dec{"Decision"}
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Always emit allow JSON for all events
  • ➕ Simplest logic; eliminates any future event-classification misses
  • ➖ May produce unexpected stdout for observational hooks; could confuse providers/tools that don’t expect output on non-gate events
2. Split into two commands: gate-decider + ingest-observer
  • ➕ Clear separation of responsibilities; gate command can be extremely minimal and robust
  • ➖ More moving parts and install/config complexity; higher chance of misconfiguration across providers

Recommendation: Keep the PR’s approach: compute gate stdout once from parsed args and emit it in a finally block. It directly addresses Cursor’s safety behavior (empty stdout treated as invalid JSON) while keeping observational events silent and preserving the existing fail-open ingest semantics.

Files changed (2) +135 / -6

Bug fix (1) +42 / -6
activity-ingest.tsEmit allow/continue JSON for Cursor gate events in a finally block +42/-6

Emit allow/continue JSON for Cursor gate events in a finally block

• Adds 'PERMISSION_GATE_EVENTS' and 'activityIngestGateStdout()' to map canonical hook events to the required Cursor stdout JSON ('{"permission":"allow"}' or '{"continue":true}'). Refactors 'activityIngestCommand' to parse args once and always write the gate decision in 'finally', ensuring gate hooks never fail closed due to empty stdout.

src/cli/commands/activity-ingest.ts

Tests (1) +93 / -0
activity-ingest.test.tsAdd tests ensuring gate hooks always emit valid allow/continue JSON +93/-0

Add tests ensuring gate hooks always emit valid allow/continue JSON

• Introduces unit tests for 'activityIngestGateStdout' covering permission-gated vs observational events. Adds integration-style tests that spy on 'process.stdout.write' to ensure 'activityIngestCommand' emits valid JSON even when ingest exits early (e.g., missing project).

src/cli/commands/tests/activity-ingest.test.ts

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Uncaught stdout write errors ✓ Resolved 🐞 Bug ☼ Reliability
Description
activityIngestCommand() adds a finally block that writes gate JSON to process.stdout without
guarding against stdout failures; a synchronous write exception or an unhandled stream error event
(e.g., broken pipe) can escape the fail-open wrapper and cause the command to fail. This contradicts
the function’s “always exit 0 / fail-open” intent and can make gate-hook execution less reliable in
constrained environments.
Code

src/cli/commands/activity-ingest.ts[R49-51]

+	} finally {
+		const gate = activityIngestGateStdout(parsed.event);
+		if (gate) process.stdout.write(`${gate}\n`);
Evidence
The function explicitly claims fail-open behavior, but the newly added finally writes to stdout
outside any error handling for stdout failures, so stdout errors can still terminate the command
unexpectedly.

src/cli/commands/activity-ingest.ts[41-52]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`activityIngestCommand()` now writes gate JSON to `process.stdout` in a `finally` block, but that write is not protected against stdout failures. If stdout is closed/broken, the write can fail outside the existing fail-open `catch`, causing an unexpected rejection/crash.
## Issue Context
This command is used as a fail-open hook handler; it should avoid throwing even when output streams misbehave.
## Fix Focus Areas
- src/cli/commands/activity-ingest.ts[41-52]
## Suggested fix
- Wrap the stdout write in a small helper that:
- catches synchronous exceptions from `process.stdout.write(...)`, and
- attaches a one-time `process.stdout.once('error', () => {})` handler (or equivalent) before writing, so stream `error` events don’t become unhandled.
- Keep behavior identical when stdout is healthy (still emit the same single-line JSON).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/cli/commands/activity-ingest.ts Outdated
Minitour and others added 9 commits August 3, 2026 21:23
Cursor already records reads via afterTool/postToolUse; the beforeReadFile hook only duplicated them as kind file and risked gate failures.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add hooks.activityCorrelation field maps per provider so ingest extracts chat/turn ids without hardcoding, persist them on tool_calls, and nest the Activity feed conversation → generation → spans.

Co-authored-by: Cursor <cursoragent@cursor.com>
Add hooks.activityAttributes field maps so model, model_id, versions, and related metadata are stored on tool_calls without provider-specific hardcoding.

Co-authored-by: Cursor <cursoragent@cursor.com>
Read PostToolUse results from provider-configured fields (Claude tool_response, Cursor tool_output) instead of assuming Cursor-only names.

Co-authored-by: Cursor <cursoragent@cursor.com>
Keep generation rows focused on the turn; show the provider once per conversation.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Inherit conversation/generation on MCP traces and keep afterShell rows for capa sh so Bash/Shell calls are visible in the activity feed.

Co-authored-by: Cursor <cursoragent@cursor.com>
Apply Biome format/import fixes across activity files and keep activity-ingest fail-open when stdout is broken.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@Minitour
Minitour merged commit 325350a into develop Aug 3, 2026
10 of 11 checks passed
@Minitour
Minitour deleted the fix/activity-ingest-gate-allow branch August 3, 2026 19:43
@Minitour Minitour mentioned this pull request Aug 4, 2026
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant