[FEATURE] Triggered injection for auto-memory topic files (rules-style frontmatter)
Preflight Checklist
- [x] I have searched existing requests and this feature hasn't been requested yet
- [x] This is a single feature request (not multiple features)
Problem Statement
Related: #34556 (memory that survives compaction — this proposes the concrete mechanism), #77261 (subagent scoping for MEMORY.md injection), #16853 (rules-loader reliability; addressed below).
Claude Code ships both halves of a triggered memory system and doesn't connect them. Auto-memory topic files are retrieved only on the model's initiative — the docs say Claude "reads them on demand using its standard file tools." .claude/rules/ has a per-file trigger loader (paths: frontmatter), but today it serves only static, human-authored instructions — and #16853 tracks cases where it doesn't fire, so reusing it here means one loader to harden for two surfaces, with the audit logging proposed below as the diagnostic those reports currently lack. Topic files — where the docs tell Claude to move the detail — have no delivery event at all; even the MEMORY.md index arrives only at session start and doesn't survive compaction (#34556).
Model initiative measurably doesn't cover the gap. In our controlled runs (artifacts below), an agent whose store was pre-seeded with four task-relevant notes (a memory store exposed as MCP tools, usage guidance in context — the same initiative-dependent retrieval model as native topic files, with more affordance, not less) made zero memory reads in 114 turns. In a forced-compaction probe, ten facts stated once in conversation were absent from the first continuation summary and from 106 of 108 summaries (k=108); the same ten facts, injected at each compact-resume, arrived intact at k=138. Project-root CLAUDE.md is re-read after /compact and rules have path triggers; memory has neither.
Proposed Solution
Let auto-memory topic files opt into triggered injection via rules-style YAML frontmatter:
---
paths:
- "src/api/**/*.ts"
events:
- post-compaction
---
# Gotcha: API handler registration
Registration order matters in src/api/handlers/index.ts — the catch-all
route must stay last; adding routes after it silently 404s.
paths:— reuses the rules loader's existing glob semantics and load point: inject when Claude reads a matching file.events:—session-startandpost-compaction. Post-compaction rides the moment the harness already re-reads and re-injects project-root CLAUDE.md; memory files with this key are re-injected there too.- Default off, settings-gated (e.g.
"memory": {"triggeredInjection": true}). Off = exactly today's behavior: the harness already strips frontmatter from the loaded index (v2.1.211) and never loads topic files itself, so no frontmatter written today is live — and with the setting off, that stays exactly true. The setting is the kill switch; removing a file's frontmatter is the per-file one. - Injected memory is labeled as fallible recall, not instruction ("recorded by a previous session on
<date>— verify against current code"), and counts against the same memory budget the MEMORY.md index gets today. Dedup is file-level: a topic file that fired isn't delivered again that session, and its index line is one line, so the budget math stays flat.InstructionsLoadedrecords which memory fired and why. - v1 scope guards: main session only (subagents unchanged); fires wherever auto-memory already resolves, so worktrees/remote inherit existing behavior; no new tools, commands, or APIs — this reuses the rules loader's glob semantics, the frontmatter handling that already exists for the index, and existing injection surfaces. The new work is pointing trigger evaluation at the memory directory and adding the
events:dimension. - Lands in stages if preferred: phase 1
paths:only (pure rules-loader reuse), phase 2events:.
Alternative Solutions
- Status quo (model-initiative retrieval): measurably doesn't happen — see the numbers above — and it is weakest exactly where memory matters most: after compaction, the MEMORY.md index that would prompt a retrieval is itself gone (#34556).
- Put everything in CLAUDE.md: unconditional injection; the docs themselves push the other way (size caps, path-scoped rules,
/doctortrims). Triggers are the anti-bloat device — content loads only when its condition holds. - User-space hooks (what we run today): built with SessionStart/UserPromptSubmit/PreToolUse hooks and a local store — every number above comes from it. But a memory tier's fixed per-cycle overhead (tool schemas, guidance, per-prompt injections) tripped the CLI's own abort on our first seeded-arm launch: "Autocompact is thrashing: the context refilled to the limit within 3 turns of the previous compact, 3 times in a row… Try reading in smaller chunks, or use /clear to start fresh." We could only run by re-tuning read caps (run log in the archive). First-class support is what makes Claude Code's own safety machinery compatible with a memory tier.
Priority
High - Significant impact on productivity
Feature Category
Other
Use Case Example
- Tuesday: Claude spends 20 minutes discovering that
payments.spec.tsis flaky unlessTZ=UTCis set, and writes it to a memory topic file withpaths: ["**/payments.spec.ts"]. - Thursday, new session: I ask for a refactor that touches payments. The moment Claude reads
payments.spec.ts, the harness injects the gotcha — labeled as recalled memory with its date. No re-discovery, no test-failure detour, no reliance on Claude thinking to check its memory directory. - Mid-task, the context compacts. My
events: post-compactionnotes (current deploy constraints) are re-injected — today, that boundary is where this knowledge silently dies.
Additional Context
- Run archives, graders, per-run posture assertions, daemon audit-log excerpts, and the forced-compaction decay probe (protocol + both gated runs): https://github.com/swapnanil/vectr/tree/main/research/proactive-gate
- Full write-up (protocol, all runs, related work): https://arxiv.org/abs/2607.20972
- Security posture: this adds no new content source and no new author — files Claude already writes and already loads are delivered at additional moments. The trust boundary is unchanged: workspace files are trusted by workspace trust, not authorship — exactly as with CLAUDE.md and rules today. The delta is persistence and delivery timing (a poisoned note re-arriving every session); that is why v1 is default-off, per-file removable, labeled as fallible recall rather than instruction, and audited via
InstructionsLoaded. - Measured safety record from our hook runs: zero false-alarm injections across 40 and 35 audit-logged trigger evaluations; the file-anchored gotcha fired on first touch of its anchored file in both runs.
- Natural later extension, deliberately out of scope here: richer trigger vocabulary (symbol/semantic/temporal). Happy to contribute the re-exploration analyzer as a regression metric.
5 Comments
The initiative gap in these runs is compelling. I agree that path/event triggers are a better delivery primitive than hoping the model remembers to query its own memory.
One design boundary I would add: a trigger should decide when a memory is considered, not automatically grant that memory instruction authority. A path match can be true while the remembered fact is already stale.
For a first-party implementation, I would want each injected item to carry a small recall envelope:
path,post-compaction, or both)verify,revise, andforgetactionsIf the anchored code changed after the memory was written, the harness could still deliver the note, but mark it
stale — re-check against current coderather than silently treating it as active guidance. That distinction matters for examples like route ordering: the exact gotcha may disappear while the glob continues matching forever.I would also keep post-compaction delivery bounded to an active recall digest, rather than replaying every topic file whose event matches. The full records can remain addressable, while the digest preserves the few facts that actually influenced the pre-compaction task. Logging both the trigger evaluation and the selected/omitted items would make the budget behavior debuggable.
Maintainer disclosure: I build GoodMemory, an external local-first memory layer for Codex and Claude Code. Its current v0.7 implementation uses scoped recall, provenance/recall traces, bounded candidates, and explicit revise/forget controls. It does not replace the first-party loader proposed here, but it is a useful test bed for the same separation: trigger timing, retrieval selection, and write authority are three different decisions.
I would be interested in using your forced-compaction protocol as an interoperability test: verify not just that the ten facts arrive, but that a deliberately superseded eleventh fact does not retain authority after its source changes.
Agreed on the core boundary: a trigger should gate consideration, not confer authority. That separation is load-bearing in this proposal, and it is why delivered items are framed as fallible recall ("recorded by an AI session, not human-endorsed, verify") rather than instruction. Delivery timing, retrieval selection, and write/instruction authority are, as you say, three different decisions.
Your recall envelope maps closely to what our test bed already attaches per injected item, and the paper measures most of it live: source note id and age, scope, the trigger reason for the fire, and a freshness signal from a content hash of the anchored file taken at write time. When the anchored file has changed, delivery still happens but carries an explicit staleness warning. Section 5.6 of the write-up records exactly your route-ordering concern in the wild: the agent had itself edited the anchored file mid-run, post-compaction recall returned the note with a live staleness flag (a frozen summary cannot self-invalidate), and the agent then superseded the obsolete note. Explicit verify/revise/forget controls exist as promote, supersede/contradict (which flips the note to a deterrent "previously believed, since revoked" rendering instead of silent deletion, so the mistake is not re-derived), and forget.
Also agreed on bounding post-compaction delivery. Our default is a compact digest tier (index lines, expand on demand) with a per-session fire ledger and injection budgets, and every evaluation, fire, and suppression lands in an audit log, which is what made the false-alarm and delivery counts in the write-up checkable at all. One measured caveat worth passing on for a first-party design: a fact must ride the digest line itself or it does not travel across the boundary; title-only digests silently under-deliver (section 5.7's preflight lesson).
The interoperability test you propose is a genuinely good extension. The full protocol is published for exactly this kind of reuse under research/proactive-gate/ in the repo (decay-protocol.md, decay-probe.sh, seeds, graders, per-run transcripts), and the paper is at https://arxiv.org/abs/2607.20972. A deliberately superseded eleventh fact that must lose authority once its source changes is a sharper endpoint than pure survival, and it would exercise the revoke path under compaction pressure, which our runs did not isolate. If you run GoodMemory against it, I would be glad to compare traces.
Status update, since two adjacent reports have landed since this was filed and they change the picture.
#87804 asks for the same mechanism on a different surface: prompt-topic triggers for
.claude/rules/, where this issue asks for them on auto-memory files. Both reduce to the gap that issue states cleanly: skills get discovery-by-description, rules get discovery-by-path, and nothing gets discovery-by-subject. One trigger vocabulary serving both surfaces seems better than two that drift apart.#88945 is a bug that undercuts the premise here, and I would treat it as a blocker rather than a neighbour.
paths:globs are matched project-relative, so the auto-memory directory cannot be addressed by a path-scoped rule at all, at any scope, with no diagnostic. The frontmatter mechanism this issue proposes to extend currently cannot reach the files it would govern.#87783 asks for the other half of the same problem: what a note is bound to, so that a drifted note and a never-verified note stop looking identical once loaded. Triggering decides when a note is considered; binding decides whether it should be believed once it arrives. They compose, and neither is sufficient alone.
@hjqcan, on the interoperability test you proposed (the deliberately superseded eleventh fact that must lose authority once its source changes): it is queued on my side but not yet run, so I have no result to report. Flagging that plainly rather than leaving it implied. It is worth running precisely because it is the case my own evaluation did not isolate: delivery under compaction pressure and revocation were measured separately, never together, and the interaction is where I would expect authority to leak.
Thanks for the explicit status — no need to infer a result before the run.
For that eleventh-fact case, the useful evidence is two separate observations: (1) whether the old note is still retrieved/delivered after compaction, and (2) whether it is allowed to influence the answer after its anchored source changes. A safe result can still retrieve it, provided the trace marks it stale/suppressed and the current source wins. Recording the trigger reason, source fingerprint before/after, and final authority decision should make the interaction unambiguous.
If a small synthetic fixture would help when you get to it, I can provide one; otherwise I’ll wait for your measured result rather than adding more speculation to the thread.
Taking the decomposition, and it is a correction rather than a refinement. The endpoint I described conflates the two things your split separates. "Loses authority" is not one observation. A system that retrieves the superseded fact, marks it stale, and lets the current source win has behaved correctly, and scoring that as a failure would fail exactly the design this issue argues for. Retrieval and authority are separate outcomes, so the test has to score them separately.
That separation is load-bearing in the implementation, not only in the write-up. Delivery is never suppressed on a staleness signal; a caveat is attached and the note still arrives. Two such caveats now compose rather than merge:
The first is drift: the anchored file changed under a note whose author had read it, and the remedy is re-derivation. The second landed this week (swapnanil/vectr@3bd5cf7) and is non-observation: the note declared an anchor to a file its author never opened, so the stored hash proves the file's state at write time and nothing about the writer. Its remedy is a first read, not a re-derivation. The third line is both at once. Folding them into a single "stale" flag would erase the fact that they carry different remedies, which is the same collapse your two observations avoid one level up.
That work also forced a third value. The observation verdict is true, false, or unknown, because an absent read-ledger is not evidence of non-observation, and a false "never verified" is worse than silence. If your fixture exercises a system with no read tracking at all, the correct output is the unknown branch, not a failure.
On the trace fields you list: trigger reason is recorded per fire, and the injection audit line already carries a per-item lifecycle state positionally aligned with the item ids, so it is possible to tell which delivered items went in as assertions and which as revoked-note deterrents. The write-time fingerprint is stored on the anchor. The gap for your test is the before/after pair across the boundary specifically, which exists today only as a derived verdict rather than as two recorded values. That is a harness change, not a product one.
Yes to the fixture, and I would rather have it from you than write it myself. A test authored by the party whose system it measures is worth less, and I would be choosing seed facts knowing which paths my own implementation takes. Send it whenever convenient and I will run it as given, including the parts I would not have thought to include. Nothing promised on timing beyond what I said above: it is queued, and I will post the trace whether or not it comes out well.