Compiled Traps: Delivering Agent Memory Only at the Moment It's Dangerous

Gerard Hale · 2026-08-23 · updated 2026-09-27

Every long-running coding agent has the same memory problem: the things it has learned the hard way — “never run aide --update blind,” “never symlink this store,” “always clean before this Maven phase” — either get reloaded into context on every single session (expensive, and it drowns the genuinely relevant lessons in noise), or they get forgotten the moment the session that learned them ends.

We solved this for our own Claude Code workspace with what we’ve been calling the trap engine. After three searches for prior art we haven’t found this specific shape anywhere else: compiling narrative, human-authored memory notes into a matcher table that fires — and only fires — at the exact tool call the memory is about.

Not to be confused with “AI agent traps”. Since Google DeepMind’s 2026 taxonomy, that phrase means attacks: adversarial content planted in web pages, documents or an agent’s memory to manipulate it, sometimes lying dormant until a later context triggers it. The traps in this post are the opposite: defensive warnings, written by the agent’s own team from lessons they learned the hard way, and delivered at the tool call where the lesson applies.

The problem with “just load everything”

Our workspace’s agent memory already had a sensible tiering system: a durable cross-host store (Open Brain), a git-tracked per-project store, and a host-local auto-memory cache. All of it gets read at session start. That’s fine until the store grows — every new memory is space that isn’t going to a memory that’s actually relevant to this session, and past a certain size the whole index gets harder to recall accurately, not easier.

But a chunk of what’s in there isn’t “background knowledge I should always have” — it’s a trip-wire. It’s only useful in the ten seconds before a specific command runs. aide --update on an already-compromised host launders the compromise into the new baseline. That fact is worthless in context for the other 99% of a session where nobody is touching aide. It’s only worth anything in the moment right before that exact command is about to execute.

The fix: compile the memory, not just store it

So we split the trap out of “always loaded” and into “loaded on match.”

A memory file gets one blockquote line directly above the paragraph it’s annotating:

> TRAP tool=Bash action=warn pattern=(?i)\baide\b[^\n]*\s(--update|-u)\b

**Never `aide --update` blind** — it records whatever is on disk as correct,
laundering a compromise into the baseline. Verify first, always.

A compiler (trap-compiler.py) walks every memory store, finds these annotations, and emits a single traps.jsonl — one row per trap: which tool it watches, what pattern it matches against (a Bash command, a file path, or — since September — the text being written; see below), and the exact paragraph to surface if it fires.

A PreToolUse hook checks every tool call the agent is about to make against that compiled table. On a match, the paragraph gets injected as additionalContext — right there, right before the dangerous call, and nowhere else. The rest of the session never sees it.

The memory file stays the single source of truth. Nothing is duplicated — editing the paragraph edits the trap, because the annotation sits directly on top of it. Compilation is all-or-nothing: one malformed annotation aborts the whole build and keeps yesterday’s traps.jsonl, rather than silently shipping a table that protects less than it did the day before. And the engine fails open — a bad regex or a corrupt table logs an error and gets out of the way; it never blocks a tool call by accident.

Why this isn’t just a security guardrail

While researching whether this had already been done, we found plenty of prior art for the mechanism — several open-source PreToolUse-hook guardrails for Claude Code already regex-match tool calls and block or flag dangerous shapes (prompt-injection scanners, destructive-command blockers). That half of the idea is well established and we make no claim to it.

What we didn’t find anywhere is the other half: using that same delivery channel for compiled prose, not a fixed security ruleset. A guardrail encodes a rule someone wrote as a rule. A trap encodes a lesson someone wrote as a memory — often with its own history, its own “why,” its own link to a fuller write-up — and the annotation is just metadata bolted onto something that would exist anyway, for anyone reading the memory store directly. The trap and the guardrail sit right next to each other in our system for exactly this reason: traps inform, at the moment they’re relevant; guards refuse, mechanically, for the handful of things that are checkable with certainty. Different jobs, same delivery mechanism.

Prior art, looked at again (2026-09-27)

A second, wider search turned up several things that overlap part of this. None of them does the whole thing, as far as we could find:

So the combination still looks unclaimed: prose from an existing memory store, compiled into a deterministic table, fired on the exact shell command or the text being written, with a baseline that notices a trap going missing. “We didn’t find it” is as far as we’ll go, not “first.”

What it’s bought us in practice

Update, 2026-09-27: five weeks in

The engine has run on every tool call in every session since 5 August. The engine’s own log counts 256 sessions and about 14,000 trap deliveries, from a table that has grown to 108 traps compiled out of 16 memory stores (the host-local cache, the shared workspace store, and one per project repo that has one). All 108 are still warn. None blocks anything.

Several things were added once the first version met real use:

What we got wrong: noise

The design assumed a trap fires rarely. Some don’t. The noisiest trap, a warning that grep -E 'a\|b' searches for a literal pipe, accounts for about 5,500 of the 14,000 deliveries, close to 40%. Its pattern matches any grep -E, including correct ones, when the mistake it warns about needs -E and an escaped \|. The next-noisiest, the cd dir && … warning, adds another 2,600. A warning that fires on correct commands teaches the reader to skim past warnings, which is the failure the engine was built to avoid.

Content traps have their own version of this. Writing this very update fired three of them, for wasNull(, setAutoCommit(true) and CAST, because the words appear in the prose above. A content match can’t tell code from text about code.

The lesson: a trap’s pattern has to match the mistake, not just the tool the mistake happens in. The engine already logs which trap fired and when, so the log is the review signal: sort by fire count, and the top of the list is where tightening pays. That review is the next piece of work.

If you’re building anything long-running on top of an agent harness and you’ve hit the same wall — memory that’s either always loaded or effectively gone — this is the shape we landed on. Happy to talk through the compiler or the hook mechanics in more detail if it’s useful to anyone building the same thing.

Questions, corrections, or you’ve built something similar? Email me at ghdev10000@gmail.com.