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:
- Inject a doctrine at session start. A short governing text (
doctrine.md+USER-RULES.md) lands in every chat asadditional_context. Smallest correct diff, verify then stop, ask don’t guess. Set it up once. - 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.
- Gate blast radius. One
beforeShellExecutiongate 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
- You give the agent a task.
- The agent edits a file →
afterFileEditrecords the edit and stashes the self-review prompt. - The next tool call →
postToolUsedrains the pending file intoadditional_context. The model audits its own diff and fixes what’s broken. - The agent finishes →
stopsubmits onefollowup_message: a final review over every file touched this session. - 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 prefersjq, falls back topython3, 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
- Clone GitHub - kleosr/cursordoctrine · GitHub and open
INSTALL.md. - Paste its contents into a Cursor agent chat on the target machine (the box where Cursor will run the hooks).
- 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
~/.cursorand~/.agents/hooks, runs every hook by hand with fake payloads, then has you restart Cursor. - Verify inside Cursor: the doctrine answers from context, a small edit triggers the self-review,
git push --forcegets 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. afterFileEditoutput isn’t consumed by Cursor, so edit hooks write to a pending file andpostToolUsere-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.





