A walkthrough of two gates inย MoFloย โ search memory before digging through files, write down what you learned before opening the PR โ including the parts I got wrong the first time.
- GitHub:ย github.com/eric-cielo/moflo
- npm:ย npmjs.com/package/mofloย โย
npm install --save-dev moflo
Why a rule in CLAUDE.md isn’t always enough
If you’ve been using Claude Code for a while, you’ve written rules into CLAUDE.md. Mine included “search memory before you grep” and “store what you learned before you open the PR.”
Those work most of the time. The failure mode is the part worth talking about. Three hours into a session, Claude will grep its way across a directory that the semantic index already had a good answer for. Or it’ll work through something genuinely hard, open the PR, and the thing it figured out along the way is just gone.
I spent a while trying to fix that by rewording the instructions. It didn’t help much, and in hindsight it wasn’t going to. Context is finite, long sessions drift, and the model is spending its attention on the task you gave it rather than the housekeeping you attached to it. That’s not really a defect you can prompt your way out of.
The thing that did work: Claude Code lets you attach hooks to tool calls. If you’re new to them, the one behavior to know is that a PreToolUse hook which exits non-zero blocks the tool call. The tool never runs, and whatever the hook wrote to stderr gets handed back to Claude as feedback. So instead of hoping Claude remembers the rule, the rule is checked on the way to the tool.
The Steps
But using a hook effectively has some nuance. I found that the following pattern worked well as a basic approach that we repeat in a number of hooks.
- Arm โ something decides the rule is in play (a new prompt, a heuristic, a config flag).
- Intercept โ
PreToolUseon every tool that could get around it, shell included, exiting non-zero with a useful message to force evaluation. - Credit โ
PostToolUseon whatever satisfies the rule, written somewhere that persists to know we’ve satisfied the rule. - Expire โ clear the credit when the thing it described changes.
Two of MoFlo’s gates are built that way. They’re worth walking through together because they sit at opposite ends of a run and the second one has some non-obvious design problems.
The memory gate
MoFlo indexes your project into a searchable store: guidance docs, a code map, and learnings from previous sessions. The rule is to search that before going through files by hand, since it’s cheap and it hits often.
Four hooks enforce it. Three of them do the blocking, all PreToolUse, each covering a different way of getting at a file:
| Matcher | Gate command | What it covers |
|---|---|---|
^(Glob|Grep)$ | check-before-scan | Searching the tree |
^Read$ | check-before-read | Opening a file cold |
^(Bash|PowerShell)$ | check-bash-memory | The same thing via cat, grep, sed in a shell |
That third row is the one I’d flag if you’re building something similar. The first version didn’t have it. If you gate Read and Grep but leave the shell alone, cat src/foo.ts does the same job with nothing in the way โ and that’s the path the model will find. When you gate a tool, go looking for the other tools that do the same thing.
The fourth hook is what lets you through. It’s PostToolUse matched on ^mcp__moflo__memory_(search|retrieve|list|stats|store)$, and it flips memorySearched in .claude/workflow-state.json. Once that’s set, reads and greps go through untouched.
Something has to turn the gate back on, and that’s a UserPromptSubmit hook โ new question, fresh look at memory. Note that it deliberately doesn’t reset on task transitions. An earlier version did, and it would re-block an agent halfway through a workflow it was already executing, which is maddening to debug.
The same hook decides whether to arm at all. Very short prompts don’t arm it, so “ty” or “yeah go ahead” passes straight through, and prefixing a prompt with @@ skips it on purpose. Worth doing if you build one of these โ a gate that fires when the user says thanks gets turned off pretty quickly.
When it does block, the message names the tool to call, the namespace to call it with, a CLI fallback for sessions where the MCP server isn’t connected, and the moflo.yaml key that disables it. Blocking without saying what would satisfy the check leaves Claude guessing, and it usually guesses wrong.
The learnings gate
The same idea applies at the other end of the run. The memory index only gets better if something keeps writing to it, and the moment you’d naturally write is the moment everyone wants to move on.
Soย gh pr createย is gated. Aย PreToolUseย hook on Bash matches the command and holds the PR until tests are green,ย /flo-simplifyย has run, and something durable has been recorded.
The wording of that last one took a couple of passes, and this is the part I’d actually want to call out as a take away. The first version saidย “learnings have not been stored (call memory_store).”ย Accurate, and yet it produced junk. It named a mechanism without saying what a good entry looked like, so the cheapest way past it was a summary of the run that had just finished. It essentially produced audit exhaust โ stored permanently, which pushes genuinely useful entries down the ranking in every future search. The gate passes but the index unintentionally gets worse.
The message now states the intent instead. A lesson qualifies only if it would help a future session working on aย differentย task โ a reusable pattern, a trap, a decision plus its rationale.
It also needed an honest way out. Plenty of runs don’t teach you anything, and if the only move that satisfies a gate is writing something, you’ll get something written. So there’s a command that records “nothing durable here” and passes without touching the index. Two details about it mattered more than I expected:
- It has to be findable. The command is printed in the block message itself, as an absolute path worked out at runtime. Claude is typing it into a shell, whereย
$CLAUDE_PROJECT_DIRย isn’t set and the working directory isn’t guaranteed, so a relative path would break depending on where it ran. - It has to check its own work. It re-reads the state after writing and fails loudly if the flag didn’t stick. Without that you get told you’re clear, the write silently doesn’t land, and the nextย
gh pr createย blocks again with nothing new in the message. That’s a deadlock, and it cost me a session before I fixed it.
At the end of each task, we run tests and simplify. The test and simplify passes are fingerprinted against the code they ran on, so editing through Bash, aย git checkout, or moving to a second issue in the same session all clear them. You want the flag to mean “tests passed on this code,” not “tests passed at some point today” โ otherwise it drifts into being decorative. Basically we’re expiring/clearing the credit that was given towards these steps if something changes so that they’re forced to run again.
The same shape works for other rules
These gates were presented as examples, but both use the same four mechanisms mentioned previously: arm, intercept, credit, expire.
None of that is specific to memory. Any rule shaped like “X before Y” drops into the same wiring: scan for secrets before writing config, check a license before adding a dependency, require a schema review before a migration lands, look up ownership before touching a protected path. You’re swapping the check in the middle and leaving the structure alone.
If you build one, spend real time on the message you print when you block. It should say what’s missing, what would satisfy it, how to skip it legitimately, and where the off switch is. The check itself is usually the easy half.
MoFlo runs both of these (and others) on its own repo, which is how they ended up this specific. Every rough edge above is one I hit first. Feel free to check out the code in GitHub or point your Claude at it to help it better understand.
- GitHub:ย github.com/eric-cielo/moflo
- npm:ย npmjs.com/package/mofloย โย
npm install --save-dev moflo


Leave a Reply