Skip to main content

fornix — Memory that moves with the code. $ uvx fornix -y

Fornix

Project memory for AI coding sessions. Lives in the repo, travels with the code.

Quick start · Four operations · Compare · Store & server docs · Skill protocol · PyPI

pypi tests license

Memory that branches, merges and gets reviewed with the code.
Shared by every model that opens the repo, consulted on every prompt. No hub, no account, no API key.
Read why →


22-second walkthrough: install with uvx, recall before answering, capture after acting, commit the memory with the code

Every AI session starts from zero. Yesterday's decision gets re-argued, last week's gotcha gets hit again, and the convention nobody wrote down gets broken by the next model to touch the code. Hosted memory tools fix this with a service outside the repo, so when the code moves, the memory does not.

fornix puts the memory in the repo. A teammate gets it by pulling, a branch carries its own decisions, a pull request reviews a decision next to the change that caused it. Any model, any MCP client.

📦 Store .fornix/memories/*.jsonl, one fact per line, committed. Git is the history.
🔌 Server An MCP server over that store. Four tools, a sqlite-vec index for semantic recall, no model inside.
🔁 Discipline A skill and a hook that make the session recall before it answers and capture after it acts, every prompt, until told to stop.
🌿 Branches with the code Check out a branch or an old tag and you get the memory that was true there, not whatever a central store believes today.
🤝 Any model, any client Claude writes a memory, Codex reads it, Gemini corrects it. Same store, same file.

Use fornix

🧑‍💻 I work in one repo

One command installs the store, the server, the skill and the hook into the repo. Restart Claude Code, turn sync on, done.

→ Jump to Quick start

🗂️ My editor is open on many repos

One store per repo, registered into the workspace folder above them. Each repo syncs on its own.

→ Jump to Many repos in one window

🔧 I use Codex, Gemini, Cursor, or want an API

Any MCP client gets the same four tools. There is also an HTTP service with a JSON API.

.fornix/setup.sh --codex --gemini

→ Jump to Other clients


Quick start

cd your-repo
uvx fornix -y

Restart Claude Code, approve the fornix server when asked, then:

/context-start-sync

That is the whole setup. Needs uv and git. Drop the -y to be asked about the target, the hook and the other clients instead of taking the defaults.

What landed in the repo

path what
.fornix/ the store and the MCP server, with an empty memories/main.jsonl
.mcp.json the fornix server entry, merged into whatever was there
.claude/skills/context-sync/ the skill that drives the per-prompt loop
.claude/commands/context-*.md /context-start-sync, /context-start-sync-readonly, /context-stop-sync
.claude/hooks/context-sync.sh + settings.json a UserPromptSubmit hook that re-arms the loop every prompt, so it survives compaction
.agent/ the same skill and commands under the vendor-neutral tree some agents read

Commit .fornix/memories/. Everything else under .fornix/ is gitignored. Re-run the command any time to update an install; memories/ is never touched.

How it works

Once sync is on, every prompt runs the same loop:

  1. Recall. Before answering, the model runs one select with a query built from the intent of your prompt and treats the hits as ground truth for this repo. If a stored decision contradicts what it was about to do, it says so instead of quietly overwriting it.
  2. Act. Your prompt gets answered as usual.
  3. Capture. After acting, it writes the durable facts it learned, one per call, correcting older memories in place rather than appending near-duplicates.

Every prompt ends with one quiet line:

context: 6 recalled · 1 written

A memory is one JSON line:

{"id": "9f3c1a7be2d4", "ts": "2026-09-09T07:20:05+00:00", "text": "Session tokens are refreshed by the gateway, never by services. Tried per-service refresh first; it raced under load.", "tags": ["auth", "decision"], "source": ""}

Durable, so it gets written: a decision and why, the option that lost, a hard constraint, a gotcha and its workaround, a convention the code does not reveal, a stated preference. Not durable, so it does not: transient state, file contents, anything re-derivable by reading the repo, general knowledge. Never: credentials, tokens, personal data. The store is committed and pushed.

Three commands

command does
/context-start-sync recall and capture on every prompt
/context-start-sync-readonly recall only, writes nothing, ever. For someone else's repo, or a branch whose decisions are not settled
/context-stop-sync final flush of anything durable, then off

The four operations

Any MCP client gets the same four tools. Scopes are named JSONL files; main is the shared default.

tool args does
select query, scope=main, k=8, tags Semantic search, best first, each with a score. Empty query lists the k newest.
write text, scope=main, tags, source, id Save one fact. Pass an existing id to replace that memory in place.
compress scope, ids, summary, threshold=0.92 ids + summary: fold those memories into one. Neither: merge near-duplicates, newest kept.
isolate scope, seed_from, query, k, tags Open a private scope, optionally seeded with the top k hits from another.

A sub-task or a sub-agent that will generate a lot of throwaway reasoning gets isolate("task-x", seed_from="main", query=...), works in that scope, then compress("main", ids, summary) folds the useful residue back.


Many repos in one window

One store per repo, but the editor is usually open on the folder above several. From that folder:

uvx fornix --set-root        # finds every repo with a .fornix/ up to 5 levels down, asks which join

or inside each repo, .fornix/setup.sh --set-root ... Either way the repo adds itself to that folder's .mcp.json as fornix-<repo> with an absolute path, and copies the skill, commands and hook into the folder's .claude/. Stores stay separate on purpose: one repo's decisions are not another's. Sync is per repo too, so one repo can be capturing while another is read-only and the rest are off.


Other clients

Claude Code · Codex · Gemini CLI · Antigravity · VS Code · Cursor · Windsurf · Cline · Zed · Claude Desktop · any MCP client

Claude Code reads .mcp.json and is done. Everything else is one command away, and setup.sh ships inside .fornix/ so teammates without this repo have it:

.fornix/setup.sh                      # asks: which clients, which workspace folder
.fornix/setup.sh --codex --gemini     # also --claude --agy --vscode
.fornix/setup.sh --print              # just list the commands

Manual configuration

Clients configured by file (Cursor, Windsurf, Cline, Zed, Claude Desktop) take this under mcpServers:

"fornix": {"command": "uv", "args": ["run", "--directory", "/abs/path/to/repo/.fornix", "python", "-m", "context_store.server", "mcp"]}

As a service

A JSON API with docs at /docs and MCP at /mcp:

uv run --directory .fornix python -m context_store.server serve     # http://127.0.0.1:8765
curl -s localhost:8765/select -d '{"query":"how is auth done"}' -H 'content-type: application/json'

Other ways to install

All of these are the same installer with the same flags. uvx fetches the release tag from GitHub, so it needs git; the rest run from a checkout.

uvx fornix /path/to/repo -y                 # a repo other than the current one
uvx fornix --no-hook                        # skill only, no hook
uvx fornix --codex --vscode                 # register those clients as you go
uvx --from git+https://github.com/ZGA2519/fornix fornix   # track main

git clone https://github.com/ZGA2519/fornix && cd fornix
./install.sh /path/to/repo        # POSIX sh, the original
python3 setup.py /path/to/repo    # the same wizard in Python, runs in cmd.exe too
uv run fornix --help      # everything the installer accepts

How it compares

Three other shapes exist. Each is better than this at something; none of them is memory that moves with the code.

AGENTS.md / CLAUDE.md hosted memory
(Mem0, Zep, Supermemory)
local memory server
(MemPalace, Basic Memory, ai-memory)
fornix
committed in the repo ✓ ✗ ✗ ✓
branches and merges with the code ✓ ✗ ✗ ✓
reviewed in a pull request ✓ ✗ ✗ ✓
a teammate gets it by pulling ✓ ✗ account ✗ hub or sync ✓
retrieved per prompt, not loaded whole ✗ ✓ ✓ ✓
recall the model cannot skip ✓ always in context ✗ tool call ~ skill or session hook ✓ per-prompt hook
no API key, no network ✓ ✗ ~ varies ✓
any model, any client ~ convention ~ ✓ ✓
nothing to run but the agent ✓ ✗ ✗ server ✓

Two rows are the ones that matter.

A teammate gets it by pulling. A hub gives everyone the same memory, but it gives everyone the latest memory. Check out a colleague's branch or a tag from three months ago and the hub still describes main. Here the memory is at the same commit as the code in front of you.

Recall the model cannot skip. Every memory server is a tool the model calls when it happens to think of it, which on a long session is less and less, right when memory matters most. AGENTS.md solves that by being permanently in context, and pays for it in tokens on every turn whether or not it is relevant. The hook here re-arms a fixed recall loop on every prompt and survives compaction, so recall is per-prompt and scoped to the prompt.


Why it is shaped like this

Memory is part of the repo. A branch has its own memory. A pull request reviews the memory along with the code. git blame on a decision works. Merge conflicts in a JSONL are trivial: keep both sides, ids are unique, run compress(). Checking out an old tag gives you the context that was true then, not whatever a central store believes today.

Sharing is git pull. No hub to deploy, no account, no sync daemon, no access control to maintain: if someone can clone the repo, they have the memory, offline, in the same commit as the code it describes. Revoking access is revoking repo access.

No model inside the server. Summaries in compress come from whichever model is calling. Claude writes a memory, Codex reads it, Gemini corrects it. Same store, same file.

The index is disposable. index.db is a sqlite-vec index rebuilt from the JSONL whenever they drift. Delete it freely. Embeddings come from BAAI/bge-small-en-v1.5 via fastembed, about 30 MB, downloaded on first run; CONTEXT_EMBED_MODEL picks another.

The skill solves when, the hook makes it stick. A memory server on its own gets called whenever the model happens to think of it, which on a long session means less and less. The skill turns it into a fixed per-turn loop. The hook re-injects that loop on every prompt for as long as .fornix/.sync-on exists, so compaction cannot erode it.

What it is not

  • Not a token reducer. It adds a few hundred tokens a turn. It saves work, not context window. Pair it with something that curates which files the session sees if that is the problem you have.
  • No temporal model. Git gives you history, not queryable validity windows. If you need "this fact was true between these dates," Zep's temporal graph is built for that and this is not.
  • No cross-repo graph. One store per repo, deliberately. A graph over everything your organisation knows is a different product.
  • Repo scale, not org scale. JSONL plus a sqlite-vec index is right for thousands of memories, not millions.
  • The write gate is still a discipline, not a solver. The skill defines what is durable and the model applies it. A store nobody prunes still decays; that is what compress is for.

Under the hood

your prompt
    │
    ├── hook            re-arms the skill every prompt while .fornix/.sync-on exists
    ├── skill           select() before answering · write() after acting
    │
    └── MCP server      .fornix/context_store/server.py, stdio or HTTP
            │
            ├── store       .fornix/memories/<scope>.jsonl, committed, git is the history
            └── index       .fornix/index.db, sqlite-vec, gitignored, rebuilt when it drifts
.fornix/
  memories/<scope>.jsonl    the store, committed
  index.db                  sqlite-vec index, gitignored, rebuilt when it drifts
  context_store/store.py    the four operations
  context_store/server.py   MCP over stdio, or HTTP with a JSON API
  setup.sh                  client registration and workspace folders
skills/context-sync/        the skill, commands and hook the installer copies
install.sh · setup.py       the installer, twice

Tests: uv run --group dev pytest inside .fornix/ for the store, python3 test_setup.py at the root for the installer.

Releasing

Bump version in pyproject.toml, push to main. The workflow in .github/workflows/publish.yml tags v<version>, builds and publishes to PyPI with trusted publishing. A push that does not change the version does nothing.



Memory that moves with the code.

Metadata

Release files for fornix 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fornix 0.2.0
File Size Uploaded
fornix-0.2.0.tar.gz 20.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fornix 0.2.0
File Interpreter ABI Platform
fornix-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 36.9 kB

Release files / fornix-0.2.0.tar.gz

Download URL fornix-0.2.0.tar.gz
Size 20.7 kB
Tags Source
SHA-256 checksum
How to use checksums
66f6417955427a217a853081b96f1223dfcf0be99a1971023e6c3da545219b5d
BLAKE2b-256 checksum
How to use checksums
f06f22a6915e22986ca93e043d1665a0a7fe469e746bcf2d23a901d53095f2d8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / fornix-0.2.0-py3-none-any.whl

Download URL fornix-0.2.0-py3-none-any.whl
Size 16.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5f57651bb204c4c819d7975d8e522cdb90ccdda3ab6b9eb119924ed6c30dcd95
BLAKE2b-256 checksum
How to use checksums
44bf72949f6b5fc16fc8f246c0d0aed7e0b371d20ecf2a475a838fa0e5c0543e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.23 {"installer":{"name":"uv","version":"0.12.23","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

0.3.0

2 release files

This release

0.2.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page