Metadata-Version: 2.4
Name: zikaron
Version: 0.3.0
Summary: Memory and searchable project knowledge for coding agents
License-Expression: MIT
Project-URL: Homepage, https://github.com/nathan-shapiro/Zikaron
Project-URL: Repository, https://github.com/nathan-shapiro/Zikaron
Project-URL: Issues, https://github.com/nathan-shapiro/Zikaron/issues
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiosqlite==0.22.1
Requires-Dist: sqlite-vec==0.1.9
Requires-Dist: fastembed==0.8.0
Requires-Dist: huggingface_hub==1.26.0
Requires-Dist: fastmcp==3.4.7
Provides-Extra: dev
Requires-Dist: ruff==0.16.7; extra == "dev"
Requires-Dist: mypy==2.3.1; extra == "dev"
Requires-Dist: pytest==9.1.1; extra == "dev"
Requires-Dist: pytest-cov==7.1.0; extra == "dev"
Requires-Dist: pytest-asyncio==1.4.0; extra == "dev"
Requires-Dist: hypothesis==6.168.0; extra == "dev"
Requires-Dist: pyyaml==6.0.3; extra == "dev"
Requires-Dist: types-PyYAML==6.0.12.20260906; extra == "dev"
Requires-Dist: build==1.3.0; extra == "dev"
Requires-Dist: setuptools==84.0.0; extra == "dev"
Dynamic: license-file

# Zikaron

[![PyPI](https://img.shields.io/pypi/v/zikaron)](https://pypi.org/project/zikaron/)
[![CI](https://github.com/nathan-shapiro/Zikaron/actions/workflows/check.yml/badge.svg?branch=main)](https://github.com/nathan-shapiro/Zikaron/actions/workflows/check.yml)
[![Coverage](https://codecov.io/gh/nathan-shapiro/Zikaron/branch/main/graph/badge.svg)](https://codecov.io/gh/nathan-shapiro/Zikaron)
[![Downloads](https://img.shields.io/pypi/dm/zikaron)](https://pypi.org/project/zikaron/)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![Checked with mypy](https://img.shields.io/badge/mypy-strict-2a6db2.svg)](https://mypy-lang.org/)
[![License: MIT](https://img.shields.io/github/license/nathan-shapiro/Zikaron)](LICENSE)


**Zikaron** (Hebrew/Yiddish זיכרון — "memory, remembrance") gives a coding agent the two kinds of
knowledge a project holds that are not in its source code.

**Tribal knowledge — memory.** What nobody wrote down, because it was learned by living through the
work: how to build and test it, which step fails silently, which env vars the integration tests need,
which API is not safe to use yet and why, what was already tried and how it failed. Agents write
this as they go and read it back in later sessions.

**Institutional knowledge — knowledge bases.** What the project *did* write down, indexed and made
searchable: a docs tree, a directory of run books, design records, a vendored dependency's manual.
The agent searches a named corpus and gets fragments back with real line ranges, quoted verbatim.

The two differ in where they come from and therefore in who maintains them. Memory accumulates
without anybody curating it, and goes stale when the code moves under it — so the agent that a memory
misleads is the one expected to correct it. A knowledge base is a view onto files somebody else owns,
and it goes stale when those files change — so you refresh it, and a result from a file that has
changed since it was indexed comes back marked as such.

The test for whether something belongs in **memory**: *could you learn it by reading the code?* If
yes, it is out of scope. That store is for what cost somebody time to discover, and would otherwise
be discovered again by the next agent, at full price. A knowledge base has no such test — it is
whatever corpus you point it at.

It runs entirely on your machine: SQLite databases under `.zikaron/`, a local embedding model, and a
small background service on a Unix socket. **Zikaron reaches the network for exactly one thing** —
the embedding-model download described under [Install](#install), pinned to one revision and
verified against a SHA256 set it ships. That download repeats only if the cache it lands in is
cleared, or if a file in it fails verification. There is no account to create.
What it retrieves still reaches your model, since that is the point: the gists a hook injects and
the groups a consolidator is handed travel to it the way the rest of your session does.

---

## How it works

The two halves share a substrate — the same store directory, the same embedding model, the same
service, the same hybrid retrieval — and differ in everything above it. Memory is described first
and at greater length because it is the half with a write path, a policy and a consolidation step;
knowledge bases are the simpler half and are covered under
[Knowledge bases](#knowledge-bases-searching-what-the-project-wrote-down) below.

**Memories are short records** — a one-line *gist* and a longer *content* body. The gist exists to let
a future agent decide whether to read further; the content carries the detail. Records live in two
tiers: new ones land in a **journal**, and consolidation later folds the journal into **long-term**
records, merging what belongs together and retiring what does not.

**Reading happens two ways.** Before every message you send, a hook injects the few most relevant
gists into the agent's context, so recall costs no tool call and no decision. When the agent wants
more, it calls `zikaron_memory_search` for a wider look or `zikaron_memory_fetch` for a full record. Retrieval is
hybrid: a vector search over a local embedding model and a full-text search over the same corpus,
their rankings fused, so an exact identifier and a vague description both find their record.

**Writing is the agent's own judgment.** Nothing summarizes your session behind your back and no extra
model runs on the write path. A policy injected when the agent starts tells it what is worth recording;
it then calls `zikaron_memory_remember`, `zikaron_memory_amend` or `zikaron_memory_retire` itself. Near-duplicates are
detected at write time and handed back to the agent to resolve rather than silently dropped, and
nothing is ever hard-deleted — a retired record stops surfacing but stays auditable.

**Consolidation is manual and runs as a separate agent.** You ask for it; a subagent with the four
consolidation verbs and no way to search or fetch is handed groups of related entries that code
selected for it, and decides for each whether to merge, promote or discard. Keeping it separate
means a fresh context, and it cannot write outside the group it was handed — every verb refuses a
uuid that was not served to it. Under kiro those four verbs are its whole surface. Under Claude Code
it is also granted a file-reading tool, because a group too large to return is handed to it as a
file instead; that grant cannot be restricted to one path there, so what keeps it off everything
else is its prompt.

**No agent can put its own words into the text a knowledge base searches.** The searchable content
comes only from the files on disk: an indexer walks the tree you pointed it at, chunks them, and
stores the text with its line ranges. What an agent *does* write is the corpus's **definition** —
the name, description, root and filters it passes to `zikaron_knowledge_add`, of which
`zikaron_knowledge_rename` changes the name alone, and the name and description come back on every
search as the label on each group — plus a build's own metadata and the four search counters
described under [Files and logs](#files-and-logs). The agent reaches them through
`zikaron_knowledge_search` and a handful of management verbs — every tool name carries its
subsystem, `zikaron_memory_*` or `zikaron_knowledge_*`, so a model choosing between the two stores
reads which one it is addressing before it reads a description. Consolidation never touches them.

### The pieces

| | What it is |
|---|---|
| **core** | The library: the store, retrieval, chunking, consolidation, and the knowledge index. No process concerns, no transport. |
| **service** | A long-running process per project. Holds the embedding model in memory and the databases open, and answers requests over a Unix socket. Starts itself when needed and stops itself when idle. |
| **MCP server** | Translates the agent's tool calls into requests to the service. One process per agent instance; loads no model. |
| **hook** | A single-shot executable your agent runs on start, before each message, and — under Claude Code — when it spawns a subagent. Deliberately tiny; loads no model and never touches the databases directly. |
| **indexer** | A detached process started per knowledge-base build — by the service on every `add`/`refresh`, whether an agent asked or you did, or by you running the foreground command a build result prints. It outlives the call that started it, loads a model, saturates a core for a minute or more, and exits when the build finishes. If you see one in `ps`, that is a corpus building, not a runaway. |

The service exists for one measured reason: loading the embedding model costs about **780 ms**, and
retrieval sits on the path of every message you send. Keeping the model resident turns that into a
**~7 ms** embed and a **~9 ms** round trip. (The indexer pays that same load cost once per build, in
its own process, which is why it is detached rather than run inside the service.) The hook stays
thin for the same reason — it runs once per message, and it costs about **50 ms** end to end.

If the service is not running, the hook starts it and moves on. If anything fails, the hook writes one
line to a log, tells the agent to mention it to you, and exits cleanly — it never blocks your message
and never fails your turn.

---

## Requirements

- **Linux, or macOS on Apple Silicon.** The transport is a Unix domain socket and the paths assume a
  POSIX filesystem.
  **Linux x86_64 and macOS arm64 are verified; Linux arm64 is supported but not verified** — CI runs
  the full hermetic gate on both of the first two and nothing has ever run on the third. What is
  verified on macOS is that gate: no harness binary exists on a CI runner, so nothing there
  exercises a real session pushing or searching. **Intel Macs and Windows are not supported**:
  Intel Macs would mean pinning a year-stale `onnxruntime` on two of the three interpreters and a
  hard install failure on the third, and Windows needs a second RPC transport rather than a flag.
  `design/distribution.md` §1 has the detail.
- **Python 3.12 or newer.** Tested versions: 3.12, 3.13, 3.14.
- **A supported harness installed: `kiro-cli` or Claude Code.** Zikaron installs hook and MCP
  entries whose only reader is that harness's own binary, so the installer checks it is on your
  `PATH` and **refuses if it is not** — writing those files where nothing reads them would exit 0
  and leave you with no Zikaron tools and no error to search for. Pass `--print-only` to see exactly
  what would be written without writing it, which is how to provision a machine before its harness.

  **The installer itself is an ordinary program and does not need a harness *running* in order to
  execute.** Run it from any shell — a terminal, an ssh session, a container build. It works out
  which harness a project is for from the project itself (a `.claude/` or a `.kiro/` directory),
  falls back to the `CLAUDECODE` marker when the project says nothing, and **refuses — telling you
  to name the harness — when the project names both, or when neither the project nor the marker
  says**. `CLAUDECODE` reaches the processes a session spawns, so a **Claude Code** agent asked to
  run the installer is detected; kiro exports no marker, so there it is a `.kiro/` in the project or
  nothing. A person running it from a terminal outside a session, into a project that has not been
  set up yet, is the case where nothing says anything and `--harness` is required.

  Under kiro it additionally validates the consolidator's model id against the binary, because kiro
  substitutes an unknown model silently; Claude Code refuses one itself, at spawn, so no such check
  is needed there.

## Install

**Two supported paths, and the first is recommended for a reason worth reading.**

```bash
# Recommended. `--managed-python` is not optional; "Why `uv` is recommended" below says why.
uv tool install --managed-python zikaron
```

```bash
# The same, from the repository rather than PyPI — for a version that has not been released.
uv tool install --managed-python git+https://github.com/nathan-shapiro/Zikaron.git
```

Both of those need [`uv`](https://docs.astral.sh/uv/) and **no Python**, which is the point; the
`git+` form also needs `git` on `PATH`, which `uv` calls rather than bundles. `uv`'s own installer is
`curl -LsSf https://astral.sh/uv/install.sh | sh`, and on a stripped-down image you may have to
install `curl` and `ca-certificates` before that line will run.

```bash
# Alternative — a source checkout on a Python you already have.
git clone https://github.com/nathan-shapiro/Zikaron.git
cd Zikaron
python3 -m venv .venv
.venv/bin/pip install -e .
```

**Why `uv` is recommended.** Zikaron's vector search is a loadable SQLite extension, and whether an
interpreter can load one at all is decided when *that interpreter* was compiled — and it is reported
off in some widely used builds, python.org's macOS installer and conda-forge among them. The
interpreters `uv` fetches were **measured** here and have it; those two were not, so treat them as a
reported risk rather than a verified failure. A host Python may well work; it is simply the path
where "it installed fine and then retrieval does not work" is possible.

**And that is why `--managed-python` is in the command.** Without it, uv falls back to a host
interpreter whenever it has not already downloaded one of its own — which is the case right after you
install uv. Measured, running both forms against empty uv directories: **with** the flag, uv
downloaded its own CPython and built the tool environment on it; **without** it, uv used
`/usr/bin/python3.12` and downloaded nothing. So the plain command hands you the host build on a
fresh machine, which is the outcome the recommendation exists to avoid.

**`uv` is used at install time only.** Zikaron never invokes `uv run` or `uvx` when it runs: the hook
executes once per message you type, and a resolver in that path costs about 20 ms every time. What
gets installed is ordinary console scripts with a fixed interpreter.

**First use downloads a 64 MB embedding model** from Hugging Face, so the very first search is slow
and needs the network. Everything after it is local, and the download survives a reboot: it is
cached per user, under `$XDG_CACHE_HOME/zikaron/models` (or `~/Library/Caches/zikaron/models` on
macOS), and `$FASTEMBED_CACHE_PATH` overrides that if you already set it. It is outside your project
either way, so no `.gitignore` entry helps.

**The download is pinned and verified.** Zikaron fetches one named revision of one Hugging Face
repository and checks every file **it downloads** against a SHA256 it ships, so an upstream change or
a damaged transfer is refused rather than used, and it re-fetches once before giving up. A file that
goes bad on disk *afterwards* is `zikaron doctor`'s to find rather than startup's — checking all
64 MB on every start costs more of the push hook's budget than it is worth, which is measured in
`design/distribution.md` §"Model acquisition". `zikaron doctor` reports which state your cache is in.

Then install into the project you want Zikaron in, naming the harness:

```bash
cd /path/to/your/project
zikaron install --project . --harness claude-code    # or: --harness kiro
```

**Name it on a first install, rather than leaving it to be detected.** Detection looks for a
`.kiro/` or `.claude/` directory in the project and for `CLAUDECODE` in the environment, and it
refuses rather than guessing when neither says. Running from an ordinary terminal, a first install
is exactly the case where neither says anything: a Claude Code project has no `.claude/` yet —
neither the trust dialog nor an ordinary tool use creates one — and `CLAUDECODE` is exported into
the processes a session spawns, not into a terminal outside one. (Ask a *Claude Code* agent to run
the installer and detection does succeed, because `CLAUDECODE` reaches it; kiro exports no marker,
so there it is a `.kiro/` in the project or nothing.)

Under **kiro**, also name the config for the agent you actually work in:

```bash
zikaron install --project . --harness kiro --agent .kiro/agents/<your-agent>.json
```

If `zikaron` is not on your `PATH` — a source checkout whose virtualenv you have not activated —
three commands have a module form you can run with that virtualenv's interpreter, and the spelling
is not uniform:

| command | module form |
|---|---|
| `zikaron install` | `python -m zikaron.install` |
| `zikaron knowledge` | `python -m zikaron.knowledge` |
| `zikaron init` | `python -m zikaron.project` |

`zikaron doctor` and `zikaron --version` have no such form; both are new with the `zikaron` command.

Omitting `--harness` falls back to the detection described under [Requirements](#requirements), which
refuses rather than guessing when it cannot tell. `--agent` is kiro-only, and passing it under Claude
Code is refused rather than ignored — there its entries go into fixed project files instead. To see
exactly what would be written without writing anything, pass `--print-only`; it previews past an
absent harness binary or an unvalidated model, and says so — but it still has to know *which*
harness, so pass `--harness` alongside it exactly as a real install would.

**Re-run the installer after you upgrade the package.** Upgrading replaces the code, not what an
earlier version wrote into your project, and a release can change a merged entry — `alwaysLoad` is
one such change. On both harnesses re-running upgrades those entries in place and needs no
`--force`; it refuses only where another Zikaron install owns something, or, under Claude Code,
where you have added a hook of your own inside Zikaron's group (both below), which is what `--force`
exists for. `zikaron doctor` afterwards says whether a Claude Code project's Zikaron entries carry
`alwaysLoad` — the one thing about `.mcp.json` it judges.

**What upgrading the package does not do by itself:**

- **A service already running keeps the old code** until it idles out (`idle_timeout`, below) or is
  stopped — `pkill -f zikaron.service.main`. Until then `zikaron knowledge` may be refused with a
  `bounds` error naming a field you never typed: that is the older service not knowing the newer
  command.
- **A store moves forward, never back.** The first time a newer service opens a project's store it
  migrates it in one transaction, without saying so, and an older Zikaron then refuses that store —
  `0.1.0` cannot open one `0.3.0` has opened. If more than one environment serves the same project,
  upgrade them together.

Finally, if the project is a git repository, tell git to ignore the store — and, under Claude Code,
the MCP config too:

```bash
echo '.zikaron/' >> .gitignore
echo '.mcp.json*' >> .gitignore    # Claude Code only; see below
```

**The `*` covers `.mcp.json.bak`**, the copy the installer takes of `.mcp.json` before the first
merge that finds none there (the backup rule is below). It sits at the repository root and holds the
file as it then stood — the same absolute paths, and whatever the entry held at the time, an `env`
included — and it is where `--force` sends you to look for a key it dropped.

**`.mcp.json` is the awkward one.** Claude Code intends that file to be committed and shared — that
is what "project-scoped" means — but the entries Zikaron writes into it name absolute paths inside
*your* virtualenv, so a clone-mate gets a server that cannot start, plus an approval prompt for it.
This is the same objection that keeps the hook entries out of the checked-in `settings.json`; the
difference is that `settings.local.json` exists as an untracked sibling and `.mcp.json` has no
equivalent. With no per-project, machine-local MCP scope to move it to, the choice is yours: ignore
the file, or accept that each clone re-runs the installer. Re-running is safe — an entry naming a
*different* virtualenv is exactly what the installer refuses loudly rather than merging over, which
is the clone-mate case.

The installer does not edit `.gitignore` for you; appending to it is not a decision an installer
should make silently. But it matters and is easy to forget: without it the memory database and its
write-ahead log, every knowledge base's index and the service logs all show up as untracked, and a
routine `git add -A` commits the lot. None of it is useful to anyone else, and none of it is
reproducible from your repository.

What it writes under **kiro**, relative to the project:

| Path | What |
|---|---|
| `.kiro/agents/zikaron-consolidator.json` | the consolidation subagent: its model, its four tools, its prompt |
| `.kiro/skills/zikaron-consolidate/SKILL.md` | how to run a consolidation, and how to recover a stuck one |
| the agent config you named | `hooks` for start and per-message, an `mcpServers` entry, `@zikaron` in `tools` and `allowedTools`, the consolidator in `toolsSettings.crew`, and the skill in `resources` |

and under **Claude Code**:

| Path | What |
|---|---|
| `.claude/agents/zikaron-consolidator.md` | the consolidation subagent, as frontmatter plus its prompt |
| `.claude/skills/zikaron-consolidate/SKILL.md` | how to run a consolidation, and how to recover a stuck one |
| `.claude/settings.local.json` | three hooks — session start, per-message, and per-subagent — plus both approval keys, `enabledMcpjsonServers` and `permissions.allow` |
| `.mcp.json` | both servers: `zikaron` for your own tools, `zikaron-consolidator` for the consolidation verbs — each marked `alwaysLoad` so their descriptions are in context from the start rather than fetched per tool |

**`settings.local.json`, not `settings.json`**, and it matters if you commit your settings: the hook
entries name absolute paths inside *your* virtualenv, so they are meaningless in anyone else's clone.
The third hook is the one with no kiro counterpart — it hands the write policy to each subagent you
spawn, which kiro achieves by firing its ordinary hooks for subagent sessions instead.

It backs up any file it merges into (`<file>.bak`, and the first backup wins), and **refreshes a
shipped file whose contents are not what this version ships** — after backing it up, and saying so.
That is what makes upgrading work, and the cost is that a hand-edit to a shipped file is reverted on
the next install rather than kept. **Under Claude Code the same holds inside a file it *merges*
into**: a Zikaron hook entry whose command is this install's but whose `timeout` or matcher you
changed is rewritten, and the install names the trigger so you can re-apply it. Otherwise a new
default timeout could never reach an existing install. What refuses is an entry naming a *different*
Zikaron install — or a Zikaron hook group you have added your own second entry to, since the
installer replaces a group wholesale rather than merging inside one, so refusing is the only way not
to drop your entry silently. `--force` overrides that, and then says which command of your own it
dropped.

`.mcp.json` is merged **per key**: a key you added to Zikaron's own entry is kept and named in the
output, a value this install writes — `alwaysLoad` in particular — is set to this install's and
named too, whether it was missing or different, and only a different `command` or `--mode` refuses. `--force` is the exception: it
replaces the entry whole, says which of your keys that dropped, and points you at the `.bak` beside
the file. It does not say whether the backup has them, because it cannot know — the first backup
wins, so the `.bak` on disk may have been written before or after you added the key.

Under kiro the same rules hold inside the agent config you name: the `mcpServers` entry merges per
key, a Zikaron hook entry whose command is this install's is rewritten and its trigger named, and
only an entry naming a different install's command, or another `--mode`, refuses. A hook of Zikaron's
on a trigger this install does not write is left alone. Kiro's install also validates the
consolidator's model id, because an unknown model would otherwise be silently replaced by the
harness's default.

`@zikaron` has to be in `tools` or Zikaron's tools are simply absent: the `mcpServers` entry
*configures* the server and `tools` is what *selects* from it. It goes into `allowedTools` too, so the
agent can record without interrupting you. That is deliberate rather than lax — the whole design leans
on the agent writing freely, and a permission prompt per write both suppresses that and trains you to
click through prompts. What it trusts is still narrow, but it is not only rows. The memory tools read
and write rows in a local database. The knowledge tools additionally **read a directory tree you
name, which may sit outside the project** — a docs tree or a vendored dependency is a legitimate
corpus, so only degenerate roots (the filesystem root, your home directory itself) are refused — and
copy its text into an index, in a detached process that works a core for minutes. No network either
way **once the model is cached** — the only fetch Zikaron makes is the embedder download described
above, which a cleared cache directory can make the indexer pay again — and nothing is written
into your project outside `.zikaron/`. A mistaken memory write is *recoverable* rather than
undoable: `zikaron_memory_retire` withdraws a record from ordinary retrieval and leaves it
auditable, while an amend overwrites prose that nothing restores. A mistaken
`zikaron_knowledge_remove` destroys that corpus's index, which is rebuildable from the files it was
built from but not instantly. Pass `--no-trust-tools` if you would rather approve each one.

**That trust stops at the primary agent's tools.** Spawning the consolidator is a separate grant — a
subagent with its own model invocation, four mutation verbs and, under Claude Code, a file read —
and the installer does not make it for you. **Under kiro** that grant is
`toolsSettings.crew.trustedAgents`, which the installer only ever reads: starting a consolidation
therefore asks your permission once, and adding `zikaron-consolidator` there yourself is what stops
it. Claude Code has no such key, so there is nothing to add and nothing withheld.

**Three keys under kiro only, and Claude Code has none of them.** The consolidator also has to be
reachable by the `subagent` tool, so if your config already restricts which agents may be spawned
(`toolsSettings.crew.availableAgents`), the installer adds `zikaron-consolidator` to that list. If
you have no such restriction it leaves it alone — an empty list means *every* agent is available,
and writing one entry into it would restrict you to just this one. The skill is also declared in the
agent's `resources` unless something there already covers it. Skills normally arrive by inheritance,
so that entry is usually redundant — but it is the only thing that makes the skill loadable if you
have set `chat.disableInheritingDefaultResources`, and declaring a resource does not disable
inheritance, so it can only help.

**One thing the kiro install deliberately does not do:** add the `subagent` tool itself, which the
consolidation skill needs in order to spawn the consolidator. Its reach is much wider than Zikaron's
own tools, so that grant stays yours. The installer says so if it is missing. Under Claude Code none
of this arises — the shipped skill is an ordinary project file, and the consolidator is spawned
through whatever subagent tool the harness already gives your agent.

**Installing into a clone of the Zikaron repository itself, under kiro,** finds
`.kiro/agents/zikaron-consolidator.json` already tracked, carrying whichever virtualenv path the last
committer had. The installer notices that the file names a different install, backs it up, and
rewrites it for yours. Under Claude Code there is nothing tracked to collide with — the consolidator
config is not in this repository — so the installer simply writes it.

### What to know under Claude Code

**There are three approval gates and the install answers two of them.** `enabledMcpjsonServers`
decides whether a project-scoped `.mcp.json` server **loads** at all; `permissions.allow` decides
whether each tool **call** goes through without a prompt. Both are written into
`settings.local.json`. The one it does not answer comes first: Claude Code's own folder-trust
dialog, on first entry to a directory, which is yours to answer — and it reads the
`permissions.allow` entries back at you as a warning that this folder pre-approves tool permissions.
If Zikaron's tools are missing after a fresh install, check `/mcp` for a server pending approval
before looking anywhere else — an unapproved server is simply absent, with nothing saying why. If
they are present but every write interrupts you, it is `permissions.allow` that did not take.

**A fourth prompt is deliberately left live.** The first time a consolidation meets a group too
large for the harness to deliver, the group is written to a file and Claude Code asks whether the
consolidator may read from the runtime directory. Allow it for the session — and expect it again in
the next one, because session scope is the only scope that grant has. It is not pre-answered on
purpose: a file-reading tool cannot be restricted to one path in subagent config, so this prompt is
the only point at which that grant is put to you as a question.

`--no-trust-tools` withholds both written keys for *your* tools. It does not withhold them for the
consolidator's: a subagent has nobody to answer a permission prompt, so an unapproved **MCP** tool
there does not ask, it fails at the moment consolidation needs it. The file read above is the
exception — that one prompts.

**A subagent that sets its own `tools:` list cannot see Zikaron at all.** Registering a server in
`.mcp.json` makes it available project-wide, but an agent whose frontmatter carries an explicit
`tools:` allowlist gets exactly that list — the registration does not add to it. So a correct
install, a running service and a subagent doing the work can still produce zero memories, with
nothing anywhere saying why. The install names any such agent it finds and tells you to add
`mcp__zikaron` to its `tools:`, and `zikaron doctor` reports the same. Your allowlist is a
deliberate grant, so neither one edits it for you.

**Your own agent can see the four consolidation verbs**, and that is not a misconfiguration. A server
has to be registered for the whole session before any subagent can reach it, so registering the
consolidator's server exposes it to you too. Under kiro the two tool sets are separated mechanically;
here it is the prompt that keeps them apart. Nothing in the store is at risk from it — the never-lose
guard, the receipts and the lease are untouched — but an unauthorized consolidation would spend
tokens and could write a poorly-judged long-term record. `permissions.deny` is **not** the fix: it is
global and unregisters the tool, after which the consolidator itself refuses to start.

### Options

| Flag | Effect |
|---|---|
| `--project <dir>` | the project to install into. Also where the store lives (default: the current directory) |
| `--harness {auto,kiro,claude-code}` | which harness to install for. `auto` reads the project, then the `CLAUDECODE` marker; it refuses when the project has **both** dotdirs, and when neither source says |
| `--agent <path>` | **kiro only.** Merge the hook and MCP entries into that config, after backing it up |
| `--print-only` | print what would be written and write nothing at all |
| `--model <id>` | the consolidator's model (default: `claude-sonnet-5` under kiro, `sonnet` under Claude Code) |
| `--format {object,array}` | **kiro only.** Which hook format to write when the target config has none yet |
| `--no-trust-tools` | do not pre-approve Zikaron's own tools, so every Zikaron tool call asks permission |
| `--force` | replace a symlink at a shipped path, and overwrite entries wired to a different Zikaron install that would otherwise be refused. It also stops merging: a server entry — `.mcp.json`'s, or kiro's `mcpServers` — is replaced **whole**, so a key you added to Zikaron's own entry goes, and under Claude Code a Zikaron hook group is replaced whole, so a hook of your own inside it goes too. Both are named in the output |

Both hook formats kiro accepts are supported, and a config that already uses one keeps it: kiro
rewrites a config in whichever format it read, so mixing them in one file has no defined meaning.

## Verify

**Start with `zikaron doctor`**, which answers the questions that decide whether Zikaron can run on
this machine at all — and names what to change for each one it answers badly, rather than leaving you
a traceback:

```console
$ zikaron doctor
ok   sqlite extension loading              available
ok   sqlite FTS5                           available
ok   sqlite-vec                            loads, and registers vec0
ok   model cache (BAAI/bge-small-en-v1.5)  present at 52398278842ec682c6f32300af41344b1c0b0bb2 under /home/you/.cache/zikaron/models, 5 files verified
ok   socket path length                    /run/user/1000/zikaron/2eb0b9c22f39e097ac31aa6adfe7fba7.sock fits 108 bytes
--   sqlite version                        3.45.1 linked by Python 3.12.3
```

That is real output with the home directory replaced; everything else is verbatim, including the
full revision and socket path the command prints.

It exits non-zero if any check fails. **A model cache that is not there yet is not a failure** — there
is no prefetch at install time, so a first run reports it as fetched on first use and exits 0. The
last row reports rather than checks: the linked SQLite version varies between interpreter builds on
one machine and there is no correct value to compare against, so stating it is the whole point.
Some rows appear only where a project gives them something to check: under Claude Code, whether
the Zikaron servers in `.mcp.json` carry `alwaysLoad` — an older install lacks it, and a re-install
fixes that — and which of your subagents cannot reach Zikaron's tools.
`zikaron doctor --project <dir>` checks another project.

**`zikaron --version` names the version you installed**, which is the first thing to put in a bug
report; `doctor` reports on the machine and leaves the version to this. A PyPI install reports a
release. A `git+` install or a source checkout between releases reports a `.dev` version — a `.dev0`
suffix means "working toward that number, not released" — so it never impersonates a release, but it
does span every commit until the next bump, and a bug report from one needs the commit as well.

Then start a session with the agent you installed into. On start you should see nothing unusual — the
write policy goes into the model's context, not to your terminal. Then, from the project directory:

```bash
# a service should be running for this project after your first message
pgrep -af zikaron.service.main

# one line per hook failure, write-policy-override note, or session/environment mismatch; absent means none happened
cat .zikaron/hook.log
```

Ask the agent to remember something, then start a fresh session and ask about it. If the memory comes
back, the whole loop works: write through MCP, injection through the hook, retrieval in between.

## Using it

**Writing.** Say "remember that" when you want something kept, or leave it to the agent — the injected
policy tells it what is worth recording. Its bias is toward recording, because the common failure is an
agent that records nothing.

**Reading.** The relevant gists arrive before every message you send. The agent fetches full records
when it wants the detail, and can search when the injected few are not enough.

**Consolidating.** Ask the agent to consolidate project memory once the journal has built up — after a
stretch of real work, or at the end of a task. It loads the shipped skill and spawns the consolidator.
If a run ever seems stuck, ask again: a second invocation takes the abandoned run over and replans.

### Knowledge bases: searching what the project wrote down

Memory holds the **tribal** knowledge agents learned by working here. A **knowledge base** is the
**institutional** half: a named, indexed corpus of text files the project already has — a docs tree,
a directory of run books, a vendored dependency's documentation. The agent searches it and gets back
fragments with line ranges, quoted verbatim, so it can read further or quote them as they stand.

Ask the agent to create one and it will, without leaving the session: *"index the docs directory as
a knowledge base called design docs"*. It has tools to list, create, rename, refresh, inspect and
remove them. The same verbs are available at a shell, for when no agent is running:

```bash
zikaron init                         # once per project; safe to run twice
zikaron knowledge list
zikaron knowledge add "design docs" --path ./design \
    --description "Architecture and design records"
zikaron knowledge refresh            # every corpus; name one to narrow it
zikaron knowledge status "design docs"
```

**`init` comes first because every `knowledge` verb refuses a project with no store**, and it is the
only command that creates one. In a project your agent has already opened there is a store
already, and `init` will say so and exit 0 — so putting it at the top of a provisioning script
costs at most the service start the next command would pay anyway. The **first** `init` — the one
that creates the store — can outlast the deadline on a cold model cache and exit 1 while the
service keeps fetching the model; running it again succeeds when the service was only slow, and the
command points at `.zikaron/service.log` for when it was not.

**They act on the project you run them in.** The directory is resolved as `--project` if you pass
it, else the harness's own project directory if it exports one, else the directory you are standing
in — so a command typed a few levels down is a different project from the one your agent uses, and
saying so is the whole point of the refusal. It names the store it found above you, if there is one,
and never acts on it.

Three more things are worth knowing before you point a knowledge base at a directory.

**Building takes minutes and runs in the background.** `add` and `refresh` start a build and return.
A **first** build answers nothing at all while it runs: the corpus reports `reindex_required` for
its whole duration, and only becomes searchable when the build completes. A later `refresh` keeps
answering from what is already indexed while it works — as does a `--full` one — with the exception
of a rebuild forced by a changed embedding model, which empties the corpus before it starts and so
answers nothing until it finishes. `status` says how far it has got.

In a script or a CI step, use `zikaron knowledge refresh --wait`: it reports progress and returns
once every build against those corpora has let go of its corpus — including one an earlier `add`
started, which it waits for rather than stepping past — exiting non-zero if any build left its corpus
unusable. By then each build's record of what it cost has been written to `memory.db`, or dropped if
the store stayed locked; the indexer process itself may still be exiting. Without `--wait` the step
can finish while an indexer is still running, and whatever kills the step kills the build too.

**Nothing updates an index on its own.** There is no watcher and no schedule: a corpus drifts from
its files until somebody refreshes it. A search says so when it can — a result whose file has
changed since it was indexed comes back marked `stale`.

**An index holds the text of every file in it.** Do not point one at a directory holding
credentials. The databases are `0600`, the same as `memory.db`, and they are under `.zikaron/`, so
the `.gitignore` line above already covers them — but what is *in* one is searchable by every agent
working in this project.

---

## Configuration

Entirely optional — every key has a working default. Two TOML files, later wins per key:

```
~/.config/zikaron/config.toml     applies to every project
<project>/.zikaron/config.toml    this project only
```

An unknown key or a value of the wrong type is **fatal rather than ignored**, so a typo cannot silently
leave you on a default. Values are range-checked when the service starts.

```toml
[retrieval]
fusion_depth = 50          # how many candidates each arm contributes before fusion
rrf_k = 60                 # the rank-fusion constant; larger flattens the ranking

[consolidation]
group_max = 12             # members per group before it is split
run_lease = 1800           # seconds a consolidation run holds the store

[service]
idle_timeout = 1800        # seconds of inactivity before the service stops itself
```

### Everything that is tweakable

| Section | Key | Default | Range | What it does |
|---|---|---|---|---|
| `embedding` | `embed_model` | `BAAI/bge-small-en-v1.5` | any model your embedder can load | which embedding model to use. **Store-coupled (hard)** |
| `embedding` | `embed_dim` | `384` | ≥ 1 | that model's vector width. **Store-coupled (hard)** |
| `embedding` | `embed_prefix_query` | a retrieval instruction | free text | the prefix prepended to a query before embedding |
| `indexing` | `chunk_max_tokens` | `450` | 64–8192 | how large a chunk may get before a record is split. **Store-coupled (soft)** |
| `indexing` | `gist_max_tokens` | `64` | 8–256 | the longest gist a write may carry |
| `indexing` | `knowledge_max_file_bytes` | `1048576` | 1–67108864 | **knowledge bases:** the largest file an indexer will read; bigger ones are skipped |
| `indexing` | `knowledge_embed_batch` | `32` | 1–256 | **knowledge bases:** how many chunks an indexer embeds per batch |
| `retrieval` | `chunk_overfetch` | `8` | 1–64 | how many extra chunks the vector arm reads to cover its candidates |
| `retrieval` | `fusion_depth` | `50` | 1–500 | how deep each arm goes before the two are fused |
| `retrieval` | `rrf_k` | `60` | ≥ 1 | the rank-fusion constant |
| `retrieval` | `supersession_penalty` | `0.5` | >0.0–1.0 | how far a replaced record is demoted rather than hidden — the score is multiplied by it, so zero is refused because it would hide the record rather than demote it |
| `retrieval` | `retired_penalty` | `0.5` | >0.0–1.0 | the same, for a retired record when one is asked for |
| `retrieval` | `supersession_max_depth` | `32` | 1–1024 | how far a chain of replacements is followed |
| `retrieval` | `fts_query_max_terms` | `64` | 1–512 | the largest full-text query built from one prompt |
| `retrieval` | `knowledge_max_chunks_per_file` | `2` | 1–20 | **knowledge bases:** how many fragments one file may contribute to a result set |
| `retrieval` | `knowledge_snippet_max_chars` | `1200` | 80–24000 | **knowledge bases:** how much text a single returned fragment carries, counted in code points |
| `dedup` | `dedup_threshold` | `0.8` | 0.0–1.0 | how similar a new record must be to be offered back as a near-duplicate |
| `dedup` | `dedup_max` | `3` | 0–20 | how many near-duplicates are handed back at once |
| `consolidation` | `mutual_k` | `5` | 2–50 | how many neighbours each entry considers when grouping |
| `consolidation` | `orphan_edge_cutoff` | `0.65` | 0.0–1.0 | how close two entries must be to group without a shared anchor |
| `consolidation` | `anchor_cutoff` | `0.65` | 0.0–1.0 | how close an entry must be to a long-term record to be anchored to it |
| `consolidation` | `group_max` | `12` | 2–64 | members per group before it is split into shards |
| `consolidation` | `max_group_serves` | `3` | 1–16 | how many times one group may be re-served before it is abandoned |
| `consolidation` | `run_lease` | `1800` | 60–86400 | seconds a consolidation run holds the store |
| `consolidation` | `spill_threshold` | `27000` | 4096–1048576 | **Claude Code only:** bytes past which a consolidator tool result is written beside the socket instead of returned inline. Nothing spills under kiro, where the key has no effect |
| `service` | `idle_timeout` | `1800` | 60–86400 | seconds of inactivity before the service stops itself |
| `signals` | `signal_horizon_days` | `30` | 1–3650 | the window the write-policy instrumentation reports over |

**Three keys are coupled to what is already stored, at two severities.** The file says what you
want; the store records what was actually done, and a file that disagrees is *requesting* a change
rather than making one.

- **Hard — `embed_model`, `embed_dim`.** They describe the vectors already on disk, so changing
  either means reindexing. The two stores say so differently. The **memory store** refuses: the
  service will not start rather than serve a store whose configuration disagrees with its contents.
  **Every knowledge base** seeds the same two keys into its own metadata when it is created and
  **degrades instead of refusing** — a corpus whose recorded encoder no longer matches reports
  `reindex_required`, answers searches with no results, and is rebuilt whole by the next `refresh`.
  A corpus's own database is opened per call and never at startup, so one in this state does not
  keep the service down.
- **Soft — `chunk_max_tokens`, and only in the memory store.** There the file governs **new** writes:
  nothing refuses, nothing needs rebuilding, and the store simply ends up holding chunks cut at more
  than one size. A **knowledge base** seeds this key at creation like the other two and then ignores
  later changes to it, so changing it affects only corpora you add afterwards — an existing corpus is
  re-chunked at the value it was created with, even by a full `refresh`.

**Three further keys are frozen into a knowledge base alone**, by the same mechanism and with no
equivalent in the memory store: `rrf_k`, `fusion_depth` and `knowledge_max_file_bytes` are recorded
in a corpus when it is created and read from there afterwards — the first two on every search, the
third on every build — so changing them in the file affects only corpora you add afterwards.
`knowledge_max_file_bytes` can also be set per corpus when you add it. They carry no marker in the
table above because the severities are a property of the *memory* store, which these do not touch.

**The write policy can be overridden per project.** Put your own text at `.zikaron/write-policy.md`
and the session-start hook prints that instead of the shipped policy. An absent override is the
ordinary case and is silent. If one is there but is unreadable, empty, a symlink, not a regular
file, or sits in a store directory that is not yours alone — owned by another user, or carrying any
group or other permission at all — the shipped text is used and one line lands in `hook.log` saying
which. An override **too large for the harness's injection budget is
printed anyway**, with a line in `hook.log` — the harness truncates in silence, and a policy the
model received part of is worse than one you were told about. The policy print is the one path that
is never allowed to fail.

---

## Files and logs

Almost everything Zikaron writes for a project *while it runs* lives in one directory, and all of
it is `0600` or `0700`. The **installer** also writes harness config — the files tabled under
[Install](#install), plus a `.bak` beside anything it replaces.

```
<project>/.zikaron/
  memory.db          the store itself — never commit this
  memory.db-wal      SQLite's write-ahead log; part of the store
  memory.db-shm      SQLite's shared-memory index; part of the store
  knowledge/         one database per knowledge base, named by a generated id.
                     Absent until you create one
  config.toml        your per-project overrides, if you wrote any
  service.log        the background service: startup, resolved config, errors, why it stopped,
                     any request running longer than 30 s, and the stack dumps below
  hook.log           one line per hook failure, write-policy-override note, or session/environment
                     mismatch. Absent means none happened. Not every line is a failure: an override
                     that is used but exceeds the injection budget is printed anyway and logged
  warmup.log         the small helper that warms the service when a session starts
  write-policy.md    your policy override, if you wrote one
```

**Two things the running system writes live elsewhere, and both have to.**

The first is the embedding model, which is **per user rather than per project**: one 64 MB artefact
shared by every project you use Zikaron in, under `$XDG_CACHE_HOME/zikaron/models` —
`~/Library/Caches/zikaron/models` on macOS, or wherever `$FASTEMBED_CACHE_PATH` points if you set it.
A copy inside each project would duplicate it for bytes that are identical by construction, since the
version fetched is pinned. Removing a project does not remove it; [Uninstall](#uninstall) says how.

The second is the service's Unix socket, which with its lock file sits in
`$XDG_RUNTIME_DIR/zikaron/` — or `/tmp/zikaron-<uid>/` where that variable is unset, which is every
Mac — named by a hash of the store's path, `0600` inside a `0700` directory. A socket belongs on a
runtime filesystem rather than in your project. **Under Claude
Code**, an over-large consolidator result also spills to a file beside them, carrying that group's
record prose verbatim and named by the same store hash; it is deleted when the consolidator asks for
its next group — or, if that process is killed first, by the next one that starts. It is the one
copy of memory text that lives outside `.zikaron/`, which is why the erasure procedure under
[Secrets](#secrets) removes it by hand. Nothing spills under kiro.

**What `service.log` tells you about a service that stopped, or stopped answering.** A clean stop
writes one `stopping: reason=…` line: `idle`, `store_replaced`, `encoder_failed`, or `sigterm` /
`sigint` when it was asked to stop. A request still running after the 30 s idle poll is logged as
`long request: method=… session=… age=…` on every poll until it ends — `session=unresolved` if it
never got that far. Two signals write a dump without stopping the service: `kill -USR2 <pid>` writes
the requests in flight and every asyncio task's stack, and `kill -USR1 <pid>` writes every thread's
stack, which is the one that still works when the service's event loop itself is stuck. The second is
raw text, without the timestamp and `pid=` every other line carries.

Two notes on reading the rest. `memory.db` can look implausibly small while `memory.db-wal` is large —
that is normal for a database held open by the service, since recent writes live in the log until a
checkpoint; the files together are the store. And `hook.log` records only a fixed failure label and an
error code, never your prompt or a memory's text, so it cannot hold a leaked secret.

There is one more log that is not a file: an event table inside `memory.db`. It records what happened
on every **memory** read and write — used to measure whether the write policy is working, and kept in
the database because those rows must commit alongside the change they describe. It also holds an
access log, best-effort: a `call` row for each request the service answers — which method, how long
it took, and what it refused, if anything — and a `knowledge_build` row for each corpus build.
Neither carries text. A knowledge base's own database has no event log: it keeps four **search**
counters in its metadata — searches, empty searches,
results returned, results stale — which `zikaron_knowledge_status` reports beside the last build's
own file, byte and skip-reason counts.

---

## When something is wrong

Zikaron is built to fail quietly rather than get in your way. A hook that cannot reach the service
prints a short note asking the agent to tell you, writes one line to `hook.log`, and exits cleanly. It
never blocks your message, and it never reads the store directly.

**Run `zikaron doctor` first for anything that smells environmental** — no search results at all, a
service that will not start, an MCP server that dies on contact. It checks the things about this
machine that can stop Zikaron working and names what to change for each. The table below is for
symptoms it cannot see.

| Symptom | Where to look |
|---|---|
| nothing searchable works at all | `zikaron doctor` — extension loading, FTS5 and `sqlite-vec` are the three that fail this way, and all three are properties of the interpreter rather than of your project |
| the MCP server starts and immediately dies | `zikaron doctor` — an over-long `$XDG_RUNTIME_DIR` makes the socket path exceed what the platform allows, and this is the only channel that says so |
| no memories are being injected | `.zikaron/hook.log`, then `pgrep -af zikaron.service.main` |
| the agent has no `zikaron_*` tools | kiro: `/tools`, and check `@zikaron` is in the agent's `tools`. Claude Code: `/mcp`, and check neither server is pending approval — then check whether that agent's own frontmatter sets `tools:`, which overrides the project-wide registration entirely (below) |
| every Zikaron tool call asks permission | kiro: add `@zikaron` to the agent's `allowedTools`. Claude Code: it is `permissions.allow` in `settings.local.json` that did not take |
| a knowledge search returns nothing, or too little | `zikaron_knowledge_status` (or `zikaron knowledge status <name>`) — a corpus **refreshing** answers from what is already indexed, but one building for the first time answers nothing until it finishes. `reindex_required` means it has no usable index **right now**: never built, or a rebuild emptied it and was interrupted before it finished, or its database is gone, or the configured embedding model changed since it was built. `refresh` rebuilds it |
| knowledge results look wrong for the file on disk | the index has drifted; nothing refreshes it on a schedule. A result whose file changed since indexing is marked `stale` — run `refresh` |
| "Agents not available for crew stages: zikaron-consolidator" | add it to `toolsSettings.crew.availableAgents`, or re-run the installer |
| consolidation seems stuck | ask to consolidate again — that takes the run over |
| the service will not start | `.zikaron/service.log`; a change to a **hard** store-coupled key (`embed_model`, `embed_dim`) is the usual cause. A soft one never refuses **over what is already stored** — but any key set outside its range refuses at startup, whatever its coupling |
| the service is running but stops answering | `.zikaron/service.log`'s last `long request:` lines name the request it is stuck in; `kill -USR2 <pid>` adds the requests in flight and every task's stack, and `kill -USR1 <pid>` every thread's stack if the loop itself is blocked (`pgrep -af zikaron.service.main` gives the pid). A `memory_plan_groups` or `memory_next_group` under five minutes is a plan, not a wedge. `hook.log` says which kind it is: **`transport`** on every push, though the socket connects, is a blocked loop; **`deadline_passed`** — or `unanswered`, when the answer lost the race to the hook's deadline — on every push with **no `call` rows** behind them in the event table is a request holding the store's writer, and the same run **with** `call` rows is every read connection taken; **`unanswered`** alone is a push whose own request hangs. One kind looks healthy in `hook.log`: pushes succeed while every write answers `store_busy` after about five seconds — the writer's lock held with nothing running — and the `long request:` line names that one too |
| Zikaron's tools are listed but arrive name-only, or the agent's first calls are refused for argument shape | Claude Code: an install predating `alwaysLoad`, which `zikaron doctor` reports. Upgrade Zikaron where the project was installed from and re-run the installer there; the entry upgrades in place and needs no `--force` |
| a hook command "not found" | the config names a different virtualenv than the one you installed from; re-run the installer with `--force` |
| a memory looks half-written when injected | an over-long gist; see the note below |

**One known limit, now narrow.** kiro truncates a hook's output past a byte cap and says nothing
when it does. Gist length is bounded in *tokens*, and tokens bound neither characters nor bytes — a
single unbroken 4000-character string counts as one token — so a pathologically long gist could
once push the injected block past that cap and lose the tail of it silently. **New writes can no
longer do this:** a gist over 1,024 characters is rejected outright, which keeps a five-row block
comfortably inside every supported harness's budget. (Emoji and other characters outside the common
range count as two each, so a gist made mostly of them is capped nearer 512 — the matching count
rather than a conservative one, because the injection budget was measured to count them the same
way.) Only records written before that bound existed can still be over-long. If you see a
truncated-looking block, find that gist with `zikaron_memory_search` and amend it; the amend is
rejected until the gist is shortened.

### Secrets

**Both stores are plaintext on disk**, and retiring a memory does not erase it. The policy tells
agents never to record secrets, but nothing enforces that — treat the store as readable by anything
that can read your home directory.

**A knowledge base is the larger plaintext surface, and it works differently.** A corpus holds the
verbatim text of every file indexed into it. A secret that reaches Zikaron inside an indexed *file*
was never recorded by an agent and is not in the memory store, so neither remedy below applies:
`zikaron_knowledge_remove` destroys that corpus's index, and the file itself is yours to fix.

**If a secret does get recorded in a memory, do not just delete the row.** The full-text index does
not hold its own copy of the text, so deleting a content row on its own leaves that row's terms
behind: the index then returns hits for a row that no longer exists, and the next write against it
can corrupt the database. Two safe options, in order of preference:

1. Stop the service, delete the whole store, **and delete the spill files, which are not in it**:
   `pkill -f zikaron.service.main; rm -rf .zikaron; rm -f "${XDG_RUNTIME_DIR:-/tmp}"/zikaron*/*.json`.
   You lose every memory **and every knowledge base**; the store rebuilds itself empty on the next
   session.
   **The separator is `;` rather than `&&` deliberately**: the service stops itself after
   `service.idle_timeout`, so it is usually *not* running, and `pkill` exits 1 when nothing
   matched — which with `&&` skips the deletion silently, leaving the store you believe you
   just erased.
   **The third command is the one that is easy to miss.** A consolidator group too large to return
   is written to the runtime directory rather than into your project, with the record prose in it
   verbatim, so `rm -rf .zikaron` does not reach it and a record erased from the store can survive
   there until the next reboot. That glob takes every store's spill files rather than only this
   one's, which is recoverable rather than free: a consolidation still in flight fails its next
   read loudly and is re-served the group with a fresh copy. It is a Claude Code path — nothing
   spills under kiro.
2. Follow the ordered procedure in `design/write-policy.md` §"The emergency erasure procedure,
   exactly", which removes the index terms and vectors before the content row.

Either way the secret is probably still live in its own original home — rotate it rather than only
deleting the copy.

## Uninstall

From the project directory, first the part that is the same on both harnesses:

```bash
pkill -f zikaron.service.main    # or just let it idle out
rm -rf .zikaron                  # the memory store, every knowledge base, and the logs
rm -f "${XDG_RUNTIME_DIR:-/tmp}"/zikaron*/*.json   # Claude Code only: spilled consolidator groups
```

The third line is there for the reason [Secrets](#secrets) gives: spill files sit outside the
project, so `rm -rf .zikaron` does not reach them.

Then the harness's own artefacts. Under **kiro**:

```bash
rm -rf .kiro/skills/zikaron-consolidate
rm .kiro/agents/zikaron-consolidator.json
mv .kiro/agents/<your-agent>.json.bak .kiro/agents/<your-agent>.json
```

The last line restores the backup the installer made. If you would rather keep later edits to that
config, remove the `hooks`, `mcpServers` and `@zikaron` entries by hand instead.

Under **Claude Code** the artefacts are project files rather than one merged config:

```bash
rm .claude/agents/zikaron-consolidator.md
rm -rf .claude/skills/zikaron-consolidate
```

and then remove Zikaron's `hooks`, `enabledMcpjsonServers` and `permissions` entries from
`.claude/settings.local.json`, and its two servers from `.mcp.json`. Both files are edited rather
than deleted, because both hold settings that are not Zikaron's. **All three settings keys, because
the install writes three** — leaving `enabledMcpjsonServers` behind names two servers that no longer
exist, which loads nothing and breaks nothing but is not an uninstall.

Note that `rm -rf .zikaron` takes the knowledge bases with it. They are rebuildable — an index is a
view onto files you still have — but rebuilding one takes minutes per corpus.

**The embedding model is not in your project and survives all of the above.** It is per user, not per
project, so it is deliberately left alone by a single project's uninstall — another project using
Zikaron still needs it. Remove it only when you are done with Zikaron everywhere:

```bash
rm -rf "${XDG_CACHE_HOME:-$HOME/.cache}"/zikaron          # Linux
rm -rf ~/Library/Caches/zikaron                           # macOS
```

That is the 64 MB the first search downloaded. If you set `$FASTEMBED_CACHE_PATH` yourself, it is
there instead. Finally, `uv tool uninstall zikaron` — or delete the virtualenv, if you installed from
a source checkout.

## Design and development

The full design record is in [`design/`](design/README.md) — start with `design/overview.md`, which
carries the decision table and the reasoning behind every choice above.
`design/coding-standards.md` is binding for contributions. `./check.sh` is the per-edit gate and the
definition of done for a change — formatter, linter, `mypy --strict`, and the hermetic test suite
under a coverage floor, with three marker tiers (`manual`, `integration_kiro`, `integration_claude`)
deselected by default and run by name. `./check-matrix.sh`, which runs all of that once per tested
Python version, is additionally required before a milestone lands.

**CI is not a third thing to run.** `.github/workflows/check.yml` asserts the matrix's claim —
every tested version green on one tree — against a commit, by running `check.sh` once per version. So a
pull request needs nothing you would not already run locally. The macOS job is **required** on the same
terms as the Linux ones. `design/distribution.md` explains what it can and cannot prove — the short
version being that a CI runner has no harness binary, so a green macOS run is a statement about the
hermetic gate rather than about a live install.
