Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "gitmemory",
"version": "0.1.0",
"description": "Registers gitmemory's hook shim on PreCompact and SessionEnd, so a session is captured at the compaction boundary instead of at the watcher's next sweep, and adds a recall skill that has the agent search the captured sessions with gitmemory recall and quote what was said, with the address of each line. The hook is only a doorbell, and the skill writes nothing but the search index: nothing is captured unless the gitmemory watcher is installed (uv tool install git+https://github.com/doronp/gitmemory) and running, and without the hook gitmemory is still correct.",
"version": "0.2.0",
"description": "Registers gitmemory's hook shim on PreCompact and SessionEnd, so a session is captured at the compaction boundary, and adds a recall skill that has the agent search past sessions with gitmemory recall and quote each line with its address. Neither captures anything: the gitmemory watcher must be installed (uv tool install --python 3.13 git+https://github.com/doronp/gitmemory), configured (gitmemory init claude-code) and running (gitmemory watch).",
"author": {
"name": "doronp"
},
Expand Down
107 changes: 103 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,29 @@ tell you.

## [Unreleased]

## [0.2.0] - 2026-10-18

### Added

- **`gitmemory init claude-code`** (or `pi`) adds that agent's standard
`[[watch]]` table to `config.toml`, so setup no longer means writing TOML by
hand. Watch roots still have no default: the root is written only because you
named the agent. It appends without changing a byte already there, because
the file also holds the `[remote.*]` push opt-ins. It does nothing if the
table is already there, and it refuses, writing nothing and exiting 1, a root
the watcher would skip or another agent already watches, a `config.toml`
that is a symlink or a hard link, and one another `init` changed while it
ran. See
[docs/watching.md](docs/watching.md#configure-what-to-watch).
- **`gitmemory service install`** writes a systemd user unit (Linux) or a
LaunchAgent (macOS) that keeps `gitmemory watch` running, and prints the
commands that start it, keep it running after you log out (Linux:
`loginctl enable-linger`), check it, follow its log, restart it after an
upgrade and remove it. It starts nothing itself, and it refuses, exiting 1,
to overwrite a file that is already there. The file names the Python, the
store and git's directory outright, because a user service inherits none of
the shell's. See
[docs/watching.md](docs/watching.md#keep-it-running).
- **`recall --policy`** decides which of two disagreeing hits comes first:
`latest-user` (the user's own words newest first, then the rest), `latest`
(newest first, whoever said it), `relevance` (best match first), or your own
Expand All @@ -22,16 +43,93 @@ tell you.
compaction or from an earlier session. It runs `gitmemory index` when the
index is missing or stale, and writes nothing else. See
[docs/USAGE.md](docs/USAGE.md#put-it-back-in-front-of-the-agent). What it
reads goes to the model provider, past no redaction gate. The plugin's
version is still 0.1.0, so a copy installed from 0.1.0 does not get the skill
from `claude plugin update`; reinstall it, or wait for the next release
reads goes to the model provider, past no redaction gate; `recall --redact`
(below) masks credential shapes in it, and blocks nothing. The skill was
added after 0.1.0 without a version bump, so a copy installed from 0.1.0
lacks it; this release moves the plugin's version to 0.2.0, so
`claude plugin update` brings it
([hook/README.md](hook/README.md#as-a-claude-code-plugin)).
- **`gitmemory --version`** prints the installed release. The recall skill
runs it first and tells you when the CLI is older than the plugin; when the
CLI lacks a subcommand or a flag the skill needs, the skill stops and says
which instead of retrying without it. See
[hook/README.md](hook/README.md#as-a-claude-code-plugin).
- **`recall` marks a subagent's turns.** A turn a subagent or workflow agent
wrote prints its role as `subagent:<role>`, as in `subagent:user/text`, in
the same column, so a line has as many fields as before. `Hit.is_sidechain`
carries the mark, read per turn from the transcript's `isSidechain`. See
[docs/USAGE.md](docs/USAGE.md#find-something).
- **Main sessions only.** [docs/watching.md](docs/watching.md#main-sessions-only-claude-code)
gives a Claude Code watch pattern, `pattern = "*/*.jsonl"`, that leaves
subagent and workflow transcripts out, and what that costs: those turns are
never captured, and the dashboard's spend leaves out what they cost.
- **`recall` says when no watcher is collecting.** When a hook record has
waited in the spool for more than an hour, `recall` prints one line on
stderr before the results, naming the store and asking whether
`gitmemory watch` is running for it. The time since the last capture cannot
tell a stopped watcher from an idle one; a record no pass has drained can.
Not under `--db`, and never on a store the hook does not ring. See
[docs/watching.md](docs/watching.md#check-it).
- **`gitmemory show <address>`** prints the whole turn at an address `recall`
printed, where `recall` stops at 160 characters. It reads the store, not the
index, so it needs no index and a stale one cannot change the answer.
`--context N` (`-C N`) adds N turns either side; a turn cut by a segment
boundary comes back whole; an offset inside a turn shows that turn; a
subagent's turn is headed `subagent:<role>`. An address that is not one, or
is masked, exits 2, and one the store does not hold exits 1. The recall
skill uses it in place of reading `raw/` by hand. See
[USAGE.md](docs/USAGE.md#read-the-whole-turn).
- **`recall --redact`** and **`show --redact`** mask the credential shapes the
push gate detects in what they print, the text and the address (except, in
a session id, a key the store's lowercasing hides from its detector), as
`AKIA…[20 bytes]`; a private key is masked down to its END line, or to the
end of the text without one. `show` masks each block whole before it splits it into
lines, so a key's body goes with its header. Off by default.
`GITMEMORY_REDACT` makes it standing for both (unset, empty or `0` is off),
and the plugin's skill passes the flag to both whenever it is set to
anything but empty or `0`, and then reads nothing under the store by hand.
It masks only those shapes, and only in what the two commands print; see
[docs/USAGE.md](docs/USAGE.md#masking-credentials-in-what-it-prints) for
what it does not cover.
- **How much disk it takes.** Budget about twice what you capture, and more if
your transcripts compress poorly: `raw/` holds the bytes once, `.git/` a
compressed copy, and `index/` the searchable text again, which
`gitmemory index` rebuilds (and, while it runs, needs room for the old index
and the new). Measured at 2.3x to 3.4x on synthetic corpora; see
[docs/watching.md](docs/watching.md#how-much-disk-it-takes). ARCHITECTURE.md
said the store holds each byte exactly once, and DESIGN.md that it has zero
duplication; both are true of `raw/` only.

### Changed

- `recall` now lists the user's own words newest first by default, not best
match first. Pass `--policy relevance` for the old order. `index.search` is
unchanged.
- **Install commands name Python 3.13.** Every documented `uv tool install`
passes `--python 3.13`, so where the machine's own Python is older (Ubuntu
24.04 ships 3.12) uv builds the tool on 3.13. Left to choose, uv can settle
on the older one and refuse (it does with Python downloads off). pipx is
down to a footnote: it installs into the Python it runs under, so it needs a
3.13 interpreter. A test reads the floor from `pyproject.toml` and holds
every install line to it. See
[README.md](README.md#quickstart).
- **Sizes for people.** `watch` and `capture` say how much a pass captured as
`+392.0 MB (391995999 B)` instead of `+391995999B`, keeping the exact count in
parentheses; under 1000 bytes it is `+N B`. A script that parsed `+NB` out of
either line needs the new form. `size=` stays an exact offset, and the commit
subject keeps `+NB`. See [USAGE.md](docs/USAGE.md#set-it-up-once).
- The watcher's warning for a root inside the store now reads
`root <r> is inside the store; skipped`. A root that contains the store
still says `contains the store itself`. `gitmemory init` prints the same
sentence as its reason when it refuses such a root.

### Fixed

- **`latest-user` no longer takes a subagent's instructions for the user's
words.** A subagent's first turn is user-role text that the parent agent
wrote, its dispatch prompt, and while it was newer than the user's rule it
led the default ahead of that rule. It now ranks with the agent's turns.
`index.search` order is unchanged.

## [0.1.0] - 2026-09-28

Expand Down Expand Up @@ -69,5 +167,6 @@ The first public release. What it contains:
- Two review rounds before publication: E7 (68 findings) and E7b (22 findings).
See [SECURITY.md](SECURITY.md).

[Unreleased]: https://github.com/doronp/gitmemory/compare/v0.1.0...HEAD
[Unreleased]: https://github.com/doronp/gitmemory/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/doronp/gitmemory/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/doronp/gitmemory/releases/tag/v0.1.0
16 changes: 16 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,22 @@ is supported when `tests/conformance.py::check_adapter` passes on a public
corpus of that agent's transcripts. Say in the PR where the corpus comes from
and what its licence is.

## Bumping the version

Claude Code keeps an installed plugin's copy until the plugin's version
changes, so a change under `skills/`, `hooks/` or `.claude-plugin/`, or to
`hook/gitmemory-hook.sh`, reaches nobody who installed the plugin until the
version is bumped. A bump moves five places in one commit: `version` in
`pyproject.toml`, `__version__` in `src/gitmemory/__init__.py`, `version` in
`.claude-plugin/plugin.json`, the release `skills/recall/SKILL.md` says it is
written for, and `uv.lock`, which records the project's version: run `uv lock`
after editing `pyproject.toml`, or CI's `uv sync --locked` fails. The tests pin
the first four equal. The same commit turns CHANGELOG.md's `[Unreleased]` into
the release's heading, with its date and a compare link from the previous tag,
and starts `[Unreleased]`'s link at the new one. Once the commit is on main,
tag it `v<version>` and push the tag: hook/README.md tells users to pin both
installs to that tag, and a pin to a tag that does not exist installs nothing.

## Pull requests

- Keep each PR to one change. Explain the *why* in the description; the diff
Expand Down
50 changes: 41 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,40 @@ dependencies.
Requires Python 3.13+ and git.

```sh
uv tool install git+https://github.com/doronp/gitmemory # or: pipx install git+https://…
uv tool install --python 3.13 git+https://github.com/doronp/gitmemory
```

Tell it what to watch. Watch roots have no default, and the config lives in
`$GITMEMORY_HOME/config.toml` (by default `~/.gitmemory`):
`--python 3.13` has uv build the tool on 3.13, fetching one where the machine's
own Python is older (Ubuntu 24.04 ships 3.12); left to choose, uv can settle on
the older one and refuse. Where uv fetches Python only when asked
(`python-downloads = "manual"`), run `uv python install 3.13` first; where it
never does (`"never"`), put a Python 3.13 on `PATH`.[^pipx]

[^pipx]: pipx installs into the Python it runs under, so it needs a 3.13
interpreter: `pipx install --python python3.13 git+https://github.com/doronp/gitmemory`.

Tell it what to watch. Watch roots have no default, so name the agent:

```sh
gitmemory init claude-code # or: gitmemory init pi
```

That adds these lines to `$GITMEMORY_HOME/config.toml` (by default
`~/.gitmemory`):

```toml
# added by `gitmemory init claude-code`
[[watch]]
agent = "claude-code"
roots = ["~/.claude/projects"]
```

It appends without changing a byte already there, does nothing if the table is
already there, and refuses a root the watcher would skip. To write the table
yourself, or for another agent or root,
[docs/watching.md](docs/watching.md#configure-what-to-watch) has the rest of
the keys.

Then:

```sh
Expand All @@ -89,25 +111,35 @@ agent search the store and quote what it finds; the watcher still does the
capturing. For watcher flags and troubleshooting, see
[docs/watching.md](docs/watching.md).

Budget disk for about twice what the store captures, and more if your
transcripts compress poorly: git keeps a compressed second copy, and the
search index holds the text again. `gitmemory index` rebuilds the index if
you delete it; rebuilding over an existing one briefly needs room for both.
[docs/watching.md](docs/watching.md#how-much-disk-it-takes) has the
measurements.

**What to do with it next** — reading `recall` output, handing it back to an
agent after compaction, derived key ideas, the dashboard — is in
[docs/USAGE.md](docs/USAGE.md).

| Command | What it does |
|---|---|
| `gitmemory init <agent>` | Adds the standard watch table for `claude-code` or `pi` to `config.toml`; does nothing if it is already there |
| `gitmemory watch` | Tails the configured roots, captures what grew, and commits |
| `gitmemory service install` | Writes a systemd user unit (Linux) or a LaunchAgent (macOS) that keeps `watch` running, and prints the line that starts it; it starts nothing itself ([docs/watching.md](docs/watching.md#keep-it-running)) |
| `gitmemory capture <file>` | Copies out one transcript's new bytes, by hand |
| `gitmemory verify` | Checks every manifest's contiguity proof |
| `gitmemory index` | Rebuilds the SQLite FTS5 index from the store |
| `gitmemory recall "<query>"` | Searches the index and prints one line per turn, dated, the user's own words newest first ([`--policy`](docs/USAGE.md#when-memories-disagree)) |
| `gitmemory recall "<query>"` | Searches the index and prints one line per turn, dated, the user's own words newest first ([`--policy`](docs/USAGE.md#when-memories-disagree); [`--redact`](docs/USAGE.md#masking-credentials-in-what-it-prints) masks credentials) |
| `gitmemory show <address>` | Prints the whole turn at an address `recall` printed, read from the store; `--context N` adds N turns either side, `--redact` masks credentials ([more](docs/USAGE.md#read-the-whole-turn)) |
| `gitmemory derive [--graph]` | Rebuilds key ideas and a timeline; `--graph` adds the decision graph (opt-in) |
| `gitmemory dashboard` | Serves the index with Datasette, read-only, on loopback, behind a sign-in |
| `gitmemory push` | Runs the redaction gate over what a push would send (it does not send yet) |

Optional extras: `hybrid` (what the benchmarks' dense and rerank arms need;
`recall` does not use them), `derive` (key ideas and the graph), and
`serve` (the dashboard). Example:
`uv tool install "gitmemory[serve] @ git+https://github.com/doronp/gitmemory"`.
`uv tool install --python 3.13 "gitmemory[serve] @ git+https://github.com/doronp/gitmemory"`.

## Results

Expand Down Expand Up @@ -185,13 +217,13 @@ real text, and that is a measurement rather than a suspicion.
| What | How it was measured | Result |
|---|---|---|
| Hook cost in the agent's critical path | Timed against spawning `true` the same way, three runs of 400 | p50 **7.4 – 7.5 ms**, p99 **10.2 – 11.5 ms** |
| The suite | On a fresh checkout, no downloads | **1,037 tests**, and **328 conformance cases** against three third-party corpora, one gated on the LongMemEval download and one on `pip install -e '.[serve]'` |
| Whether the tests hold anything | Every fix mutated to remove its behaviour; the named test must fail | **606** negative controls |
| The suite | On a fresh checkout, no downloads | **1,134 tests**, and **328 conformance cases** against three third-party corpora, one gated on the LongMemEval download and one on `pip install -e '.[serve]'` |
| Whether the tests hold anything | Every fix mutated to remove its behaviour; the named test must fail | **662** negative controls |

The two suite counts do not add up, and should not. Switching the corpora on
collects 1362, not 1365. Three conformance cases fill parametrisations that
collects 1459, not 1462. Three conformance cases fill parametrisations that
collect as one empty placeholder each while the corpora are absent, so they
replace three of the 1037 rather than joining them. Every figure on this board
replace three of the 1134 rather than joining them. Every figure on this board
is pinned by a test, which is how it stays true.

**What is not measured**, and is shown as a row on the dashboard rather than
Expand Down
24 changes: 18 additions & 6 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ maintained back-branches.

In scope: anything in this repository that runs on a user's machine. That
means capture, the store and its contiguity proof, `verify`, the redaction gate
behind `push`, the index, `derive`, the dashboard, the watcher, the hook shim and the plugin's
behind `push`, the masking behind `recall --redact` and `show --redact`, the
index, `derive`, the dashboard, the watcher, the hook shim and the plugin's
recall skill.

Worth reporting, for example:
Expand All @@ -47,7 +48,9 @@ Worth reporting, for example:
`derive` crash, hang, write outside `$GITMEMORY_HOME`, or cost superlinear time
- a way to make `verify` accept a store whose segments do not tile, or whose
bytes do not match the recorded digest
- a secret that the redaction gate lets through on `push`
- a secret that the redaction gate lets through on `push`, or that
`recall --redact` or `show --redact` prints unmasked, of a shape its
detectors are meant to catch
- the dashboard being reachable without the sign-in URL, or writable

Out of scope: vulnerabilities in the third-party dependencies themselves
Expand Down Expand Up @@ -90,7 +93,16 @@ that gets waved through on a deadline.

The gate stands at `push` and nowhere else. The Claude Code plugin's recall
skill reads the store into the agent's context, so each line it quotes goes to
the model provider with the rest of the conversation, unscanned. That includes
text another watched agent never sent to that provider, and a credential `push`
would refuse to ship. The skill quotes what the question needs and no more,
which is a rule it follows, not a gate.
the model provider with the rest of the conversation, unscanned unless you turn
on the masking below. That includes text another watched agent never sent to
that provider, and a credential `push` would refuse to ship. The skill quotes
what the question needs and no more, which is a rule it follows, not a gate.

`recall --redact` and `show --redact` mask the gate's shapes in what those two
commands print. `GITMEMORY_REDACT` set to anything but empty or `0` turns it on
for both, and the skill then passes the flag to both and reads nothing under
the store by hand. It masks and never refuses, and it is off unless you turn it
on. What it does not guarantee is listed in
[USAGE.md](docs/USAGE.md#what-it-does-not-guarantee): it masks sixteen shapes
and nothing else, decodes nothing, and covers only what those two commands
print.
Loading
Loading