TLDR: the best memory for a coding agent is its own past sessions, and the right interface to that memory is a filesystem, not an API.
Every coding agent I run — Claude Code, Codex, Copilot CLI, opencode
— writes a full trajectory of each session to disk: my instructions, its
tool calls, the outputs, the token counts. That data sits in some
~/.dotfolders, one bespoke format per agent. It is the
single richest record of how a project was actually built, and it is
write-only in practice: nothing ever reads it back.
Trajectory memory
The core concept I propose is trajectory memory: the agent’s own past sessions, normalized to a single format, and mounted as a read-only filesystem inside the project they belong to.
Four properties define it:
- Retrospective, not curated. A
CLAUDE.mdorAGENTS.mdfile is memory I write for the agent. Trajectory memory is the record of what actually happened — including the three approaches that failed before the fourth worked. Nobody has to maintain it: it accumulates as a byproduct of working. - Project-scoped. The mount inside a project exposes exactly the sessions whose working directory is that project. The agent’s memory of a repository lives in the repository.
- Format-normalized. One file shape for every agent, so a reader needs no per-vendor knowledge.
- Read-only and lazy. Nothing is materialized until something reads it. The 9 GB of trajectories on my laptop stays on disk; a file is translated when it is opened, and never written back.
How you use it
One command, in the project you are working on:
$ trajectoriz-cli memory
Mounting trajectory memory for /home/martin/workspace/prototypes/trajectoriz at
/home/martin/workspace/prototypes/trajectoriz/memory (daemonized).
Unmount with: trajectoriz-cli memory --unmount memory
From there the usage needs no new verbs. “Why is this function
written this way?” becomes a grep over past sessions. “What
did we try before this?” becomes reading the session from the week the
file was last touched. And because the mount is a directory, the agent
does this with the tools it already has — its own Read, Grep and Glob —
with no integration on my part.
The directory explains itself, which matters when the reader is an
agent that just found it: memory/README.md says what the
files are, what an ATIF payload contains, and how to locate a session
without grepping.
The idiom I use most is find, then read.
trajectoriz-cli search locates the session — matching first
messages, IDs and agent names, so it parses nothing — and the mount
serves it:
$ trajectoriz-cli search "tokens per trajectory" --local
## Search: `tokens per trajectory` — 1 result(s)
| ID | Agent | Date | First message |
|---|---|---|---|
| `cl-eb1c6529` | claude | 2026-06-10 | add support for computing the number of tokens per traje… |
$ ls memory | grep cl-eb1c6529
2026-06-10_claude_cl-eb1c6529.atif.json
$ jq -r '.steps[0].message' memory/2026-06-10_claude_cl-eb1c6529.atif.json
add support for computing the number of tokens per trajectory, inspired from code in @../agent-reports-codex/
$ jq '.final_metrics' memory/2026-06-10_claude_cl-eb1c6529.atif.json
{ "total_steps": 90, "total_tool_calls": 49, ... }
Search IDs and filenames carry the same identifier, so the two
compose without glue. Add --content to look inside every
step instead of only the first message: exhaustive, and slower, because
it parses each session it touches.
Why it works
The filesystem is the protocol. And it is the most
agent-native protocol there is: every coding agent ships with file
tools, so the API is already implemented on both sides. An MCP server
must be installed, configured, kept running, and — the part people
forget — its tool definitions must occupy the context window before the
agent can use them. A directory costs none of that: no server lifecycle,
no schema negotiation, no version handshake, no tokens. The agent does
not need to be told that a memory API exists; it needs a directory that
answers ls.
The cost is paid on read. An index would have to be built and refreshed; a service would have to be running. A mount is inert until something touches it, which is what makes it safe to leave mounted in every project.
Agents are already good at this. Grepping a directory and reading the interesting file is the single behaviour every coding agent has been trained hardest on. Trajectory memory needs no new agent capability — it reuses the strongest one.
The implementation
It is a FUSE
filesystem, pip install trajectoriz[fuse], in which every
past session of the current project is one JSON file — generated when
read, never stored:
$ ls memory | head -3
1780222476_opencode_oc-b83cd02d.atif.json
2026-05-31_claude_cl-261c8a5c.atif.json
2026-05-31_claude_cl-c2f9679f.atif.json
$ ls memory | wc -l
30
$ grep -rl "pytest" memory | wc -l
26
The filenames carry the date, the agent and the session ID, because those are the three things you sort and filter by. Everything else is inside.
Each file is the session translated to ATIF
v1.7, the Agent Trajectory Interchange Format: one envelope with
session_id, the agent and model, the ordered
steps (messages, tool calls, observations), and
final_metrics.
Why translate at all? Because every agent encodes the same three
concepts — a message, a tool call, its result — in a different
vocabulary. Claude Code stores a tool result inside the next user
message, keyed by tool_use_id. Codex emits a
function_call_output carrying a call_id.
Copilot CLI emits a tool.execution_complete event with a
toolCallId. Same event, three encodings, and that is before
the ones that keep sessions in SQLite instead of JSONL. Without
translation, one-file-per-session would only relocate the problem: the
reader would still need to know which of eight stores it is looking at.
With it, .steps[3].tool_calls[0].function_name means the
same thing everywhere, and a jq filter written today keeps
working when I install tomorrow’s agent.
See also
- trajectoriz: https://github.com/monperrus/trajectoriz — https://pypi.org/project/trajectoriz/
- ATIF, Agent Trajectory Interchange Format: https://harborframework.com/docs/agents/trajectory-format
- fusepy (ctypes bindings for libfuse): https://github.com/fusepy/fusepy