2026-09-01 · Architecture
We built an ops layer out of text files
Open any project where someone is running agents seriously. You'll find a folder: AGENTS.md, CLAUDE.md, .mcp.json, a skills/ directory with a dozen files, a todo.md the agent rewrites after every step, and a memory/ folder with dates in the filenames.
That's not documentation. Documentation describes a system. Those files are the system. They decide who does what, who can see what, what counts as done, and what the agent remembers about yesterday.
Nobody calls this anything, so nobody recognises it as their problem. Here's a name: markdown-ops. Running a system on files that nothing validates, nothing constrains, and everything re-reads from the top every session.
First, credit where it's due
There's already a shelf of posts titled some variation of "Markdown is not agent memory." Almost all of them were written by companies selling memory. They get exactly the reply they earned — a counter-genre of "markdown memory is all you need." I'm not interested in being the fourth voice in that argument, because the format won on merit.
Markdown diffs. You can see in a pull request what changed about your agent's behaviour — try getting that from a row in a database. It's readable: something breaks, you open the file, you fix it in five seconds. It rolls back with one command. It requires nothing — no service, no schema, no migration. And it's portable: one AGENTS.md is understood by Codex, Cursor, Copilot, Gemini CLI and opencode.
No memory service gives you all of that at once. So when the "files are fine" camp snaps back at the vendor posts, they're right to.
The format isn't the problem. The problem is the job we quietly gave it.
The line is here
The argument isn't Markdown versus a database. It's about what the unit of change is — the file, or the fact.
A file holds intent. State is a fact. Markdown is excellent at the first and falls apart on the second.
"Always run the tests before committing" is intent. It changes rarely, it belongs in git, it belongs in a file. Markdown is unbeatable here.
"Task 14 is with the second agent, waiting on a human, and a third agent is already building on it" is a fact. It changes every few minutes, several participants write it at once, and you need to ask about it, not re-read everything.
While there's one writer, the distinction doesn't matter — the file is the memory. That's exactly why CLAUDE.md took off: it was the right technology for a moment when you had one agent. Then the thing that breaks every file-based store happened. The number of writers went to N. Not "lots of data" — there's barely any data. Lots of writers.
Seven failures, each one a scar
These aren't nitpicks about a text format. This is the list every system that outlives its own process arrives at eventually. Every example below is ours; we paid for all of them.
1. No transaction. Two agents in one working tree share one git index and one HEAD. We hit this as a real incident: git add staged nothing, because a neighbour moved HEAD mid-command. We fixed it with worktree-per-task and a mutex on the index — which is to say, we hand-built transaction isolation that a database hands you for free. UPDATE ... WHERE id = ? doesn't collide with an update to a different row. A Markdown file is one blob: two changes to two different facts are physically two changes to one object. The conflict isn't because the agents are careless. It's because the storage granularity is coarser than the fact granularity.
2. No query, so there's a context tax. Numbers from our own setup, measured this morning:
CLAUDE.md |
13,466 bytes / 214 lines (~4k tokens), read in full by every agent every session |
MEMORY.md |
17,957 bytes (~5k tokens), loaded in full at startup |
| memory files on disk | 94 |
The part that matters: MEMORY.md is a table of contents, not the memory. Eighteen kilobytes just to list which facts exist. None of the actual facts are in context yet.
A file has no WHERE. You can't read "only the deploy stuff" — you can read the file. So the cost of memory grows linearly with everything you have ever learned, while the context window is a constant. That isn't "suboptimal," it's a guaranteed collision: file-based memory eventually eats the window it was supposed to help with. A query returns 400 tokens instead of 18,000, and memory stops being a tax.
3. A paragraph has no address. A row has a stable identifier that survives renames, moves, and rewrites of the text around it; you can point at it from a chat message, a ticket, a line of code. "The Deploy Commands section of CLAUDE.md" is not an address. It breaks when you edit the heading, it can't travel over an API, it can't hold a foreign key. [[wiki-links]] in a memory folder imitate addresses without the guarantee — ours literally documents that a link to a file which doesn't exist yet is fine. In a database that's called missing referential integrity, and it's a defect, not a convention.
4. No schema, so the field lies. From our own operating instructions:
Don't trust the Status field in an ADR: "done" in the doc ≠ done in the code. Verify status against the code.
We wrote a rule that obliges us to distrust our own documentation, because we know it drifts. ## Status: Accepted isn't a field. It's text shaped like a field. Nothing requires it to be updated and nothing notices when it goes stale. Markdown gives you the appearance of structure without any of its guarantees — the worst combination available: looks like data, behaves like prose.
5. No trust model. A file in a repository has exactly one access level: whoever cloned it read all of it. There's no "this agent sees finance, that one doesn't." And agents treat the contents of these files as instructions by default — that's the entire point of the format. Which means your control plane is a set of unsigned text files that anything with write access to the repo can append to, including a build step. Nobody has to take my word for the risk: there are now scanners whose whole job is to sweep AGENTS.md, SKILL.md, .instructions.md and .mcp.json for hidden instructions before that content is trusted by another agent. You don't build a scanner for a file format. You build one for a control plane that has no authentication.
6. No events, so nobody finds out. git log answers "how did this file change." It can't answer "when did this task change owner," because it doesn't know what a task or an owner is. A diff is a delta of text, not a delta of meaning. And you can't subscribe to a file change from inside the system. A row mutation is an event: a notification, a webhook, a recompute, a message in chat. Memory that can wake a process is a different kind of thing from memory you have to remember to check.
7. File memory forks along with the filesystem. The measurement I like least, and it's ours:
.../-srv-godcrm-live/memory/ MEMORY.md 17,957 bytes 73 files
.../-root-production-business-crm/memory/ MEMORY.md 4,138 bytes 21 files
One project. Two memory indexes. A 4× divergence. 94 files, two silos, no sync. The cause is dumb: the silo key is the working directory path. An agent started from a different directory of the same repository lands in a different memory and cannot see what its neighbour already knows.
File storage inherits the topology of the filesystem — every copy, symlink, rsync and deploy artifact that comes with it. Our two code trees drifted the same way, and for six months we were building the wrong one. A database doesn't inherit that: one primary copy, and a replication strategy you choose on purpose instead of getting as a side effect of cd.
Notice that none of the seven is "Markdown stores meaning poorly." All seven are about operations.
Where Markdown is still right
I'm not proposing you migrate everything. For one agent and one human, files are close to unbeatable: zero infrastructure, everything visible, one-command rollback. I'm proposing you draw a line and stop crossing it.
Configuration stays in files. State moves to a substrate.
In Unix terms: AGENTS.md is /etc, not /var.
| Stays in Markdown | Moves out |
|---|---|
| Rules everyone reads, always | Facts about one task |
| Changed by humans, rarely, deliberately | Changed by agents, often, concurrently |
| Versioned with the code | Survives /clear and a change of agent |
| Meaningful in a diff | Meaningful in a query |
| One author per change | N writers at once |
It breaks at three thresholds, and crossing any one is enough: a second agent writing to the same place; a second human who needs to see status but not everything; facts that change rather than rules that sit still for months.
Leave CLAUDE.md alone — it's doing its job. The complaint is different: state leaked into files. Hundreds of snapshots, 94 memory files, dozens of agent prompts. That isn't configuration any more. That's operational data living in a format that was never meant for it.
A five-question test
Not "do you need a database." Do you have an answer:
- What happens if two agents write to the same file in the same second?
- How do you let an outsider see status without handing over keys and conversations?
- How do you ask "what's been blocked for more than a day" without reading everything?
- Where did this line in memory come from, and who owns it?
- What survives
/clear, and why that and not the rest?
Four out of five answered with "well, I'm careful" means you're doing markdown-ops. That isn't a verdict by itself. The verdict is not knowing you're in it.
And the clearest sign you've crossed the line: you created a file that explains how to read the other files. Congratulations, you wrote a schema. Without a validator.
What belongs there instead
Not "put it in Postgres" — that pushes the problem down a layer and leaves you without an interface. Five properties, and the value is in having them together:
- A row is an addressable entity. A stable identifier that outlives rewrites of its contents.
- Columns are a contract. Types, value domains, required-ness — plus a free-form field where the schema hasn't settled yet.
- Query instead of read. A slice by condition, not the whole file. The cost of a lookup stops scaling with everything you've accumulated.
- A mutation is an event. Notify, react, recompute. Memory that's awake.
- One store, two surfaces. Humans see a board; agents see the same rows over an API. No sync between "the agent's memory" and "the system where people work," because it's one object.
Number five is the whole point. Markdown-ops asks the agent to keep its memory, the human to keep their tracker, and someone to reconcile them. Nobody is going to reconcile them.
What you call the thing — database, table engine, memory service — is the least interesting question. What matters is that it isn't a folder.
How to move without breaking things
You can't cut over in one step: tools, humans and outside agents all read those files.
- Markdown becomes a projection, not a source. The store is authoritative; the file is an export for human eyes and disaster recovery. A telling detail from ours: we had to explicitly forbid agents from searching inside the exports. The moment a copy stops being the source of truth, it becomes noise in your search results. That comes free with every file replica you keep.
- Dual-write during the transition, with a declared winner on conflict. "Both sources are equal" has never worked once.
- One access path. Agents don't read the memory file directly, even while the file still exists. As long as a second read path exists, the migration never finishes.
- A cutoff with a date on it. A rule without a shutoff date isn't a migration, it's a wish.
What it costs. Honestly
- Higher barrier to entry. A file opens in any editor; a substrate has to be running. Which is why one-command self-install isn't marketing, it's a precondition — without it, "move your state" has no first step.
grep -rstops being the universal tool. You need real search instead, and it had better be good, or people will go back to files and they'll be right to.- You've signed up for operations. Backups, migrations, uptime. Files don't go down. Databases do — so you want a file-based snapshot layer as insurance.
- You lose diff as a review surface. "What changed this week" has to be rebuilt on top of events.
- A file belongs to nobody; a substrate lives in a specific system. The only cure is an open format and a one-click export back to Markdown, and it has to exist on day one, not "eventually."
The verdict
Markdown-ops wasn't a mistake. It was the correct answer to configuring a single agent, and for that job it's still the best answer available.
The mistake is continuing to keep state in it after the writer count passed one. All seven failures are one failure seen from seven angles: storage granularity coarser than fact granularity, with more than one writer.
The industry's current fix is more files. Another format, another standard, another layer of instructions on top of the last one. That treats the symptom.
The real answer is duller: leave intent in files, give state a substrate. Rules go in git, where they're happy. Facts go where there are permissions, concurrent writes, queries and an audit trail.
We walked this path ourselves and paid in git-index races, forked trees, and memory half our agents couldn't see. As noted last week, this post isn't a file either — it's a row in a table in our own Postgres, rendered to markdown on read. Which is the shortest version of the whole argument: the thing you're reading has an address, a schema, and an audit trail, and the folder it used to live in had none of the three.