[Guide] Thin Self-Review Hooks for Cursor | The Model Is the Auditor (Windows + Linux)

Hey everyone,

Following up on the workflow guides I’ve shared before, this one goes the other way: less structure for the AI to manage, more hooks that make the agent review its own work. Four hook events, one message bus. Think of it as a seatbelt, not a co-pilot.

Repo: GitHub - kleosr/cursordoctrine · GitHub

The whole system in one line: the model is the auditor Cursor only carries context and gates blast radius.

From audit pipelines to one message bus

The temptation with hooks is to bolt a static-analysis pipeline onto every keystroke: linters, scorers, AST passes, stuck-file trackers. I tried that. Heavy, noisy, and the model ignored half of it.

What works is smaller. The model already has the file, the diff, and the user’s intent. A self-review it runs itself costs almost nothing. So the hooks do three jobs:

  1. Inject a doctrine at session start. A short governing text (doctrine.md + USER-RULES.md) lands in every chat as additional_context. Smallest correct diff, verify then stop, ask don’t guess. Set it up once.
  2. Hand the model its own edits back. After each agent edit, a self-review prompt is stashed in a per-conversation pending file; the next tool boundary delivers it. The model reads its own diff, fixes real bugs (security, correctness, logic), and says nothing otherwise. Two optional advisories ride the same bus: an over-edit check based on diff size, and an anti-slop check for new deps, premature abstractions, and restate-the-code comments.
  3. Gate blast radius. One beforeShellExecution gate denies a short, explicit list: rm -rf / on absolute paths, curl | sh, force-push, git reset --hard, npm publish. Everything else is allowed. Deny-by-list, fail open.

When an implementation finishes, a stop hook fires exactly one final review pass (correctness, reliability, coverage, anti-slop) over everything that changed. Then it arms a per-conversation brake so it can never loop.

How the loop works

  1. You give the agent a task.
  2. The agent edits a file → afterFileEdit records the edit and stashes the self-review prompt.
  3. The next tool call → postToolUse drains the pending file into additional_context. The model audits its own diff and fixes what’s broken.
  4. The agent finishes → stop submits one followup_message: a final review over every file touched this session.
  5. Done. The next stop clears the brake and the cycle is ready for your next task.

Every bound is enforced twice: in the script and in hooks.json (loop_limit, timeouts). All hooks always exit 0; nothing here can block or crash your session.

Windows and Linux

The repo ships two functionally identical packages:

  • windows/ — PowerShell 7 hooks (pwsh.exe), the original implementation.
  • linux/ — full bash ports for Linux machines and SSH remotes. JSON parsing prefers jq, falls back to python3, and degrades gracefully (fails open) if neither exists.

If you drive a remote box over SSH from Cursor, the Linux package installs on the remote’s $HOME. That’s where the hooks need to live, not on your laptop.

Getting started

  1. Clone GitHub - kleosr/cursordoctrine · GitHub and open INSTALL.md.
  2. Paste its contents into a Cursor agent chat on the target machine (the box where Cursor will run the hooks).
  3. The install prompt asks whether you’re on Windows or Linux before it copies anything. It won’t guess. A Windows laptop driving an SSH remote needs the Linux package on the remote. The agent copies the right folder into ~/.cursor and ~/.agents/hooks, runs every hook by hand with fake payloads, then has you restart Cursor.
  4. Verify inside Cursor: the doctrine answers from context, a small edit triggers the self-review, git push --force gets blocked, and one final review fires when you stop.

Prerequisites: git everywhere; pwsh on Windows; bash + jq or python3 on Linux.

Kill switches if anything misbehaves: HOOKS_ENFORCE=0 (all advisories), PERM_GATE_ENFORCE=0, MINIMAL_EDITING_ENFORCE=0, ANTI_SLOP_ENFORCE=0, FINAL_REVIEW_ENFORCE=0.

Architecture

Four flows, one message bus. Self-review always runs; the two advisories only fire when their thresholds trip.

                        ┌─────────────────────────────┐
  session start ──────► │ inject-doctrine             │
                        │ doctrine.md + USER-RULES.md │──► additional_context
                        └─────────────────────────────┘    (every new chat)

  agent edits a file (afterFileEdit, matcher ^Write$)
        │
        ▼
  ┌───────────────────────┐   ┌─────────────────────┐   ┌────────────────────┐
  │ self-review-trigger   │   │ minimal-edit-audit  │   │ anti-slop-audit    │
  │ always: stash prompt  │   │ only on WARN/FAIL   │   │ only on signal or  │
  │ + record edit marker  │   │ (diff > thresholds) │   │ >= 40 added lines  │
  └──────────┬────────────┘   └─────────┬───────────┘   └─────────┬──────────┘
             │                          │                         │
             ▼                          ▼                         ▼
       ~/.cursor/.hooks-pending/feedback-<conversation_id>.txt  (append)
                                    │
  next tool call (postToolUse)      ▼
        └────────────► drain file ──► additional_context ──► model self-reviews
                       (one-shot)                            its own diff, fixes
                                                             real bugs, stays quiet

  any shell command (beforeShellExecution)
        └────────────► permission-gate ──► allow (default)
                                       └─► deny (rm -rf /, curl|sh,
                                            force-push, npm publish, ...)

  agent stops (stop, status == completed)
        └────────────► final-review ──► edits this loop? ──► followup_message:
                       (one-shot brake,                      ONE review pass over
                        loop_limit cap)                      everything changed

Design notes

  • State lives under ~/.cursor/.hooks-pending/, keyed by conversation id: no repo litter, and concurrent chats never drain each other’s prompts. Stale state sweeps itself after 7 days.
  • afterFileEdit output isn’t consumed by Cursor, so edit hooks write to a pending file and postToolUse re-emits it next turn. That’s the entire message bus.
  • Cursor-only by design. Installs into Cursor’s own config locations and touches nothing in your projects.

Still evolving if you want to contribute a port, tighten a deny pattern, or extend the advisories, open a PR on GitHub. Questions welcome below.

2 Likes

nice launch! will be reviewing it out!!!

1 Like

Two fixes landed since the original post.

- Final review no longer breaks if scan_slop.py is missing falls back to the anti-slop checklist.

- Anti-slop is now a full Cursor skill (skills/anti-slop/). Install copies the whole folder; /anti-slop works in chat. Scanner has more checks.

Already installed? please, pull from repository and re-run INSTALL.md

2 Likes

cursordoctrine is on npm now.

Run npx cursordoctrine@latest install, then npx cursordoctrine verify. It merges into your existing hooks.json, has no dependencies, and works on Windows and Linux

1 Like

New update on: npx cursordoctrine@latest install

  • QoL Improvemetns
  • Unified anti-slop checklist
  • stop hook pulls your last request into FINAL REVIEW; every diff hunk must trace to it or get reverted. Catches clean code, wrong feature.

New update on: npx cursordoctrine@latest install 0.3.1

  • Injected YAGNI ultra ladder at session start.
  • Semantic-density gate new hook that flags semantic opacity

Updates in bulk npx cursordoctrine@latest command installs version: 0.6.9

  • 0.6.8 Intent enforcement gate on stop: blocks final review until intent ≠ verbatim trace.query
  • 0.6.9 Hook contamination fix

Don’t miss out. This last update will not only bring quality to your agent, it will make your life more easier with prompting and file changes.

1 Like

Hey everyone,

I built cursordoctrine because I got tired of watching agents ship clean diffs for the wrong problem. The code looked fine. Tests passed. But the work didn't trace back to what I actually asked for. So I stopped trying to fix the model with more prompts and started building a harness that makes the model audit itself, with two hard levers for the things it will always skip if you leave them soft.

This is the story of how that harness evolved: every branch, every version bump, every hook I added and every hook I deleted, and the logic behind all of it.

The core idea

The model is the auditor. The harness does only what the model cannot do for itself.

The model can read a diff, spot a bug, fix it, and explain why. That's free. What it won't reliably do on its own: remember your governing rules after 40 tool calls, keep a declared blast radius in working memory, refuse to edit before it has restated what you want, or block curl | sh when you're not looking.

So cursordoctrine injects short doctrine at session start, maintains a single repo file (.scope.json) as a living contract, nudges and gates when the contract is empty, and fires one structured final review when the session ends. Nine hooks. Seven Cursor events.

Harness hooks vs model roles

What .scope.json is

Before any code edit, the agent writes .scope.json at the repo root. The harness and the agent split ownership:

FieldOwnerPurpose
prompthookVerbatim latest user message
intentagentStep 0 restatement in the agent's own words
decomposition\[\]agentMulti-step plan per milestone
verifications\[\]hookPENDING, ACCEPT, or REVISE per step
files\[\]hook + agentDeclared blast radius plus session footprint
acceptanceagentDeterministic done-check

Contract lifecycle

The tri-role lifecycle (doctrine-ultra, v1.3.0)

Thinker, Worker, Verifier
  • Thinker — restate intent, declare blast radius, write decomposition.
  • Worker — edits. scope-refresh records files; scope-git-sweep catches shell-written files.
  • Verifier — mid-session milestone verify and eight-axis final review at stop.

Eight axes at stop

AxisNameWhat it catches
0Intent traceClean code, wrong feature
1CorrectnessLogic, edges, security
2ReliabilitySwallowed errors, leaks
3CoverageTests, linters
4Anti-slop40-item checklist
5WiringClick to handler to real effect
6MechanicsStack integrity
7Role-traceDecomposition verdicts

Version timeline

112 commits. 51 merged PRs (#2 through #52). Four phases.

Phase 0: Birth (initial commit to v0.3.x)

Heavy per-edit advisories: self-review-trigger, minimal-edit-audit, anti-slop-audit. Salience dilution killed them.

Phase 1: The contract emerges (v0.4.0 to v0.6.9)

Stash-and-drain pattern: scope-refresh stashes, scope-drain delivers on next tool boundary.

Phase 2: The v1.0 reset

v1.0 reset

Deleted almost everything. Shipped five hooks. Added precompile back in 1.1.0 because the contract arrived too late without it.

Phase 3: doctrine-ultra (v1.3.0 to v1.3.5)

Tri-role model, milestone verify, verify-revise loop, files\[\] as session edit surface.

Phase 4: Hard gates (v1.4.0 to v1.5.1)

step0-gate on preToolUse (v1.5.0): second hard lever beside permission-gate.

Current branch tips (all merged into main):

BranchCommitStatus
mainc6803c1HEAD, v1.5.1
feat/step0-contract-nudges-v144de7fb99Merged
fix/scope-hardening-v1451bc7403Merged
feat/step0-gate-v150e49dfefMerged
fix/final-review-minimality-v151bf44e74Merged

Current hook map (v1.5.1)

Hook map

What got pruned

  • anti-slop-audit, minimal-edit-audit — moved to final-review axis 4
  • semantic-density-audit, subagent-stop-review — experiments, cut
  • scope-gate-audit (Compuerta 1) — useful, didn't survive v1.0 reset
  • self-review-trigger — replaced by scope-refresh + final-review

Install

npx cursordoctrine@latest install

GitHub: github.com/kleosr/cursordoctrine

That's the agentic mind in text: Thinker, Worker, Verifier. One model. Nine hooks. Two hard gates, thank you for reading.

Best,
kleosr.

2 Likes