The Hygiene Gate: Making an AI Earn Its Memories

I was debugging a recall problem in Engram a while back, trying to figure out why a search for project context kept returning junk, and I ended up just dumping the raw memory store to see what was actually in there. It was not great. Somewhere around a third of the entries were things like “Session complete, all tasks done,” “PR merged, pushed to main,” “GROUP B COMPLETE. NEXT SESSION: start Group C.” Entries that were technically true the moment they were written and worthless roughly forty minutes later. Meanwhile the stuff I actually wanted back, like why we went with a REQUEST authorizer instead of the Cognito one, was either missing entirely or buried inside one of those session summaries as half a sentence.

The frustrating part is that the model was doing what I asked. I built a memory system and told it to store important things, and at the end of a session the summary of that session genuinely feels like the important thing. It’s recent, it’s complete, it wraps everything up. It’s also the least useful category of memory that exists, because semantic search doesn’t age anything out. Every “wrapped up today” entry I let through is still sitting there months later, matching queries, taking a top_k slot from something that mattered.

Instructions got me to about 75%

First attempt was the obvious one, rules in CLAUDE.md. Don’t store session summaries. Generalize lessons before storing them. Check whether a state entry for this project already exists before creating another one. And this mostly worked, I’d guess 75% of the time, but memory pollution is one of those problems where a 25% miss rate just means the pile grows slower. It still grows. Several stores per session, every day, and the misses accumulate in a place where nothing ever expires.

I’d already been through this exact cycle on the recall side, where “remember to call recall_memory before proposing solutions” worked right up until it didn’t, and the fix there was a hook that injects the memories into context whether the model thinks to ask or not. So the write path got the same treatment, with the difference that the write path needs to say no, which is a more satisfying thing to build than I expected.

What the hook actually does

store-hygiene-gate.sh is a PreToolUse hook, which in Claude Code terms means it runs before the tool call executes and can cancel it by exiting non-zero. Claude Code hands it the tool input as JSON on stdin, it pulls out the memory text with jq, and then it runs the text through a filter that is, frankly, not sophisticated:

ANTI_PATTERN_RE='(session (complete|end|summary|wrap)|group [a-z] complete|next session[: ]|\[backlog\]|pr merged|pushed to (main|master)|task complete|wrapped up today|completed (today|this session)|all (tasks|steps) (done|complete))'

if echo "$TEXT" | grep -qiE "$ANTI_PATTERN_RE"; then
  ...
  exit 2
fi

That regex is a list of the exact phrases I kept finding in the memory dump. Nothing clever, no classifier, just the observed slop patterns written down. There’s also a word-count floor, anything under 8 words gets rejected, because a memory has to make sense without the conversation it came from and “fixed the Lambda bug” does not. When I added that check I ran it mentally against the existing store and a depressing number of my OWN early entries wouldn’t have passed.

When the gate blocks a call, the model gets a message back telling it the store was cancelled and pointing it at /hygiene, which is a skill that does the actual thinking. And this turned out to matter more than the blocking itself, because the failure mode isn’t “information lost,” it’s “go reprocess this into something worth keeping.” I’ve watched a blocked session summary come back a minute later as a properly generalized entry about hook execution order. The summary died, the one useful fact inside it survived.

Anything that passes the filter still gets a reminder injected into context, confirm /hygiene was run, cancel and run it if not. Whether that reminder does anything, honestly I can’t prove it does. The hard checks I can verify from the exit codes. The soft nudge is more of a hope.

The skill, briefly

/hygiene makes the model classify every candidate memory as exactly one of STATE, DECISION, RULE, DISCOVERY, or QUIRK before storing, and the classification isn’t decorative, it changes what’s allowed. Two rules do most of the work:

STATE entries are singletons. One project, one “where things stand” entry, updated in place. Before this rule, every session ended with a fresh status memory and recall was an archaeology dig through six months of increasingly stale “current” states. Now the recall query has to find the existing entry first and propose an update to it. This one change probably cut more noise than the regex did.

RULES and DISCOVERIES have to pass a generalizability test, which asks whether the lesson is about this one resource or about a pattern. The example I keep coming back to, because it’s the one that taught me the difference: “aws_api_gateway_rest_api_policy must be imported before plan/apply or Terraform will overwrite it” is true and nearly useless, it only ever matches if I touch that exact resource type again. The version that earned its place in memory is that ANY resource created outside Terraform and never imported is invisible to plan, and Terraform will silently overwrite it, with the API Gateway incident attached at the end as a citation. Same lesson, but one of them fires in every Terraform project I’ll ever have.

Where this leaves things

The split ended up being a dumb deterministic gate at the execution layer and all the judgment in a skill the gate funnels the model through. I didn’t design it that way up front, it fell out of the regex being obviously insufficient on its own and the skill being obviously skippable on its own. Together they cover for each other reasonably well.

I still don’t have a good answer for slop that doesn’t match any known pattern, a plausible-sounding memory that’s subtly too specific or restates something already in the README. The regex won’t catch it, and the skill only helps if it actually gets run with honest effort rather than gone through as a checklist. There’s probably a version of this where a second model reviews candidate memories, and there’s also a version of me that doesn’t want to pay per-token for a bouncer. For now the periodic memory dump review stays on the calendar, and the pile grows a lot slower than it used to.