Agent memory: Markdown, Git, discipline

July 2026 · 13 minute read
No prior experience with coding agents is assumed beyond having tried one (Claude Code, Codex, Cursor and friends); Git basics help. The setup shown is one I actually run daily; examples are sanitized — structure and scripts are real, note contents are illustrative.

Coding agents are usually sharp inside a session and forgetful between them. Every new thread starts from zero — the pitfall hit last Tuesday, the reason a dependency got pinned, the deployment decision everyone agreed on — gone, unless someone repeats it again. The popular fix is refreshingly low-tech: write things down in Markdown files, and let the agent read them back.

The idea has real momentum behind it. AGENTS.md files have become a cross-tool convention, and Letta found that a plain agent with nothing but filesystem tools scored 74% on a standard memory benchmark — competitive with purpose-built memory systems. Hence the advice going around: skip the heavy infrastructure, just use simple structured text files.

That matches the experience here — this post describes exactly such a setup. But maintaining Markdown files seems to be only half the lesson. The other half tends to get skipped: memory written by agents can rot, and can leak and often has the tendency of accumulating junk and growing indefinitely. Notes an LLM writes at 2am pile up duplicates, stale facts, and unmarked guesses — and, occasionally, things that should never land in a Git repository. Files are enough; what keeps them useful is the discipline around them, and that’s what this post is about.

Instructions are not memory

A quick distinction that saves a lot of confusion. An AGENTS.md or CLAUDE.md tells the agent how to behave: build commands, style rules, guardrails. It’s written mostly by the human, changes rarely, and lives with the code. Memory is the other direction: what the agent (and its human) learned along the way — decisions taken, pitfalls hit, procedures that worked, important results/notes. It’s written mostly by the agent, grows constantly, and outlives any single project or tool. Much of the “memory files” content out there is really about instruction files; both are worth having, and they age differently.

The paths on offer

For durable agent memory today, there are roughly five ways people go about it:

PathCore ideaStrong atTends to leave out
Built-in memorythe tool stores notes for itself (e.g. per-project auto-memory)zero setupownership, portability across tools, review
Instruction filesAGENTS.md/CLAUDE.md rules in each repoconsistent behaviouraccumulated learning; it’s not memory
Obsidian vault on Gita note vault the agent updates as it workshuman browsing UX, diff reviewsearch mechanics, validation, secret hygiene
Memory platformsMem0 / Letta / Zep — dedicated memory databases with SDKsproduct-scale, multi-user recallsimplicity; a lot of machinery for one dev’s agents
Repo-first (this post)a plain Markdown + Git repo the user ownstrust over time, portability, reviewa pretty UI (a trade-off, made knowingly)

The Obsidian-vault pattern deserves a special mention because it’s the closest relative — and, credit where due, jxnl’s “codex-maxxing” post is where this setup started. Its best idea is one worth underlining: Git diffs are the review surface for memory. An agent that must “write down what changed” produces something a human can inspect, question, and revert. Where this setup ended up diverging is the Obsidian layer itself — more on that below, because it’s a preference, not a verdict.

The layout

The memory lives in a dedicated repository — not inside any project — organized by note type:

memories/
├── AGENTS.md          # rules for agents working in this repo
├── README.md, TODO.md, PROJECTS.md, CHANGELOG.md
├── projects/          # per-repo state, workflows, quirks
├── decisions/         # dated architecture/process decisions
├── runbooks/          # repeatable procedures
├── pitfalls/          # failure modes and their fixes
├── preferences/       # stable owner preferences
├── people/            # human context, only when useful
├── inbox/             # unprocessed notes awaiting review
├── templates/         # one template per note type
└── scripts/           # memory.py CLI + git hooks

The types aren’t decoration — they hint at how a note is allowed to change. A decision is dated: if it turns out wrong, a dated correction gets appended rather than the history edited. A pitfall is living: symptom → cause → fix, updated when the fix improves. A runbook is a checklist that has to stay executable. The inbox is where half-formed observations wait, so capturing something cheap never requires deciding where it belongs first.

Every typed note carries front matter — the small metadata block between --- lines at the top — and this is where the anti-rot machinery starts:

---
title: "Docker builds fail after VPN reconnect"
type: pitfall
status: active
confidence: medium
updated: "2026-07-14"
sources:
  - "repo: infra-stack, docker logs 2026-07-14"
---

Two fields do the heavy lifting. confidence makes every memory admit how sure it is — an agent recording a hunch marks it low, and a future session reading it knows not to treat it as gospel. sources asks for evidence: a repo path, a command output, a link. A memory without evidence is close to a rumour with good formatting.

Four rules that do the real work

The repo’s own AGENTS.md is short, and four of its rules carry most of the value:

  1. Search before work. Any session touching a project starts with memory.py search <terms> against this repo, before relying on whatever the tool’s built-in memory recalls. It’s cheap, and it keeps the memory load-bearing — notes that get read tend to get maintained.
  2. Evidence and confidence, always. Durable facts only, cited, with uncertainty marked explicitly. “Probably” is allowed; unlabeled “probably” is not.
  3. Corrections, not overwrites. A prior decision is never silently edited; a dated correction is appended instead, so the history of why the mind changed survives. Git-style thinking, applied to knowledge.
  4. Inbox first, consolidate weekly. Capture is allowed to be messy; the inbox absorbs it. A recurring weekly scheduled pass promotes worthwhile notes into typed directories, merges duplicates, and deletes noise.

That last rule has a nice parallel in current agent vocabulary: the inner loop vs outer loop framing (see this thread and Phil Schmid’s write-up). The inner loop is the agent verifying its work within a single task; the outer loop is whatever carries lessons across sessions — the part, as Schmid notes, almost no agent does natively today. A weekly consolidation pass is simply an outer loop run by hand, on a calendar. Unglamorous, but a better option exists: a scheduled weekly maintenance by an agent, which puts it ahead of most setups. The prompt using Codex scheduled tasks in this case is:

Maintain `~/projects/memories` weekly.

Read `AGENTS.md`, `README.md`, `PROJECTS.md`, and `TODO.md`. Run `python3 scripts/memory.py lint`;
fix safe structure issues such as front matter, broken internal links, PROJECTS index entries,
stale TODO wording, and note placement. Verify local path references and `sources:`
entries still exist where practical, including referenced repos under `~/projects`;
if a path is historical or vanished, add a concise dated correction instead of deleting useful memory.
Never write secrets or raw credentials; refer to secret locations by name only.
Run `python3 scripts/memory.py export-codex` after fixes; do not commit `dist/`.
Re-run lint, and run `python3 -m unittest discover -s tests` if scripts/tests changed.
Check `command -v gitleaks` before committing; if missing, report that the pre-commit hook will
block the commit and leave changes uncommitted rather than bypassing the hook. Review `git diff`
for sensitive data. If tracked files changed and gitleaks is available, commit with `docs(memory):
weekly maintenance week DD-DD Month YYYY`, using the Monday-Friday range for the run week, e.g.
`docs(memory): weekly maintenance week 22-26 June 2026`. If no changes, report no commit needed.
Output checks run, changed files, stale references fixed/flagged, commit hash if committed,
and follow-ups.

None of these rules need software — which is arguably the point. The discipline would survive a complete change of tooling.

A ~300-line script, not a platform

There is some software: a single Python file (standard library only) exposing search, lint, and new, plus an export helper. No dependencies, no index to maintain, no server. Search is deliberately simple — case-insensitive matching of all terms over the Markdown files, printing file:line, the nearest heading, and the matching line:

def cmd_search(args):
    terms = [t.lower() for t in args.terms]
    for path in iter_markdown(ROOT):
        text = path.read_text(encoding="utf-8")
        if not all(t in text.lower() for t in terms):
            continue
        for line_no, line in enumerate(text.splitlines(), start=1):
            if any(t in line.lower() for t in terms):
                heading = heading_at_line(text, line_no)
                print(f"{path.relative_to(ROOT)}:{line_no}: {heading}: {line.strip()}")
                break

Output looks like (contents adapted):

$ python3 scripts/memory.py search docker vpn
pitfalls/docker-vpn-reconnect.md:12: Symptom: - builds hang after VPN reconnect …

Is plain text matching a limitation? Yes — there’s no semantic search, and the limits section returns to it. But at personal scale — hundreds of notes, not millions — an agent that can read files iteratively does remarkably well with exactly this, which lines up with Letta’s filesystem benchmark above. The new command renders a typed template with title and date filled in, so notes start out valid instead of needing cleanup later.

Lint as a safety net

Code changes get automated checks before merging; memory written by agents deserves the same. memory.py lint fails the repo on:

  • typed notes missing front matter, or missing the title/type/status/confidence keys;
  • duplicate titles across notes (a classic agent move: writing a fresh note instead of updating the existing one);
  • broken links between notes;
  • lines matching secret-shaped patterns (api_key = …, BEGIN PRIVATE KEY, and friends — a cheap regex net, nothing clever);

and warns on unresolved TODO markers outside the actual todo file. It runs before any handoff and before commits, and it’s covered by tests — the current state:

$ python3 scripts/memory.py lint
lint ok (0 warnings)

$ python -m pytest tests/ -q
4 passed in 0.01s

A pleasant side effect observed in practice: when a duplicate-title error fails the commit, the agent tends to go update the existing note instead — which is exactly the consolidation behaviour the rules ask for anyway.

Fail-closed secret scanning

Here’s the risk the “just use files” posts rarely mention. An agent that can read environment variables, logs, and config files is an agent that can paste them — and a memory repo is the one place it’s explicitly encouraged to write down “useful context.” The lint regexes above are a tripwire, not a defence. The defence is gitleaks running in a pre-commit hook. There are several tools for this purpose: gitleaks, betterleaks, trufflehog, detect-secrets, etc. one can choose depending on preference and scope. The actual use case is a small script Git runs right before recording each commit:

#!/usr/bin/env sh
set -eu

if [ "${SKIP_GITLEAKS:-}" = "1" ]; then
  echo "SKIP_GITLEAKS=1 set; skipping gitleaks scan" >&2
  exit 0
fi

if ! command -v gitleaks >/dev/null 2>&1; then
  echo "ERROR: gitleaks required but not on PATH" >&2
  exit 1          # fail CLOSED: no scanner, no commit
fi

gitleaks protect --staged --redact --verbose

The detail that matters most to us: the hook fails closed. If gitleaks isn’t installed, the commit is refused — a missing scanner doesn’t quietly become a skipped scan. The escape hatch (SKIP_GITLEAKS=1) exists for the rare false positive, but it has to be typed on purpose after a manual look at the diff. Alongside the hook, the repo’s rules keep secrets out by reference: a note may name where a credential lives, never its value.

Why this setup skips Obsidian

A fair question, since the closest relatives of this pattern put an Obsidian vault at the centre — and Obsidian’s front-end is genuinely pleasant: graph views, backlinks, a polished way to browse a growing pile of notes. Leaving it out here was a deliberate choice, as a matter of personal preference:

  • The agent never sees the UI. Graph views and plugins are affordances for the human, and they gently pull the note format toward human-only features — [[wikilinks]], for instance, that standard Markdown tooling and simple search handle less well than plain relative links.
  • Obsidian’s workspace files (.obsidian/) change with every click and end up in the diffs — and a clean diff being the review surface is the best part of this pattern. It felt worth protecting.
  • A vault invites collecting; a memory repo works better as a terse, evidenced, prunable working set. Keeping the format plain helps keep the writing plain.

So the repo simply avoids Obsidian-specific files. Nothing is lost by this: it’s all standard Markdown, so the folder opens in Obsidian like any other vault — pointing Obsidian at it as a viewer is always an option for anyone who misses the front-end. The preference here is just to keep the UI behind the memory rather than in front of it.

Which path fits

Some pointers, by profile — with a caveat that only the first two are backed by daily use here; the rest are ideas that seem plausible, offered as starting points rather than recommendations:

  • One agent CLI, getting started → the tool’s built-in memory is fine; a dedicated repo starts paying off around the time the same context gets repeated across projects or tools.
  • Multiple tools (this is the tested case here: Claude Code and Codex sharing one repo) → repo-first works well: one memory, several agents, and no migration when tools change.
  • Small team → untested from here, but the shape is tempting: memory changes as pull requests, review as a shared editorial step, lint keeping entropy down as writers multiply. Maybe worth a try.
  • Obsidian at the centre of the workflow → possibly keep the vault for personal notes, keep agent memory in standard Markdown, and open the memory repo in Obsidian read-only when the UI is missed.
  • Building a product that needs memory for many end-users → likely better served by the Mem0/Letta/Zep class of infrastructure; that’s a different problem from a personal setup.

Limitations

  • Search is plain text matching. No embeddings, no semantic recall; the agent compensates by iterating (search, open, follow links), which works at personal scale but probably won’t at thousands of notes with many writers.
  • Secret scanning is pattern-based. gitleaks plus regex tripwires catch secret-shaped strings; a novel token format, or a sensitive fact phrased in prose, can slip through. The by-reference rule and the diff review are the real last line.
  • Discipline is load-bearing. Skip the weekly consolidation for a month and the inbox becomes a junk drawer (relative to daily use). The system degrades exactly as gracefully as the habits behind it.
  • Single-user by design. Concurrency, permissions, per-note access control — none of it exists. At this scale that’s simplicity; beyond it, a blocker.

Takeaways

  • For a developer’s coding agents, simple structured text files go a very long way — even on benchmarks, as Letta’s filesystem result suggests.
  • Instructions ≠ memory, and agent-written memory rots and leaks unless something controls the flow.
  • The control loop is cheap: typed notes with confidence + evidence, search-before-work, dated corrections, an inbox with a weekly consolidation pass, lint checks, and a fail-closed secret scan. All of it fits in a ~300-line script, a shell hook and a scheduled task.
  • The weekly pass doubles as an outer loop — the carry-lessons-across-sessions cycle most agents still lack natively.
  • Git diffs as the memory review surface is the keeper idea from the vault-style setups; here, the diffs stayed and the UI moved behind the memory.
  • Own the memory. Tools will change; a Markdown repo under one’s own control moves along.

Enjoy coding!

Above opinions and any mistakes are my own. I am not affiliated in any way with companies, or organizations mentioned above. The code samples provided are licensed under the Apache 2.0 License and rest content of this page is licensed under the Creative Commons Attribution 3.0 License, except if noted otherwise.