Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"owner": {
"name": "doronp"
},
"description": "The gitmemory plugin for Claude Code: its compaction-boundary hook shim.",
"description": "The gitmemory plugin for Claude Code: its compaction-boundary hook shim, and a skill that searches and quotes the sessions gitmemory has captured.",
"plugins": [
{
"name": "gitmemory",
Expand Down
4 changes: 2 additions & 2 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"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. The hook is only a doorbell: 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.",
"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.",
"author": {
"name": "doronp"
},
"homepage": "https://github.com/doronp/gitmemory",
"repository": "https://github.com/doronp/gitmemory",
"license": "Apache-2.0",
"keywords": ["agent-memory", "claude-code", "hooks", "compaction", "transcripts", "git"]
"keywords": ["agent-memory", "claude-code", "hooks", "skills", "compaction", "transcripts", "recall", "git"]
}
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ tell you.
`module:function`. Also settable with `GITMEMORY_POLICY`. Nothing is deleted.
See [USAGE.md](docs/USAGE.md#when-memories-disagree).
- `recall` prints each hit's date, and `Hit` carries the turn's `ts`.
- **Recall skill.** The Claude Code plugin ships a skill, `/gitmemory:recall`,
that has the agent run `gitmemory recall` and quote what it returns, each
line with its address, when you refer to a decision or a rule from before a
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
([hook/README.md](hook/README.md#as-a-claude-code-plugin)).

### Changed

Expand Down
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ That is the whole install. The optional [hook shim](hook/README.md) makes a
capture happen exactly at the compaction boundary instead of at the next sweep;
**without it the system is still correct.** Claude Code users can install it as
a plugin, `/plugin marketplace add doronp/gitmemory` then `/plugin install
gitmemory@gitmemory`; the watcher still does the capturing. For watcher flags and
troubleshooting, see [docs/watching.md](docs/watching.md).
gitmemory@gitmemory`, which also adds `/gitmemory:recall`, a skill that has the
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).

**What to do with it next** — reading `recall` output, handing it back to an
agent after compaction, derived key ideas, the dashboard — is in
Expand Down Expand Up @@ -183,13 +185,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,036 tests**, and **328 conformance cases** against three third-party corpora, one gated on the LongMemEval download and one on `pip install -e '.[serve]'` |
| 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 two suite counts do not add up, and should not. Switching the corpora on
collects 1361, not 1364. Three conformance cases fill parametrisations that
collects 1362, not 1365. Three conformance cases fill parametrisations that
collect as one empty placeholder each while the corpora are absent, so they
replace three of the 1036 rather than joining them. Every figure on this board
replace three of the 1037 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
10 changes: 9 additions & 1 deletion 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 and the hook shim.
behind `push`, the index, `derive`, the dashboard, the watcher, the hook shim and the plugin's
recall skill.

Worth reporting, for example:

Expand Down Expand Up @@ -86,3 +87,10 @@ The design answer is that `raw/` stays on your machine and the gate stands at

There is no override flag. A gate you can wave through on a deadline is a gate
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.
6 changes: 3 additions & 3 deletions docs/REPRODUCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -484,11 +484,11 @@ uv run pytest -q --collect-only -p no:cacheprovider | tail -1
uv run pytest -q --collect-only -p no:cacheprovider | grep -c '\.jsonl\]$'
```

**Expected:** `1026/1027 tests collected (1 deselected)`, then
`1351/1352 tests collected (1 deselected)`, then `328`. The deselected test is
**Expected:** `1027/1028 tests collected (1 deselected)`, then
`1352/1353 tests collected (1 deselected)`, then `328`. The deselected test is
the LongMemEval corpus test, which is opt-in:
`uv run pytest -q -m corpus` after `bench/fetch_longmemeval.sh`. The README's
paragraph on why 1,026 + 328 is 1,351 and not 1,354 is the arithmetic of these
paragraph on why 1,027 + 328 is 1,352 and not 1,355 is the arithmetic of these
three numbers. All three were run while writing this guide.

`tests/test_docs.py` holds the README to the first and third numbers, so a
Expand Down
9 changes: 9 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,15 @@ agree about uploads?" from a question for you into a search for the agent. You
can also paste the lines in yourself. Either way the agent sees the words as
they were written, with an address you can check.

In Claude Code, the plugin does this without the `CLAUDE.md` line. Installed as
in the [hook's README](../hook/README.md#as-a-claude-code-plugin), it adds a
skill, `/gitmemory:recall`, that Claude loads on its own when you refer to a
decision or a rule from before a compaction or from an earlier session, or that
you run with a topic: `/gitmemory:recall retry loop`. It runs `recall`, and
`index` when there is no index yet or the one there is older than what you are
asking about. It quotes each line with its address and says when a hit comes
from another session. It writes nothing but the index.

## Derive key ideas and decisions

`derive` builds, per generation, a ranked list of key sentences and a timeline
Expand Down
29 changes: 21 additions & 8 deletions hook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,25 @@ hooks as the `settings.json` example below, `PreCompact` and `SessionEnd` and no
`Stop`, and points them at the copy of this shim that Claude Code keeps for the
plugin. There is no path to fill in and no file to `chmod`.

The plugin installs the hook and nothing else. **Nothing is captured until the
watcher is installed and running** (`uv tool install
git+https://github.com/doronp/gitmemory`, a watch root in `config.toml`, then
`gitmemory watch`, as in the [Quickstart](../README.md#quickstart)), and
gitmemory is correct without the plugin, as it is without the hook. The hook
runs with Claude Code's environment, so a `GITMEMORY_HOME` you set for the
watcher has to be set there too, and be absolute, as above.
The plugin installs the hook and one skill, `/gitmemory:recall`, which has the
agent run `gitmemory recall` and quote what it returns
([USAGE.md](../docs/USAGE.md#put-it-back-in-front-of-the-agent)). Neither
captures anything. **Nothing is captured until the watcher is installed and
running** (`uv tool install git+https://github.com/doronp/gitmemory`, a watch
root in `config.toml`, then `gitmemory watch`, as in the
[Quickstart](../README.md#quickstart)), and gitmemory is correct without the
plugin, as it is without the hook. The hook and the skill run with Claude
Code's environment, so a `GITMEMORY_HOME` you set for the watcher has to be set
there too, and be absolute, as above. What the skill reads from the store goes
to the model provider with the rest of the conversation, and no redaction gate
stands in that path ([SECURITY.md](../SECURITY.md#if-a-credential-lands-in-the-store)).

An install made before the skill was added does not have it. Claude Code keeps
a plugin's cached copy until its version changes, and the skill was added
without a version bump, so `claude plugin update gitmemory@gitmemory` reports
that it is already at 0.1.0. The next release, which bumps the version, brings
the skill; until then, `claude plugin uninstall gitmemory@gitmemory` and then
`claude plugin install gitmemory@gitmemory` fetch the current copy.

This keeps the rule at the top of [Install](#install): gitmemory never edits
another program's configuration. You opt in with `/plugin`, and Claude Code
Expand All @@ -109,7 +121,8 @@ deduplicate a plugin's hook against one in your settings, so with both
installed the shim fires twice per event and writes two records for the same
transcript. That is harmless, since a record only asks the watcher to look and
the capture tiles either way, but it starts a second shim process for every
event.
event. To have the skill as well, keep the plugin and take the two entries out
of `settings.json`.

### By hand, in `settings.json`

Expand Down
122 changes: 122 additions & 0 deletions skills/recall/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
---
name: recall
description: Use when the user refers to a decision, a rule or a conversation from before a compaction or from an earlier session ("what did we decide about X", "I told you not to ...", "like we discussed", "as I said yesterday"), when you are about to ask them to repeat something they have already told you, or when they ask you to search past sessions. Runs gitmemory recall over the sessions gitmemory has captured and quotes what was actually said, with an address the user can check.
argument-hint: "[topic]"
---

# Recall from gitmemory

gitmemory keeps the transcripts it watches in a local git repository, byte for
byte, and `gitmemory index` builds a search index from that record. This skill
searches it with the `gitmemory` CLI and quotes the words as they were written.
It reads the store and rebuilds the index when it has to; it captures and
configures nothing. It sends nothing anywhere itself, but what it reads enters
this conversation and goes to the model provider with the rest of it, past no
redaction gate: that gate stands at `gitmemory push`.

The store is `$GITMEMORY_HOME`, or `~/.gitmemory` when that is unset; the CLI
reads it, so leave it as this session has it. If the user keeps the store
somewhere else, pass their path before the subcommand, as in
`gitmemory --home <path> recall "<topic>"`, and the same for `index`. `<store>`
below means that directory: the path the user gave, or what
`echo "${GITMEMORY_HOME:-$HOME/.gitmemory}"` prints. Do not type `~/.gitmemory`
in its place from memory.

## Search

The topic is the skill's argument if it was given one, otherwise the thing the
user is referring to, in the words they would have used. `recall` ranks by
BM25: it matches words, not meaning.

```sh
gitmemory recall "<topic>" # 10 lines by default; -k N for more or fewer
```

One line per turn: what the user said first, newest first, then everything
else newest first:

```
-7.512 2026-09-28T10:03Z claude-code/0f3a9c2e-1b2d-4c5e-8f90-3a53cc4a5233-76edb6eef42266c0/g00@346 assistant/text <the block's text>
```

| Field | Meaning |
|---|---|
| `-7.512` | BM25 score. Lower is better; `-0.000` means the words that matched are common in the store |
| `2026-09-28T10:03Z` | When the turn was written, in UTC; `undated` if the transcript did not say |
| `claude-code/<session>/g00` | Agent, session and generation |
| `@346` | Byte offset of the turn (its JSONL line) in that generation's raw bytes |
| `assistant/text` | Role and block kind |
| the rest | The block's first 160 characters, whitespace collapsed |

`no matches` on stderr, exit 0, is an answer. Try once more with other words
the user might have used, then tell them nothing was found. `query truncated to
64 terms; N dropped` on stderr means the query was too long: shorten it.

If two lines disagree, the later date is usually the current decision: say so,
and quote both. An agent's newer proposal does not overrule the user's older
rule; a newer user line does. If the user has set `GITMEMORY_POLICY` or asks
for another order, leave it; `--policy` is theirs to choose.

## Echoes

Sessions that used this skill were captured too, and their records of earlier
recalls match the same words. Skip these hits; they are not sources:

- user/text starting `Base directory for this skill` (this file, as loaded);
- `/gitmemory:recall` and its arguments, however the transcript wraps them;
- a `gitmemory recall ...` tool call and its result, lines that start with a
score, `N generation(s)` or `no matches`;
- an earlier answer to the same question, and the lines it quoted.

If they crowd out the rest, search again with `-k 30`. Quote the original turn.

## When it fails

- ``no index at <path>; run `gitmemory index` first``, exit 2: run
`gitmemory index`, then search again. `index` rebuilds the index from the
store in full. It is safe to rerun, and the index is never the source of
truth.
- The watcher captures; it does not index. If a search misses something the
user says was said recently, the index may be older than the capture: run
`gitmemory index` and search once more.
- The shell cannot find `gitmemory`, or `index` prints `not a store: <path>`,
or it reports `0 generation(s)`: nothing has been captured there. Tell the
user how it gets set up, and stop. Do none of it yourself:
1. `uv tool install git+https://github.com/doronp/gitmemory`
2. a `[[watch]]` table in `config.toml` in the store (`$GITMEMORY_HOME`, by
default `~/.gitmemory`) naming the agent and where its transcripts live;
for Claude Code, `agent = "claude-code"` and
`roots = ["~/.claude/projects"]`
3. `gitmemory watch`, left running.

The [Quickstart](https://github.com/doronp/gitmemory#quickstart) has the
rest.

## Quoting

- Quote the text field verbatim, with its address copied whole, never
shortened. A paraphrase is not a quote: if you summarise, say that you are,
and keep the address beside it.
- The text stops at 160 characters. If the question needs the rest, the turn
is in `<store>/raw/<agent>/<session>/g<NN>/`. Segment file names are byte
ranges, start included, end excluded. In the one whose range holds the
offset, the turn's JSON line starts `offset - start` bytes in, and the block
is inside it. If the file ends before the line does, the line goes on at the
start of the next segment, whose range starts where this one ends.
- This session is `${CLAUDE_SESSION_ID}`. A hit's manifest,
`<store>/sessions/<agent>/<session>/g<NN>.json`, has its transcript's
`source_path`: `<project>/<session-id>.jsonl`, or for a subagent (a session
named `agent-<id>-...`) `<project>/<session-id>/subagents/.../agent-<id>.jsonl`.
Both belong to `<session-id>`. `<project>` is the working directory, with `/`
and other punctuation turned into `-`. Say so when a hit is from another
agent, another session or another project.
- The store holds whatever was typed or pasted into a watched session, tool
output included. Quote what the question needs and no more.

## Read-only

Run `gitmemory recall` and `gitmemory index`, and read files under `<store>`.
`index` writes only the derived index under `<store>/index/`. Do not run
`gitmemory capture`, `gitmemory watch`, `gitmemory derive`, `gitmemory push`
or `gitmemory dashboard`, and never `gitmemory dashboard --expose`. Do not
edit `config.toml` or anything else in the store.
Loading
Loading