feat(cursor): preToolUse enforcement — block non-compliant writes - #60
Conversation
The Cursor integration registered a `postToolUse` hook, which (per Cursor's
spec) can only inject `additional_context` AFTER the write — it cannot force a
revision. So "block non-compliant edits" was really "surface and hope."
`init --agent cursor` now registers a **`preToolUse`** hook by default
(`--cursor-mode enforce`): `prodcycle hook` detects the event and returns
`{"permission":"deny","agent_message":<remediation>}`, so Cursor rejects the
non-compliant write before it lands and the agent revises and retries — the true
analog of Claude Code's `{"decision":"block","reason":...}`.
- `--cursor-mode warn` keeps the non-blocking postToolUse/additional_context path.
- Legacy postToolUse/afterFileEdit prodcycle hooks migrate automatically.
- Fail-open: missing key / 401-403 / scanner-unavailable (exitCode 2) never emit
deny — they allow the edit, so we never block on our own error.
- node + python kept in lockstep; corrected the overstated "self-healing" wording
on the postToolUse formatter. +tests both languages (suites green 124 / 112).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Greptile SummaryThis PR upgrades the Cursor integration from a non-blocking
Confidence Score: 5/5Safe to merge — the fail-open invariants are enforced at every exit path, and the two implementation nits are both in non-critical code paths. The core blocking logic and all fail-open paths (scanner unavailable, auth error, clean scan, non-Write tool) are handled correctly and tested in both Node and Python. The only gaps are a missing input-validation error for an unrecognized --cursor-mode value in the Node CLI, and the exported formatCursorPreToolUseOutput function not internally guarding against exitCode === 2 — neither of which can cause incorrect blocking behavior in any current code path. node/src/cli.ts — --cursor-mode option validation and the formatCursorPreToolUseOutput defensive guard. Important Files Changed
Prompt To Fix All With AIFix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
node/src/cli.ts:229-231
**`--cursor-mode` silently ignores invalid values**
The Node option accepts any string and coerces it to `'enforce'` if it isn't `'warn'`. A user who typos `--cursor-mode enforced` or passes an unsupported value gets no error — they silently end up in enforce mode, which happens to be the right default, but the discrepancy from the Python side (which uses `choices=['enforce', 'warn']` and rejects unknown values with an argparse error) is confusing. Consider adding explicit validation, e.g. after parsing: `if (opts.cursorMode && !['enforce','warn'].includes(opts.cursorMode)) program.error(...)`.
### Issue 2 of 2
node/src/cli.ts:687-710
**`formatCursorPreToolUseOutput` emits `deny` when called with `exitCode === 2`**
The function only guards `exitCode === 0` → allow, and falls through to emit `{"permission":"deny",...}` for any other exit code, including `exitCode === 2` (scanner unavailable). All current callers correctly intercept `exitCode === 2` before calling this function, but since it is an exported public symbol, a future caller (or a test) that passes a scanner-unavailable response directly would get a deny instead of an allow — violating the fail-open contract. A defensive guard for `exitCode === 2` inside the function would make the invariant self-enforcing. The Python mirror has the same gap.
Reviews (2): Last reviewed commit: "style(test): add blank line between test..." | Re-trigger Greptile |
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Why
The Cursor integration registered a
postToolUsehook. Per Cursor's hook spec, post-write hooks (postToolUse/afterFileEdit) can only injectadditional_context— they cannot force a revision. So the agent received the remediation but, having finished its turn, often didn't act on it. "Block non-compliant edits as you code" was really "surface and hope." (Observed live: the hook flagged a hardcoded key correctly, but the file stayed.)What
prodcycle init --agent cursornow registers apreToolUsehook by default (--cursor-mode enforce).prodcycle hookdetects the event and on violations returns:{ "permission": "deny", "agent_message": "<remediation>", "user_message": "ProdCycle blocked this edit: N compliance violation(s)…" }Cursor rejects the write before it lands and feeds
agent_messageto the agent, which revises and retries — the true analog of Claude Code's{"decision":"block","reason":…}.--cursor-mode warnkeeps the old non-blockingpostToolUse/additional_contextbehavior.postToolUse/afterFileEditprodcycle hooks are migrated to the chosen event automatically.deny— they allow the edit, so we never block on our own error.postToolUseformatter.Verified
node + python in lockstep; full suites green (124 node / 112 python), incl. new formatter/detector tests + updated init/collector tests.
🤖 Generated with Claude Code