Stop an AI coding agent from crossing an architectural boundary
Load the Mneme plugin in Claude Code. Before each Edit, Write or MultiEdit, and before a single cat > file <<'EOF' command, its hook checks the change against your recorded decisions. In strict mode, the default, it blocks a violation with exit code 2, so the file doesn’t change and the agent sees the reason. Other shell writes, scripts included, get past that first check; the hook catches them when the agent ends its turn and sends it back to fix them.
You’ll end up with: Claude Code refusing to write a direct database import under app/handlers/, with ADR-012 named in the feedback the agent receives.
Goal
A pull request check catches a forbidden import after the agent wrote it, and someone still has to send the work back. A hook catches it while the agent is still working on the change. The plugin registers one: a command Claude Code runs before every file edit and shell command, at session start, and each time the agent finishes a turn.
Prerequisites
- A git repository with ADR-012 imported into
.mneme/project_memory.json. Guide 1 covers it. - Mneme 0.9.2 installed with pipx, so the
mneme-hookcommand is on the PATH Claude Code uses:
pipx install "mneme-hq==0.9.2"
- Claude Code. The plugin isn't in the Claude Code marketplace yet, so you load it from a checkout of the Mneme repository. The Claude Code integration page lists what the plugin contains.
Configure
1. Get the plugin at the release tag
git clone --branch v0.9.2 --depth 1 https://github.com/MnemeHQ/mneme mneme-src
2. Start Claude Code with the plugin, from your repository root
claude --plugin-dir ./mneme-src/integrations/claude-code-plugin
The plugin runs in strict mode by default: a violation blocks the change. Leave it there. Warn mode (MNEME_HOOK_MODE=warn) stops the hook blocking edits, and Claude Code doesn't show the reason for each edit to the agent or to you. See Troubleshooting.
Run
Ask Claude Code for a change that breaks ADR-012, for example: “Add a list_invoices handler in app/handlers/invoices.py that queries the invoices table with get_connection from app.db.client.”
When the agent goes to write the file, Claude Code sends the hook an event like this one. We shortened the paths to relative ones; Claude Code sends absolute paths.
{
"hook_event_name": "PreToolUse",
"session_id": "guide-2",
"cwd": ".",
"tool_name": "Write",
"tool_input": {
"file_path": "app/handlers/invoices.py",
"content": "from app.db.client import get_connection\n\n\ndef list_invoices(customer_id: str):\n conn = get_connection()\n return conn.execute(\"SELECT * FROM invoices WHERE customer_id = ?\", (customer_id,))\n"
}
}
You can replay the event without an agent. That's how we tested this guide:
mneme-hook < events/write-direct-import.json
Expected result
The hook exits with code 2 and prints:
mneme: FAIL - architectural decision violated
[ADR-012] FAIL "from app.db.client import" - trigger: from app.db.client import
Request handlers do not import the database client
path: app/handlers/invoices.py via app/handlers/**
Claude Code treats exit code 2 from this hook as a block. The file isn't written, and the agent receives that text as feedback. A write that calls the repository instead exits 0 with no output, so the agent can carry on.
Shell writes
The hook also reads shell commands. A single command of the form cat > app/handlers/invoices.py <<'PY', with a quoted delimiter and nothing else in the command, is blocked the same way:
mneme-hook < events/bash-heredoc.json
mneme: FAIL - architectural decision violated
[ADR-012] FAIL "from app.db.client import" - trigger: from app.db.client import
Request handlers do not import the database client
path: app/handlers/invoices.py via app/handlers/**
Other shell writes can't be checked before they run: echo >, tee, sed -i, an unquoted heredoc, a heredoc chained after another command, or a script such as python scripts/gen_invoices.py. The hook can't reconstruct what they will write. The hook lets them through and says so:
mneme-hook: Bash classified POTENTIALLY_MUTATING: no deterministic pre-execution check (session-delta backstop still applies).
The end-of-turn check catches them instead. At session start the hook records the state of the repository. When the agent finishes its turn, the hook compares the repository against that baseline and checks every changed file:
mneme-hook < events/session-start.json python scripts/gen_invoices.py mneme-hook < events/stop.json
{"decision": "block", "reason": "mneme: repository mutations made during this session violate 1 governed artifact(s):\n[app/handlers/invoices.py]\nmneme: FAIL - architectural decision violated\n [ADR-012] FAIL \"from app.db.client import\" - trigger: from app.db.client import\n Request handlers do not import the database client\n path: app/handlers/invoices.py via app/handlers/**"}
Evidence
Every FAIL block names the decision (ADR-012), the matched text, and the path selector that made the rule apply. Behind the hook is the same mneme check from Guide 1. To keep a machine-readable record of a file's state, run it with --json:
mneme check --memory .mneme/project_memory.json \ --input app/handlers/invoices.py \ --target-path app/handlers/invoices.py \ --query "edit to app/handlers/invoices.py" \ --json
{"schema": "mneme.check/v1", "verdict": "FAIL", "mode": "strict", "evaluation_complete": true, "applicability": [{"decision_id": "ADR-012", "rule_type": "FORBID_LITERAL", "rule_value": "from app.db.client import", "rule_index": 0, "path_scoped": true, "input_path": "app/handlers/invoices.py", "outcome": "APPLIED", "selector": "app/handlers/**", "reason": "an include selector matched"}], "violations": [{"decision_id": "ADR-012", "decision_text": "Request handlers do not import the database client", "severity": "FAIL", "rule": "from app.db.client import", "trigger": "from app.db.client import", "kind": "typed_rule", "rule_type": "FORBID_LITERAL", "input_path": "app/handlers/invoices.py", "selector": "app/handlers/**"}], "freshness": []}
Troubleshooting
Nothing is ever blocked, and the hook prints nothing
The hook didn't find a memory file. It looks for .mneme/project_memory.json in the session's working directory and each parent directory, or at the path in MNEME_MEMORY. With no memory file it allows every change silently:
MNEME_MEMORY=missing.json mneme-hook < events/write-direct-import.json
Start Claude Code from the repository root, or set MNEME_MEMORY.
not inside a git work tree
{"hookSpecificOutput": {"hookEventName": "Stop", "additionalContext": "mneme: not inside a git work tree; the session-delta gate is inactive for this turn."}}
The end-of-turn check compares against git. Run Claude Code inside a git repository. Checks before each write still work without git.
Edits that break ADR-012 go through, with no message
MNEME_HOOK_MODE=warn is set. In warn mode the hook hands the edit back to Claude Code's normal permission flow with the violation as a reason:
{"hookSpecificOutput": {"hookEventName": "PreToolUse", "permissionDecision": "defer", "permissionDecisionReason": "mneme: FAIL - architectural decision violated\n [ADR-012] FAIL \"from app.db.client import\" - trigger: from app.db.client import\n Request handlers do not import the database client\n path: app/handlers/invoices.py via app/handlers/**"}}
Claude Code ignores that reason, so neither the agent nor you sees it. Only the end-of-turn check reaches the agent in warn mode. Unset the variable to go back to strict mode, and use mneme check or the CI check to collect findings instead.
Limits
- The hook fails open. If a
mneme checkcall crashes or takes longer than 10 seconds, the hook allows the change without telling the agent. Its notes go to Claude Code's debug log only. - Shell writes other than that single
catheredoc form are checked only when the agent finishes its turn, after the file exists. The hook then keeps the agent working and feeds back the violation. It doesn't undo the write. - The rule matches exact text, so
import app.db.client as dbisn't caught by this ADR. See Guide 1. - Other agents get different coverage. Codex CLI checks
apply_patchadd and update operations before they run, and audits each changed file as a whole when the agent stops. Kiro checks its native write tools only, with no shell or end-of-turn checks.