How-to guide

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.

Tested with mneme-hq 0.9.2 · Claude Code

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-hook command 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 check call 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 cat heredoc 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 db isn't caught by this ADR. See Guide 1.
  • Other agents get different coverage. Codex CLI checks apply_patch add 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.

Next