The problem
Every session with a coding agent starts from zero. It's sharp in the moment and remembers nothing from last week, because there's nowhere for a memory to actually live between conversations.
Fine for a one-off task. Not fine once it's a daily tool running across a dozen threads at once โ "start from zero" runs straight into "there's a decision from three weeks ago that still matters," constantly. Pasting more context into every prompt doesn't scale, and honestly it's backwards: a human re-deriving state a machine should've been tracking the whole time.
So I gave it a real memory instead. External, versioned, and its job to keep current โ a git repo of markdown files, a few read/write rules, and a pattern for how agent work touches it. None of it's exotic on its own. What's actually worth writing down is why it ended up shaped this way, which is mostly the scar tissue from watching the first, naive version of each piece fail on me.
01The core bet: a git repo, not a database
Two decisions sit upstream of everything else. Memory lives in plain markdown files, not database rows or vector embeddings. And the store is a normal git repo, synced the ordinary way โ commits, branches, pull requests.
Neither is clever. That's kind of the point. A markdown file is the one format both I and the model can read and edit without a translation layer in between โ no query language, no admin UI, nothing to export. Git already does versioning, diffing, blame, merge, multi-device sync. I'd have had to badly reinvent all of that from scratch otherwise.
RAG over a vector store is genuinely a good answer to "search a huge, mostly-static pile of docs." It's a bad answer to "keep a small, living record current," because now there's an indexing step and a similarity metric and a re-embedding pipeline sitting in a spot where git log and grep already do the job, for free, forever.
The result is unglamorous on purpose.
The store is a directory of markdown files that would still be useful if every model on earth vanished tomorrow.
I mean that as a literal test, not a nice line to end a section on. Every format choice, every sync mechanism, every convention below got run through it: would this still make sense to a person holding nothing but a text editor and git log? If not, it didn't ship โ didn't matter how much cleaner it would've made things on the agent's side.
02Two tiers of instruction
Not all context is the same kind of fact, so I split it before any of it reaches per-topic memory:
- A global instruction file, loaded in every project, that answers how should you behave โ tone, when to ask before acting, how to handle sensitive data, how to run sub-agents. Behavioral, not situational.
- A project instruction file, checked into each repo, that answers what's true about this codebase โ structure, conventions, the session start/end protocol. Anyone working in that repo gets the same brief, human or agent.
Where they overlap, the project file wins. Same reason a linter config in a subdirectory beats the one at the root. Neither file gets to hold the durable, dated, individual facts โ those belong one layer down.
# Behavioral Rules - No sycophancy. Say when you're wrong and move on. - Check a tool is still maintained before recommending it. - Sensitive data: compute locally, share only aggregates, never route secrets through a hosted tool. # Agent Orchestration - Spawn sub-agents for parallelizable work; coordinate results yourself. Don't do sequentially what can run in parallel. - Match model to task complexity (see ยง6).
03The part that writes itself
Instructions describe behavior. A separate mechanism handles facts โ decisions, corrections, standing preferences โ that accumulate over time and have no business being hardcoded into an instructions file. Each fact is its own markdown file with a small, consistent frontmatter schema:
--- name: tool-currency-check description: Verify a tool is still maintained before recommending it type: feedback tags: [tooling, recommendations, maintenance] --- Confirmed: recommended a tool that turned out to be in maintenance-only mode; an actively maintained successor existed under a different name. One search costs less than being corrected.
Recall is two-tier on purpose. An index file holds one line per fact, cheap enough to load in full every session, so most lookups never touch the filesystem beyond that one file. When the index misses, fall back to grepping frontmatter tags before deciding nothing's there. Cheap path first, exhaustive path second โ basically an index versus a table scan, just run over a folder of markdown instead of a database.
What's harder is what I keep out of it. If the repo or its git history already records something, it's not allowed in a memory file โ no code structure, no past fixes, none of that. Memory is for what you can't derive: a decision's reasoning, a stated preference, a correction someone had to make twice. A memory store that also mirrors the codebase is just a worse, staler copy of the codebase.
04Derive state, don't hand-maintain it
Every session opens with a generated brief โ active projects, what changed last time, anything urgent. A script builds it from frontmatter (status, priorities, deadlines) plus git commit history for freshness. Not a hand-typed "last updated" field.
That part matters more than it sounds like it should. A hand-maintained timestamp drifts the second someone forgets to touch it, and someone always forgets eventually. Git history doesn't drift. It's a mechanical record of when a file actually changed, so "how recently was this touched" stops being a chore and just becomes a query. Same logic one level up: the brief itself gets regenerated every session instead of living as a standing doc, so it can't go stale in a way that outlives its own point.
General rule I keep coming back to: anywhere the system has to answer "is this current," derive it from something mechanically hard to lie to โ git, filesystem timestamps, test results โ instead of a field someone has to remember to update.
05Multiple surfaces, one sync layer
The same repo gets written to from more than one place โ a primary workstation, an always-on home server, a phone client for quick edits on the go. All of it's git. No separate sync protocol bolted on top. What differs is the write path, matched to how much I trust that surface unsupervised.
Workstation and home server commit directly. A human's present, or the change is low-stakes enough to review after the fact. The phone client's different โ it drives edits through a chat interface, less friction, less scrutiny per edit โ so it opens a pull request instead of pushing straight to main. Same repo, same format, but the review gate shows up automatically where it's actually needed and stays out of the way everywhere else.
The other lesson came from concurrency, not trust. More than one session can be active against the same repo at once โ a branch changes underfoot, a commit lands mid-task from somewhere else, an in-progress stash just vanishes because another process touched the same files. No locking mechanism fixes that. It's a habit instead: check the real state (git status, current branch) right before any commit or push, not whatever was true when the session started. If something looks off, stop and say so instead of forcing it through. Git will tell you the truth. You just have to ask it right before you act, not once at the beginning and never again.
06Orchestrator and sub-agents
For anything with independent, parallelizable pieces: one agent plans and holds context โ the orchestrator โ while scoped sub-agents go do pieces of it and report back. Two rules make this actually pay for itself instead of just adding ceremony.
Match the model to the task
Not every step deserves the most capable model available. A quick validation pass runs on something fast and cheap. General implementation and research runs on a mid-tier default. A genuinely hard, ambiguous architecture problem escalates to the frontier model โ but only after saying why it's worth the cost, because that's a spending decision as much as a technical one. Cost-awareness is a routing input from the start, not something bolted on after the bill shows up.
Sub-agents return data. Only the orchestrator writes.
This is the one rule everything else leans on. A sub-agent reads, researches, reasons โ then hands its findings back as data, and only the orchestrator gets to commit anything to the memory store.
It's a security boundary as much as a workflow one. A sub-agent that ingests an untrusted web page or PDF can get manipulated by whatever's embedded in that page, so the line between "content I fetched" and "instructions I follow" has to be structural โ not just the model staying polite about it. External content gets wrapped in explicit delimiters before it ever reaches a model, referenced from instructions rather than pasted into them. Ordinary prompt-injection hygiene, just applied to something personal instead of customer-facing.
07War story: the silent revert
The most useful bug in this whole thing didn't even look like a memory-architecture problem. A "propose changes" flow โ the thing that turns an agent's edits into a reviewable pull request โ quietly started eating entire sessions of work. Files reverted to what they were before, nothing got committed anywhere, no error, nothing. For a while it genuinely looked like something else was interfering, because the pattern lined up too well: files that had recently merged "survived," files that hadn't "kept reverting." Real correlation. Exactly backwards as an explanation.
What was actually happening: a cleanup step that restores the working tree ran on every exit path out of the function, including the failure one. A pre-flight lint check sat upstream of the git operations and was failing on literally every proposal for a dumb environment reason โ a spawned linter resolving against the wrong runtime version. So every single time: make the edits, fail the lint check, revert the files, commit nothing, say nothing. Hell of a way to lose real work and never even get an error message about it. The files that "survived" had just gotten lucky and merged before this started; the ones that "kept reverting" hadn't, so the same revert function kept stomping them, over and over, looking for all the world like sabotage.
When something "never works," check whether it's switched off or quietly swallowing its own errors before you start re-diagnosing the implementation. A disabled feature and a bare catch {} look identical to a subtly wrong implementation from the outside โ and they're both way cheaper to rule out first.
The fix was two things, and only one of them was the actual lint bug. First: the destructive step now only runs after a local commit exists, so a failing check leaves the work sitting recoverable on a branch instead of just gone. Second: the test suite finally got coverage for the failure path โ branch created, content correct, nothing pushed, tree clean, the message actually naming the recovery branch. That path had zero tests before, which is exactly how a change that silently ate real work sailed through thirty other passing assertions without tripping a single one of them.
What actually stuck
Strip the specifics out and this is what survived actually living with it every day:
- โUse a format you can read without a viewer. Markdown and git beat a database any time the corpus is small enough for that to be a real option, which is more often than people assume.
- โDerive state, don't hand-maintain it. If a script can compute it from git history or the filesystem, it's not also allowed to be a field someone has to remember to update.
- โEvery write path gets a trust level, and the review gate follows it. Same repo, commit-direct here, PR-gated there โ matched to the actual risk, not one blanket rule.
- โSub-agents buy parallelism and cost control, not permission to skip oversight. Data flows up. Writes go through exactly one place, no exceptions.
- โWhen in doubt, assume it's failing silently. An "off" switch and a swallowed error both dress up as a subtler bug than they are, and it's cheap to rule them out first.
- โIt doesn't need to be a complete record to earn its keep. It just has to beat starting from zero, and it clears that bar easily.
The honest limits
It's not a life-log and I'm not pretending it is. There's always a gap between what the system knows and what actually happened โ nobody's narrating their day into a markdown file in real time, it only ever holds what got deliberately written down. I'd rather design around that gap than paper over it. A system that admits its blind spots straight out is more trustworthy than one that quietly acts like it sees everything.
Six months in, the interesting part isn't any one mechanism, it's that the whole thing is boring in exactly the right ways, and it still clears the bar from the top of this piece: still useful with every model gone. The store doesn't need a dashboard to stay honest โ a text editor and git log get you the whole way there โ which is precisely why building one on top turned out to be worth doing rather than a crutch. That's its own write-up. What carried this one is that the parts that break are debuggable with tools older than any of this: git log, grep, a test suite.