Skills only persuade the model, and they load only when it decides to (progressive disclosure). So a convention that lives only in a skill - “use UUID primary keys”, “prefer Prisma as the ORM for TypeScript” - is often skipped: the agent never reads the skill. Guardrails enforces conventions instead, outside the model.
After the agent writes a file, a post-edit hook runs a set of data-driven rules against it. A blocking violation is fed straight back so the agent fixes it in the same turn. Nothing runs until an edit lands, and a clean file produces no output - so the cost is zero tokens until there is an actual violation, and a tiny message when there is.
It is on by default and everything runs locally.
How it works
Each rule is data, not code - one entry, either built in or in ~/.enigma-guardrails.json:
- file rules regex-scan the edited file - e.g. a migration using
SERIAL/AUTO_INCREMENTinstead of a UUID key, a handler that assigns a request body without validating it (Zod/TS, Pydantic/Py), a raw<input type="password">that should use your reusable Input, a new password accepted with no breach check against Have I Been Pwned, an analytics snippet loading before the cookie banner is answered, a person-name field whose value is never capitalized, a nativealert/confirm/promptthat should be a design-system dialog, or atext-overflow: ellipsiswith no overflow value, which does nothing and lets the text spill. - project rules run a small built-in check against the project (e.g. a TypeScript project on a relational datastore with no Prisma).
- size rules set a
maxBytesbudget on a file instead of a pattern - used to stop an agent memory file (CLAUDE.md,AGENTS.md) from growing past 40 KB.
A file rule can also require a mitigation to be present: with an absent regex, the rule fires only when the risky pattern appears without the fix in the same file - that is how “reads input without validating it” is detected across languages.
Rules carry a severity: block is enforced (the agent hook exits non-zero, feeding the fix back to the model; a commit fails), warn is advisory (printed, never blocks). Messages name the owning skill (e.g. database-expert) so a fix is traceable.
Some violations never reach the model at all. Where the correct edit is mechanical, the hook applies it itself before reporting anything: a person-name <input> missing autocapitalize="words" gets the attribute written in, with the right casing for the file, and only that line is touched. A fixer declines anything ambiguous - a custom <TextField> that may not forward the attribute, two inputs on one line - and those fall through to the normal block, so the agent still handles what code cannot decide. Fixing costs no tokens and no turn; the message is only paid for when a human decision is genuinely needed.
A fixer can also belong to a diff-stage rule. fe-truncated-value-unreachable blocks a value that is clipped (truncate, text-overflow: ellipsis) with no title, aria-label or tooltip anywhere to recover the full text - a card that renders a long name or email and shows only its ellipsis. Its fixer runs in the post-edit hook, immediately, on whichever lines of the file the current change actually touched, rather than waiting for the turn-end sweep to find it; only what it cannot copy safely (a fallback expression, a call, a component that may not forward the attribute) still reaches the model, through the sweep.
One rule is about cost rather than correctness: ctx-memory-budget blocks an edit to a CLAUDE.md or AGENTS.md larger than 40 KB. A memory file loads into every session in the project, so a knowledge base living there is paid for on every task, relevant or not. Keep it an index - one line per topic - and move the detail into docs the agent opens on demand. Disable it with enigma guardrails disable ctx-memory-budget if you keep a large memory file on purpose.
The seed set is deliberately tiny and precise - it matches only unambiguous violations, so a false positive never trains the agent (or you) to ignore it. Add your own rules by dropping an entry in ~/.enigma-guardrails.json.
Two stages: the edited file, or the lines a change added
A rule also declares when it runs (stage):
edit(the default) is the post-edit hook and the commit backstop. They see a whole file, so a rule here has to be precise enough that your repository’s existing code never fires it - otherwise it blocks every unrelated edit and you switch it off.diffis only ever reported by the turn-end sweep, against the lines the current change added. There is no legacy backlog by construction, so the rule can only fire on code the agent just wrote. A fixer for it still runs earlier, in the post-edit hook - repairing is not a report, so it is not held back for the sweep, only the finding is.
That second stage is what makes a demanding convention affordable. fe-server-first-mutation is the first of them: it blocks a handler that awaits a mutation and only then touches the UI (await fetch(...), then drop the row from the list) - the optimistic-UI defect frontend-policy describes. In a measured corpus of real UI files, most components that mutate anything hold no optimistic update at all, so as a whole-file rule it would have fired forever on code nobody wrote today. Mark the line enigma:allow-server-first when the server’s answer really is required first (a payment, a server-assigned id, an irreversible action).
The sweep runs inside the turn-end completion hook (see Verified completion): blocking findings deny the stop, warnings ride along in the same message, and both are recorded. It is also the only channel that can reach the model with a warning at all - a post-edit warning exits zero and is printed where the model never reads it.
Where it runs
- Claude Code - a
PostToolUsehook (matcherEdit|Write|MultiEdit|NotebookEdit) for the edit stage, and the turn-endStophook for the diff-stage sweep. - opencode - an auto-loaded plugin that runs after each write/edit, through the same hidden hook Claude Code calls - so a diff-stage fixer still repairs what it can there. It has no turn-end hook that can answer the model, so an unrepaired diff-stage finding is never reported.
- Codex has no per-edit hook mechanism, so it is skipped (like auto-lint).
- Commit / CI backstop -
enigma securityalso copies the engine into.githooks/, so the same rules block a commit even if the agent hook was off or bypassed. Diff-stage rules stand down here: they are written for the lines a change added, not for a whole file.
What it keeps skipping
A rule that gets skipped leaves no trace. A block is fed back and forgotten, a warning exits zero, and an auto-fix is silent by design - so “the agent keeps ignoring this one” was never a number you could look at.
Every finding the agent is actually confronted with is appended to a local ledger (~/.enigma/guardrail-log.jsonl, one JSON line each, kept bounded). enigma guardrails stats reports it per rule in three columns: blocked (it was stopped and had to fix it), warned (it was told - or the turn-end gate stood down - and the code shipped anyway), fixed (repaired by code, no turn spent). The warned column is the point: it counts what the agent got away with, which is what tells a rule that should be promoted to block from one that only ever fires wrongly and should go.
Only the model-facing channels write to it - the post-edit hook and the turn-end sweep, never the commit backstop, whose whole-file scan would bury the signal under code nobody wrote today. The dashboard’s “Enigma Systems” row shows the 7-day count beside the active rule count.
The same file also holds a second population: the verify hook’s reply-style findings, one row per turn whose final message broke the output-style level. They answer a different question - “does the agent keep padding replies”, not “which convention keeps getting skipped” - so they are never counted alongside the rows above; enigma guardrails stats prints them as their own table.
Use it
enigma guardrails [cmd]
No argument lists the rules (built-in and custom, with their on/off state); check <file> runs the rules against a file (every rule, diff-stage ones included, so you can reproduce a finding you were blocked by); stats [days] reports which rules the agent keeps breaking, over the last days or over everything recorded, plus a second table for how often a reply broke the output style; disable/enable <id> toggles a built-in; remove <id> drops a custom rule. Toggle the whole feature with enigma config guardrails on|off (applies immediately to Claude Code and opencode); guardrails: false in a repository’s .enigma.json stands the turn-end sweep down for that project.
Add a custom rule
Custom rules live in ~/.enigma-guardrails.json. A file rule is a regex over the edited file:
{
"rules": [
{
"id": "no-select-star",
"label": "No SELECT *",
"files": ["*.sql"],
"scope": "file",
"pattern": "select\\s+\\*",
"flags": "i",
"message": "Avoid SELECT * on hot paths (database-expert).",
"severity": "warn"
}
],
"disabled": ["db-uuid-pk"]
}
disabled turns off a built-in by id. Add "stage": "diff" to a rule that should only be judged on the lines a change adds - that is how you write a demanding rule for a convention your existing code does not follow yet. That’s the whole authoring surface - one JSON entry per convention, no code.
What guardrails can’t check
Guardrails see the syntax of the code, not intent or behaviour. Some conventions have no reliable syntactic signature - “reads are cached for 30s”, “the form validates in real time”. A regex for those is either impossible or pure noise. Those live in enigma’s always-on memory instead (the “Engineering Defaults” block), stated once, language-agnostic, so the agent applies them by default without a hook. Guardrails back up the syntactic half; the memory covers the semantic half.
A rule also only sees the file it is scanning. fe-server-first-mutation catches the clearest shape of a server-first mutation, but a handler that hides the wait behind a parent refetch (await onChanged(), await load()) is the same defect with no local state write to judge - that half stays persuasion, and frontend-policy says so explicitly.
Privacy
Everything runs in-process on your machine; nothing is sent anywhere. The ledger is a local file holding rule ids, file paths and line numbers - never file contents. Turning the setting off removes the hooks from your agents.