Rules from Memory to Code — Can Claude Code Mods Enforce My Blog’s Writing Rules?#

2026-10-11

On the left, a bulletin board with paper notes pinned to it stands beside an open doorway that nothing blocks; on the right, the same doorway has metal latches and gears built into its frame that physically stop passage

Rules that don’t get followed#

This blog has quite a few rules for writing. In the Korean original, the first person is written as “필자” (a formal “the author”) rather than the casual “저는” (“I”). External links use an HTML tag that opens a new tab. Measured figures such as visitor counts never go into a public post. When a Korean post changes, its English translation at the same path changes too. When an agent skill is edited, both the Claude Code copy and the Cursor copy are updated together. Every one of these rules is written in a Markdown file, and Claude Code reads that file when a session starts.

And yet these rules get broken often. The one breaking them is not a person but the agent. After a long session compacts its context, the casual “저는” reappears, and the Korean post gets fixed while the English one stays as it was. Writing the rule in bolder type or moving it to the top of the file changes nothing fundamental. As long as a rule lives in the model’s memory, how well it is followed is a matter of probability.

In the last AI news brief, where I covered Claude Code Mods, I wrote in the “what to watch” note: “I plan to test whether some of the prohibitions I keep as document rules can be moved into a mod and enforced.” This essay is the record of what I worked out before running that test. What exactly Mods are, how the same requirement would have been handled before Mods existed, and what actually changes when a mod takes over. I compared them all against one guard.

The conclusion first. Going from a document rule to a settings hook is the shift from memory to enforcement. Going from a settings hook to a mod is the shift from a script to a program with state. For a single one-line ban, the second shift gains little. It gains a lot when a rule spans several tool calls or when a human decision has to be inserted in the middle.

What Mods are#

The following is based on Anthropic’s announcement and the official Claude Code documentation as of October 2026.

A mod is a plugin containing JavaScript or TypeScript event handlers that run inside the Claude Code process. Claude Code emits an event each time it is about to do something: call a tool, receive a prompt, draw a part of the screen. A mod steps into that event and can watch it, rewrite it, or take it over entirely.

Claude Code already had a mechanism called hooks. You register a shell command or an HTTP request in a settings file and it runs on a given event. To tell the two apart, the official documentation calls the existing kind a settings hook and calls a mod’s handler simply a hook. The difference is where they run. A settings hook runs outside Claude Code as a separate process; a mod runs inside it as a function. That is what gives mods abilities nothing else has: drawing a pane or buttons on screen, restyling the spinner or a tool result row that Claude Code draws itself, holding a tool call while asking the user a question, and running a slash command immediately without a Claude turn (a turn being the unit of work from one prompt until Claude finishes answering it).

A small mod is three files: the plugin manifest plugin.json, a hooks.json that points to the code file, and the code itself in register.js. The whole skeleton of the code is one call, on(event, matcher, handler). The matcher is a filter for which of that event’s occurrences the hook receives, and the handler always takes three arguments.

  • $ is the Mods API. Reading a file, starting a process, reaching the network, drawing on screen, calling a model: everything outside the mod’s own code goes through this object. A mod runs in an isolated environment with no DOM and no Node, so there is no other way out.
  • e is the event’s input. It is deeply frozen, and assigning to it throws.
  • next(e) calls the next mod’s handler, and at the end of the chain runs Claude Code’s own behavior and returns its result.

How a handler uses next is what the handler is. Do some work and return next(e) as it came: that is observing. Pass a modified copy, as in next({ ...e, text: changed }): that is rewriting. Return { deny: 'reason' } without calling next: that is answering in Claude Code’s place. You can also await next(e), then retry the call or withhold its result, which is post-processing.

When several mods handle the same event, the one loaded first sits outermost. It sees the event first and the result last. The order is: mods an organization manages, mods the user installed, mods the organization appends, then the remaining built-in mods. When you are signed in with a Team or Enterprise plan, or the machine has managed settings, a built-in guard mod named sec-default loads ahead of everything and stops a user’s mod from overriding the organization’s deny rules or editing the system prompt.

One thing to know up front: mods are not sandboxed. They act with the permissions of the user who installed them, reaching files, processes, the network, and environment variables. Even with the Bash sandbox on, a process a mod starts runs outside it. The documentation recommends running claude plugin validate before installing a mod to list, without executing it, which events it handles and which API methods it calls.

Some of Claude Code’s own features have already been rebuilt as mods: the /diff command, loading AGENTS.md, and sending usage analytics. Anthropic has also published sample mods. token-weather draws a forecast of how full your context window is above the prompt. blast-radius holds a risky command such as rm -rf, shows what it would change, and asks whether to proceed. replay-theater adds a /replay command that steps through the files Claude edited in the last turn.

The same guard three ways#

Back to the original problem. Let me build one guard, the one that blocks the casual “저는” in a Korean post, in three ways.

A document rule: leave it to memory#

This is where the blog is today. A line in the rules file, and the hope that the model follows it. There is no control point, so it is the easiest to set up and the most often broken. How well it is followed depends on the model, the length of the context, and whether the rule survived the last compaction summary.

A settings hook: block from outside#

Enforcement was possible before Mods. The tool is a PreToolUse settings hook. Register a shell script against the Edit and Write tools, and Claude Code starts the script just before running the tool, passing the tool’s arguments as JSON on standard input. If the script exits with code 2 or prints a JSON decision to deny, the tool doesn’t run, and Claude reads whatever the script wrote to standard error as the tool’s result.

{ "hooks": { "PreToolUse": [{ "matcher": "Edit|Write",
  "hooks": [{ "type": "command", "command": "bash .claude/hooks/guard.sh" }] }] } }
# .claude/hooks/guard.sh
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path')
TEXT=$(echo "$INPUT" | jq -r '.tool_input.new_string // .tool_input.content')
case "$FILE" in
  */content.ko/*)
    if echo "$TEXT" | grep -q '저는'; then
      echo '"저는" 대신 "필자"를 쓰세요' >&2
      exit 2
    fi ;;
esac
exit 0

The error message is in Korean because Claude reads it as the tool’s result and then rewrites the Korean post. With this alone, the first shift is complete. The rule has moved out of the model’s memory and into a checkpoint on the execution path. Even if the model forgets the rule, the file doesn’t change.

A mod: block from inside#

The same guard as a mod looks like this.

on('tool.call', { tool: ['Edit', 'Write'] }, ($, e, next) => {
  const text = e.tool === 'Edit' ? e.new_string : e.content
  if (/content\.ko\//.test(e.file_path) && /저는/.test(text)) {
    return { deny: '"저는" 대신 "필자"를 쓰세요. 수정해서 다시 시도하세요.' }
  }
  return next(e)
})

To be honest, at this level it is almost the same as the settings hook. Both block just before the edit, both hand Claude a reason, and both lead Claude to fix the text on the next try. The only convenience is reading a typed field like e.new_string instead of parsing JSON with jq. For a one-regex ban, the settings hook is actually the lighter choice.

Where the gap opens#

The gap appears as soon as the guard gets a little more complex. Summarizing from the official documentation:

AspectSettings hookMod
ExecutionA shell process per callA function in the same process
ReachDeny before the call, replace argumentsBefore and after the call, retry, return a result directly, withhold a result
State between callsNone; must write to a fileModule variables, session state, a cross-session store
Asking the userCan only escalate to the permission promptCan pose its own question and hold the call
Changing a permission decisionNot possibletool.check can turn allow / ask / deny into another decision based on context
TestingFeed fake JSON by handclaude plugin test fires events with no session
Shipping and reviewCopy a settings file and a script separatelyOne plugin; validate inspects it before it loads

The row I weighed most is state between calls. The rules this blog breaks most often are not one-line bans like “저는” but pair rules: “if the Korean post changed, change the English one,” “if the Claude Code skill changed, change the Cursor copy.” A rule like that cannot be judged from a single tool call. Something has to remember which files changed during a turn and check at the end whether a partner is missing.

To do this with settings hooks, a PreToolUse script writes paths into a temporary file, and a Stop script that runs at the end of the turn reads that file and compares. Two scripts stitched together through a file. In a mod, the tool.call handler pushes the path into a module variable and the turn.complete handler reads it. The two handlers live in the same file, so the design is natural.

The second row that matters is asking the user. Take the rule against publishing measured figures. Not every number in a post is a measurement, and a regular expression cannot tell whether a figure came from the analytics dashboard. A mod can hold the call with $.ui.ask in a doubtful case and put the question to a person. A settings hook can only escalate to the permission prompt; it cannot design the choices.

The third is testing. With ten or so rules, a shell script becomes hard to read, and there is no good way to check that fixing one rule didn’t break another. A mod can keep a test file inside the plugin and fire events against it without a session.

There is also a point where a mod is no safer. Both default to fail-open. A settings hook that times out lets the tool call proceed, and a mod whose handler throws or exceeds its time limit is skipped. Recent versions let a settings hook fail closed with the onFailure: "block" option, and a mod does the same with a .catch handler. Either way, that setting has to be made explicitly before the word “enforce” is earned.

Mods where there is no screen#

While researching, I found one fact that lands directly on my own setup. I work inside the Claude Code extension in the Cursor IDE, and according to the documentation’s table of environments, in the chat panel of a VS Code-family extension, a mod’s handlers run, but nothing it draws appears. Cursor uses the same extension, so the same applies. Panes and buttons show only in a terminal and in the Code tab of the Desktop app.

My first reaction was “then what is the point of using mods?” On reflection, that was wrong. None of the guards compared above need a screen. The deny reason is delivered not through the interface but as the tool result Claude reads. The pair-check warning can be delivered as a line appended to the conversation when the turn ends. The only things that need a screen are a pane such as a token monitor and a question posed to a person, and when I need those, I can run claude directly in Cursor’s integrated terminal. The documentation classifies an editor’s integrated terminal as an environment with full drawing support.

So is the missing screen a design limit of Mods, or has the extension simply not caught up? The documentation doesn’t say, so I read the TypeScript declaration file that Claude Code generates for mods and made an inference. The declarations already define four kinds of drawing target: terminal, desktop, mobile, and vscode, and vscode is labeled “Claude Code for VS Code.” Structurally, only the terminal is drawn by the engine itself; the other three ask the engine for what to draw and render the returned tree on their own. There is also a separate list of elements that vscode can draw. The engine and the protocol look ready, and the extension’s drawing side looks like the part that hasn’t arrived. But this is an inference read from code, not a schedule Anthropic has announced. Cursor also has its own extension marketplace, so it may arrive later there than in VS Code.

Mods worth building for this blog#

Based on the research, these are the mods I actually intend to build. The first five work without a screen, so they take effect in the extension environment I use now. The last two matter when running in a terminal.

  • A content rule guard: when a file in the content directory is edited, reject pattern-detectable violations before the edit runs: the casual “저는” in a Korean post, Jekyll-style link syntax, banned translation terms. Rules go in a single array, each with its own test. This is the first one I’ll build.
  • A pair check: remember which files changed during a turn, and at the end of the turn report whether only the Korean post changed while the English one stayed put, or only the Claude Code skill changed while the Cursor copy didn’t. This is the representative case where a mod is more natural than a settings hook.
  • Front matter and image validation: when a content file is written, send it back if a required field is missing or the file named in images doesn’t exist. Today a pre-commit hook, which runs just before a commit, catches this; the mod catches it much earlier.
  • A deploy hold: catch a Bash call that is about to run the deploy script and check first that the local build succeeded. The same idea as the blast-radius sample.
  • A build check at the end of a turn: after a turn that edited content, quietly run the Hugo build and report a failure. Broken syntax gets caught on the spot instead of in the next session.
  • A measured-figure question: when a number and the name of an analytics tool appear together in a post, hold the call and ask a person, “This looks like a measured figure. Keep it?” Where there is no screen, the question never reaches anyone, so the catch side has to be designed to block by default.
  • A token and cache monitor: collect cache hits and token usage per model request and show them in a pane. Close to what the token-weather sample does, and a way to see with my own eyes what I wondered about in the prompt caching essay.

Whatever I build, a few things hold throughout. A blocking mod always gets a .catch so it fails closed. A deny reason is written as an instruction Claude can read and act on immediately. And rules that a pattern cannot catch, such as “explain a term the first time you use it” or “does the prose read naturally,” stay in the document rather than moving into a mod. A mod could call a model to judge them, but that adds cost and latency to every call, and in the end it is just the memory approach wrapped one layer deeper.

Closing#

When I first saw Mods, my interest was in one sentence: “enforce rules as code.” Having done the research, that sentence is half right. Enforcement itself was already possible with settings hooks. What Mods newly open is not enforcement but rules that carry state, ask a person, get tested, and ship as a single plugin. In other words, a rule that stops being a script and becomes a program.

So the next step for this blog is clear. Right now every rule lives in memory, so moving in either direction is an improvement. A one-line ban is fine as a settings hook, but if the rules broken most often are the ones that span a turn, like the pair check, it makes sense to go straight to a mod. I plan to bundle the “저는” guard and the pair check into one plugin, build it, and write up what I find.

One more thing remains, which is the fact that a mod is code. Until now, most of the work of managing an agent has been writing documents. Mods turn part of that back into programming. Settling something in code is not always better than persuading with a document, but if you already know which rules are the ones that get broken, I believe those rules at least belong on the code side.

References#