- Python 93.7%
- Shell 6.3%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| AI | ||
| .gitignore | ||
| README.md | ||
om-ai — durable project memory for coding agents
An Obsidian folder that gives AI coding agents memory of your projects: what is true now, what was decided, what was tried and rejected, and where the last conversation stopped. One store, shared by Claude Code, Codex, Gemini and Antigravity, that survives moving between machines and accounts.
No vector models and no background services. Markdown files, a small local SQLite search index, seven Python hook scripts and a few hundred lines of Python that already ships with your system.
The problem
Agents forget. Every session starts from zero: you re-explain the stack, the branch layout, the decision you made last week, the approach that already failed twice. Knowledge never accumulates, and the cost is paid in the same conversations over and over.
The usual answers do not hold. Transcripts are logs, not knowledge — 98% of a session file is tool output, and nobody reads a 44 MB record to find out where work stopped. Per-agent memory stores are invisible to other agents and stay on the machine that wrote them. Instructions committed into every repository scatter the same context across a dozen places and cover nothing that has no repository at all.
How it works, end to end
you open an agent in a project directory
└─ SessionStart hook fires
├─ resolves which project this is
├─ reads the repository state (branch, dirty, last commit)
└─ injects ~1500 tokens: state, fact descriptions, overdue list,
and the previous conversation's "what to continue with"
you work; when a decision is spoken
└─ UserPromptSubmit hook reminds you to record it now, not later
you write a note
├─ PreToolUse hook protects the vault and rejects a second memory store
└─ PostToolUse hook checks its frontmatter and links immediately
the context fills up
├─ PreCompact hook saves a verbatim digest of the conversation into the notes
└─ PostCompact saves the model's own summary where the agent provides one
after each response
└─ Stop refreshes the derived native-memory pointer
a Codex conversation ends without a note
└─ SessionEnd calls the same stop.py and files a fallback draft
Everything the agent learns lands in three kinds of note and nowhere else:
| Note | Holds | Changes |
|---|---|---|
Project.md |
the current state and standing facts of one project | edited as things change |
Facts/*.md |
one statement each: a decision, a trap, a procedure, a preference | edited or deleted when disproven |
Sessions/*.md |
what happened in one conversation, and what to do next | written once, never rewritten |
A project may have no directory on disk. A radio station, a contract, a line of research are projects too — the memory is organised by what you work on, not by what has a git remote.
Install
git clone <this repo> ~/om-ai
sh ~/om-ai/AI/install.sh
The installer offers to move the folder into your Obsidian vault, fills in your profile, creates your first project, and wires whichever agents are installed. Nothing happens silently: it announces each step and backs up any file before its first edit.
On every other machine, once the vault has synchronised:
sh "<vault>/AI/bootstrap.sh"
That writes a short pointer block into ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md,
~/.gemini/GEMINI.md and ~/.agents/AGENTS.md; registers seven Claude events in
~/.claude/settings.json and eight Codex events in ~/.codex/hooks.json; links commands
and subagents; and touches nothing else. It edits only its own blocks and hook groups, so
your own rules and foreign hooks survive. Re-running is safe; --check reports state
without changing anything.
Project directories are machine-local. A synced Project.md may keep portable hints such as
~/code/project, but different absolute paths belong in
~/.local/state/ai-protocol/project-paths.json, not in the shared note. Register a checkout:
python3 "<vault>/AI/hooks/project-paths.py" add <project-id> <workspace> <local-path>
The workspace is a stable logical name, for example main, vault, or repo. Thus one
project can have both ai-flow/vault and ai-flow/repo, with different absolute paths on
each machine. Omitting the name remains compatible and assigns main, then path-2, etc.
The state file is already separated by the machine's filesystem and the user's HOME; it
must not be synced and needs no machine id.
This local map is the complete mechanism and works when the vault arrived through Nextcloud,
Syncthing, a USB drive or a plain copy. Git is optional: for a checkout whose origin is
listed under the project's remotes:, discover finds the project without being told its id.
SessionStart also uses the same Git-remote fallback:
python3 "<vault>/AI/hooks/project-paths.py" discover /path/to/checkout
Existing session notes can be migrated without guessing. Check first, then write only the
entries whose old absolute cwd belongs to a known project root:
python3 "<vault>/AI/hooks/migrate-session-paths.py" --check
python3 "<vault>/AI/hooks/migrate-session-paths.py" --write
When an older note says only workspace: project and that project now has several named
roots, leave it unchanged unless the old root is known. Then migrate that project explicitly
with --assume-workspace project/name.
Before removing legacy absolute paths: from synced Project.md files, run
project-paths.py import-legacy on every machine. Bootstrap does that import locally but never
erases shared notes; removing them too early would break a machine that has not imported yet.
Codex reviews exact hook definitions before running them. After bootstrap reports new or
changed Codex hooks, open Codex TUI, run /hooks, review the eight commands from
~/.codex/hooks.json, and trust them. Then start one fresh Desktop task and ask what it
already knows about the project without telling it to read files. This trust step is a
Codex security boundary; bootstrap writes the definitions but does not approve them for you.
After installing a new agent, run bootstrap.sh again. It only touches agents whose
configuration directory already exists — it will not create ~/.gemini for a Gemini you do
not have, because an empty directory for an absent agent is litter. So a newly installed agent
is wired on the next run, and --check will show it as missing until then.
Agent updates do not undo the wiring. A pointer block lives in the agent's instruction
file, which no installer rewrites — that file is yours. The one file an application does write
is ~/.claude/settings.json (it stores the theme there), which is why the hooks are added by
merge rather than by overwrite, and why --check exists: if anything ever clobbers the block,
a second run restores it.
Requirements: python3, an Obsidian vault, and a way to sync it if you use more than one
machine. Bases views need Obsidian 1.9+. Git, Nextcloud, Syncthing, rclone are transport
choices — the architecture does not depend on which you pick.
Platforms. Linux and macOS. One thing degrades on macOS: the hooks identify the calling
agent by walking /proc, which does not exist there, so agent: in a note falls back to
unknown — everything else works. Windows is not supported: the scripts are POSIX sh and
the paths are Unix.
First hour
- Fill in
AI/00 - Protocol/About me.md. It is read at every session start; the section that pays off most is "how to work with me", with a reason next to each rule. - Register its directory on this machine with
project-paths.py add, or list a portable~/...hint inpaths:. Existing absolutepaths:remain compatible. - Open an agent there and ask what it knows about the project. If it answers from the note without being told to read anything, the injection works.
- Work normally. When something is decided, say so — the agent files it.
- Run
memory-healthafter a week. It will find what you left half-written.
Seven scripts, platform-specific events
| Hook | Event | What it does |
|---|---|---|
session-start.py |
SessionStart | resolves the project, injects state, fact descriptions, overdue facts, repository state, and how the last conversation ended |
pre-compact.py |
PreCompact | saves a verbatim digest of the conversation into Sessions/ — every message, no tool output, 11–24x smaller than the transcript |
post-compact.py |
PostCompact | saves the model's own compaction summary where that event supplies one; current Codex does not provide compact_summary, so it stays quiet there |
stop.py |
Stop; Codex SessionEnd | Stop only refreshes the derived pointer. On Codex SessionEnd it files a fallback draft if the conversation produced no session note |
nudge-record.py |
UserPromptSubmit | on a message that sounds like a decision, a breakage or a state change, one reminder to record it now. At most two per conversation |
validate-note.py |
PostToolUse | after every note write: required fields, allowed type and confidence, name matching the filename, broken wikilinks. Complains into the context, fixes nothing itself |
guard-vault.py |
PreToolUse | rejects content written into agents' native memory stores. Claude can also ask before touching the rest of the vault; Codex leaves that approval to its sandbox |
Claude Code and Codex use the same seven Python scripts, but not the same event count.
Claude registers seven events. Codex registers eight because both Stop and the true
end-of-conversation event SessionEnd call stop.py.
Hooks are registered in each agent's own configuration rather than shipped as a file in this
repository, because sessions run in your code directories, not in the vault. Claude Code
takes them from ~/.claude/settings.json; Codex from the user-level
~/.codex/hooks.json. bootstrap.sh generates and merges both registrations. It disables
the legacy ai-memory Codex plugin if found, because Codex loads every enabled source and
would otherwise run duplicate events.
Every hook appends one line to ~/.local/state/ai-protocol/hooks.log — which hook fired, for
which session, in which directory, and what it decided. Without that log the only way to tell
whether a hook ran is the absence of a file, which is guesswork rather than diagnosis.
Every hook exits zero and stays silent on error: broken memory must never break work.
Two stores: facts and memories
A fact lives in 01 - Projects/<id>/Facts/ and is a document you maintain: how this
project actually is, edited in place as it changes.
A memory lives in 06 - Memories/YYYY/MM/ and is one claim with a declared audience:
python3 hooks/remember.py "the claim" --description "the line search shows" \
[--project <id>] [--platform codex] [--why "..."] [--apply "..."] [--supersedes <name>]
scope comes from the flags — general, project or platform — and reach is enforced on
both ends: the tool refuses an unknown project, and the session-start injection only shows
entries matching this project and this agent. General ones always show.
A memory is never edited. A wrong one is superseded by a new entry; the old keeps its text
and gains superseded_by, the new one links back. What you believed before is evidence about
the decision that followed, and a silently rewritten memory cannot be argued with.
Why this exists next to facts: reach used to be implied by the folder, so a claim made from an unrelated repository had nowhere to go, and a claim that applies to one agent only had to be written into every project or none.
People and organisations
07 - People/ holds one note per person or organisation whose decisions the work depends on —
role, side, projects, and how to work with them in their own words. Without it, "the client
asked for it this way" is re-explained every session and the same argument is re-litigated.
The session-start injection names the people attached to the current project.
Only what bears on the work: how to work with them, not who they are.
The four tools
| Tool | What it does |
|---|---|
search.py |
ranked full-text search over every note, digest and raw transcript — normalized Russian/English terms, strict match plus partial fallback, curated-note priority, sqlite FTS5/BM25 |
health.py |
audits the whole vault: broken links, overdue facts, oversized notes, duplicate names, facts with no inbound links |
digest.py |
extracts the readable text of any past conversation into the notes |
bind.py |
declares which project this conversation belongs to |
remember.py |
records one atomic memory with declared reach, or supersedes an older one |
pointerize-agent-memory.py |
hollows out each agent's native memory to a single pointer file |
memory-index.py |
builds that pointer — derived from note frontmatter, byte-for-byte reproducible |
project-paths.py |
registers named machine-local workspaces and discovers projects by Git remote |
migrate-session-paths.py |
replaces unambiguous legacy session cwd values with portable workspace/workdir fields |
similar.py |
near-duplicate check: before a write, and across the whole vault |
mend.py |
the deep sweep: compares notes against the repositories, repairs what is derivable |
test.py |
170 tests against a temp vault, no dependencies, about fifteen seconds |
Why declaring the project matters
Hooks see the working directory. Only the agent sees the topic. Work that lives in no repository — or in a directory that has hosted several different projects — would otherwise leave no trace at all, because the directory says nothing. So the agent declares it:
python3 "<vault>/AI/hooks/bind.py" <session-id> <project>
The session id arrives in the injected context. Every hook then resolves in one order: declaration → machine-local path/portable hint → Git remote → history. When nothing resolves, the hooks say so with the exact command instead of failing silently.
Search, concretely
$ python3 hooks/search.py "гул кроссфейд буфер"
omfm-cross-buffer-386 [omfm/fact, project]
гул и залипание буфера при cross на станции 386: что проверено и что не подтвердилось
…из `next_track` убрали — »гул« остался. Гипотеза 2 — голодает »буфер« на текущем треке…
01 - Projects/omfm/Facts/omfm-cross-buffer-386.md:12 читать: sed -n '10,52p' "…"
Long notes are split into chunks, so a hit points at a line and comes with a ready command to
read that place. A hit inside a transcript prints the claude --resume command for that
conversation. Natural questions ignore common Russian and English service words and tolerate
common endings; when not every meaningful term exists, a partial-match fallback is ranked by
term coverage. Curated facts and project notes win ties over sessions and raw transcripts, and
one long file occupies only one result slot. Filters: --type decision, --project name,
--limit n.
The index lives outside the vault, in ~/.local/state/ai-protocol/search.sqlite. It is
derived: machine-local, never synchronised, rebuilt from the notes on every call, only
re-reading changed files.
Secrets never enter the vault
The vault travels: another machine, a sync service, and this template is generated from one. So a note records where a secret lives, never what it is.
The digests are the trap, because a script writes them and a digest is verbatim: whatever was
on screen goes in as is. digest.py runs every message through a redactor first — known token
prefixes, KEY=value assignments whose value actually looks like a secret rather than a
reference to one, a password named in quotes next to the word "password", PEM blocks — and
prints in the note header how much it cut. health.py rescans every note, digests included,
and reports hits in its first block, so a value pasted by hand still surfaces.
This was not a hypothetical. The audit that added this pass found a live mail password in three notes and a shared production password in a fourth, both carried in verbatim from conversations months earlier. Redacting a value does not un-compromise it: the tooling cuts it out of the vault, and telling the owner to rotate it is your job.
The agent's own memory is hollowed out, not switched off
Every agent ships a private store: Claude Code writes ~/.claude/projects/<path>/memory,
Codex and Gemini have their own. Two stores drift, and then nobody knows which one is true —
that is not hypothetical, 240 KB of copies sat in ours for a month and had already gone stale.
The fix is not to disable auto-memory. One file stays in that directory, MEMORY.md, holding
nothing but pointers into the vault: protocol, project notes, memories, people, and the search
command. The loader keeps working and pulls the agent toward the vault instead of competing
with it.
hooks/pointerize-agent-memory.py performs the conversion and refuses to touch a directory
whose files are not already in the vault — it moves them to .imported/ rather than deleting,
then writes the pointer. guard-vault.py then returns deny for any write into an agent
memory directory other than the pointer itself, so this is a state, not a one-off cleanup.
The pointer is per project: Claude Code gives every working directory its own memory
directory, so each pointer lists that project's facts, its memories by declared reach, and its
people. memory-index.py --write writes them, bootstrap.sh does it on install, and the Stop
hook refreshes them at the end of every conversation — rewriting only what changed, since the
output is deterministic. A project added to the vault gets its pointer before its first
conversation: the directory does not exist until Claude has worked there, so the generator
creates it for every existing effective path: machine-local registrations plus compatible
paths: hints from the project note.
This mechanism is Claude-only, because only Claude keeps its memory as files. Codex keeps
its own in a sqlite pipeline with no external write path; Gemini has no such store. For them
the equivalent is the pointer block written into ~/.codex/AGENTS.md, ~/.gemini/GEMINI.md
and ~/.agents/AGENTS.md, plus the hooks, which inject the same project state at session
start. Those two directory paths are watched anyway, in case a future version grows one.
Upkeep: duplicates, drift, tests
Duplicates. Two notes about one thing drift apart and search returns both, so remember.py
checks before writing and refuses at high similarity — override with --force, or supersede the
older entry. similar.py --all lists suspicious pairs. The metric is a bag of crudely stemmed
words scored by overlap; it does not catch paraphrase, which is the honest limit of a method
without models.
Drift. health.py is the fast structural check. mend.py is the deep sweep: it walks into
each project's repositories and compares the notes against what is there — files named in facts
that exist in no branch and no history, branches the repository no longer has, a project note
untouched since the newest session, a repository running far ahead of the notes. With --fix it
repairs only what is derivable without judgement: a name that disagrees with its filename, a
missing asof, a superseded entry lacking its back-reference, a broken link with exactly one
close candidate, a stale derived pointer or index. Everything else is reported, because "this
fact names a file that is gone — is the fact stale, or did the file move?" has no mechanical
answer, and a confident wrong repair is worse than a gap.
Tests. python3 hooks/test.py — 170 tests, plain unittest, about fifteen seconds. They run against a
temporary vault (AI_VAULT / AI_STATE), so they neither touch the real one nor prove nothing.
Each one exists because something broke: a transcript format that silently produced nothing, a
redactor that matched a log line, a sweep that overwrote the machine's pointers from a temp vault.
The seven commands
| Command | What it does |
|---|---|
handoff |
hands the work to another agent, account or machine: finishes the session note, records what is uncommitted and what is still running, sets the exact next step |
dump |
you talk, it files — a stream of thought becomes facts, decisions, drafts and open questions, not a summary read back at you |
standup |
what is open across every project, oldest first, read-only |
memory-review |
walks the overdue facts and checks each on its merits — does that file, branch, port still exist |
memory-health |
runs the audit and repairs what needs no judgment |
transcript-digest |
saves the readable text of a past conversation into the notes |
project-archive |
archives a project, or merges two, without losing links or history |
Only Claude Code has a commands directory. On Codex and Gemini the same files are invoked by
name — say handoff, and the agent reads AI/commands/handoff.md and follows it. One
instruction, every agent.
The two subagents
memory-curator runs the review pass apart from the main conversation, because reading
dozens of facts would otherwise crowd out the work you are actually doing.
project-scanner reads an unfamiliar repository and drafts its project note, verifying
the README against the code rather than trusting it — stale READMEs are the usual case.
Both follow the same protocol. They are not a second set of rules.
Curation: the part that actually matters
Most "my agent has no memory" complaints are really "my agent has no curation."
A stale fact is worse than a missing one, because it looks like knowledge. So every fact
carries when it was last confirmed (asof) and how well (verified, inferred,
unverified), and a shelf life by type:
| Type | Shelf life |
|---|---|
project |
120 days |
reference |
180 days |
decision, workflow |
365 days |
feedback, user |
never stale |
You are not asked to remember any of this. The session-start banner shows the overdue count, and the oldest overdue facts appear in the injected context — so a rotten fact surfaces where it gets in the way, not on a schedule nobody keeps.
Two rules make this work rather than accumulate:
Write on the decision, not at the end. Sessions die on context limits and crashes, and the write "at the end" then never happens.
Fixing memory needs no permission. Changing code requires asking. Correcting a note that contradicts the repository does not — otherwise the wrong fact survives until someone remembers it.
Folders
AI/00 - Protocol/ the rules (Protocol.md), the reasoning (Reference.md),
your profile (About me.md), your agent rules
AI/01 - Projects/ one folder per project: Project.md + Facts/ + Sessions/
AI/02 - Bases/ live tables computed from note frontmatter
AI/03 - Knowledge/ material that is not tied to a project
AI/04 - Templates/ blank forms for the three note kinds
AI/05 - Drafts/ thinking out loud; promoted into a fact or deleted
AI/agents/ two subagents
AI/commands/ seven commands
AI/hooks/ seven hooks, four tools
AI/bootstrap.sh wiring, idempotent, --check
AI/install.sh interactive first-time setup
Surfaces
| Surface | Filesystem | Hooks | Memory |
|---|---|---|---|
| Claude Code (terminal and app) | yes | seven events via ~/.claude/settings.json |
full |
| Codex | yes | eight events via ~/.codex/hooks.json; review once with /hooks |
via ~/.codex/AGENTS.md as well |
| Gemini CLI, Antigravity | yes | own events, not wired yet | via ~/.gemini/GEMINI.md |
| Cowork | yes | not verified | via the Claude Code pointer |
| Chat in a browser | no | no | paste the note by hand |
Codex sandboxes writes to the working directory, so bootstrap.sh adds the memory folder to
writable_roots in ~/.codex/config.toml — one path, the rest of the sandbox untouched.
Design decisions, with the measurements behind them
Why not the agents' own memory stores. Each belongs to one agent and one machine. Work started in one agent has to continue in another, so the store must be shared and textual. The note format is borrowed from them wholesale — one fact per file, a description used for retrieval, wikilinks — because that part is good.
Why one note per session. It is append-only: created, extended, never rewritten. Two machines never write the same file, so file sync never produces conflict copies.
Why no semantic search. Measured in a sandbox against QMD, the engine the reference implementation uses, on the same corpus: the npm package pulls 850 MB before any model, the embedding model is another 318 MB, and the vector query loads that model on every invocation — it returned nothing within two minutes on a laptop. Its keyword mode answered in 0.19 s and found the same notes as this one at 0.15 s. On a meaning-based query both keyword engines return nothing, which is the honest limit of the class; fact descriptions written as search queries are what close it. The upgrade path stays open: point any engine at the same notes.
Why transcripts are indexed but never copied. Compaction does not truncate a transcript — it starts a new file and leaves the old one whole, so a copy protects against nothing. And a copy renamed without its session id cannot be resumed. Indexing the originals in place gives the search without the duplication; the digest gives the travelling copy without the bulk.
Why hooks are Python. The reference implementation runs TypeScript through Node's
--experimental-strip-types, and warns in its own README that a Node release renaming that
flag would break the hook configuration of all three agents. Measured: their hooks cost
0.1–0.3 s per call, these cost 0.04–0.09 s. The speed is irrelevant at one call per session;
the dependency is not.
Troubleshooting
Nothing gets written to the notes. Check ~/.local/state/ai-protocol/hooks.log. If it
says "проект не определён", the conversation is not bound to a project: declare it with
bind.py. If the log is empty, the hooks are not registered — run bootstrap.sh --check.
The injection does not appear. It is deliberately invisible in the transcript
(suppressOutput); what you see is the one-line banner. To verify, ask the agent what it
knows about the project without telling it to read anything.
Hooks stopped firing after the agent updated. Run bootstrap.sh --check. If an
application rewrote its settings file, a second run restores the registration. Never copy a
settings file over an existing one in an install script — merge, as bootstrap.sh does.
Codex ignores the hooks. Run bootstrap.sh --check, then open Codex TUI and run /hooks.
The eight definitions from ~/.codex/hooks.json must be present, enabled and trusted; new or
changed exact definitions are skipped until review. codex exec does run hooks, so it is a
valid isolated smoke path, but a fresh Desktop task is still the final check for the surface
people actually use. The legacy ai-memory plugin should be disabled to avoid duplicates.
A note has the wrong project. Move the file and fix the project: field, then
search.py --rebuild. Nothing else refers to note locations.
What is not included
No projects, no facts, no sessions, no personal rules — those belong to whoever wrote them. What you get is the mechanism, the format and the reasoning.
A note on language
The protocol and the reference are English; the hooks speak Russian in their user-facing
messages, because that is the language of their author. Nothing depends on it — the strings
are plain literals in AI/hooks/*.py. Your own notes go in whatever language you work in:
agents follow About me.md, not the protocol's English.
Where to start reading
AI/00 - Protocol/Protocol.md — the rules, about 280 lines.
AI/00 - Protocol/Reference.md — why it is built this way, and what was rejected.