π
Recall β fully-local project memory for Claude Code
> Claude Code starts every session cold. Recall keeps a local log of your
> sessions and condenses it into a resume-ready summary β **entirely on your
> machine**. No API key, no external model, nothing sent anywhere. It's built for
> people running Claude Code locally on a subscription: the only AI in the loop is
> Claude Code itself; the summarization is done by a classical Python summarizer.
## Why Recall
- **Free on your subscription.** It solves the cold-start problem β no more
re-explaining the project each session β without a metered summarizer running up
a bill. The summary is a local algorithm, not an LLM call, so persistent memory
costs you nothing beyond the subscription you already pay for.
- **Saves your usage credits.** Two ways: (1) the summary is built locally, so
capturing and updating your memory spends **zero** model tokens; and (2)
resuming from a compact `context.md` (~1β2K tokens) instead of re-explaining the
project from scratch each session means far fewer tokens spent per session β
stretching your subscription's usage limits (or, on the API, lowering billed
credits).
- **Nothing leaves your machine.** Your transcripts (code, paths, sometimes
secrets) are never sent to any API. Most "memory" tools pipe your context to a
model endpoint; Recall makes a privacy guarantee they can't. See
[PRIVACY.md](PRIVACY.md) for the full policy.
- **Zero-friction.** No `pip install`, no local model to run, no key to configure,
works offline. It starts working the moment the plugin loads.
Two files, written into your project under `.recall/`:
- **`history.md`** β *the log.* Append-only. Every session is captured here as it
happens (your prompts, Claude's replies, the files touched and commands run).
- **`context.md`** β *the summary.* Overwritten by the local summarizer β the
condensed "where are we right now" you load into the next session: goal,
summary, **next steps / open threads**, files touched, and where you left off.
## "Doesn't Claude Code already have memory?"
It does β and Recall is complementary, not a replacement. The built-in options
solve different problems:
- **`CLAUDE.md` (and the `#` shortcut)** is *hand-written* memory: rules and notes
**you** curate, loaded as **instructions Claude follows**. Great for "how I want
you to work," but it's manual upkeep and it doesn't record what actually
happened in a session.
- **`--continue` / `--resume`** replays a *prior conversation* β full fidelity, but
it reloads the whole transcript (token-heavy) and is tied to your local session
history on one machine, not a portable, readable digest.
- **Context compaction** condenses a conversation *within* a session; it isn't a
durable record you reopen days later.
Recall fills the gap between these: an **automatic, deterministic record of what
each session did**, condensed into a compact resume point.
| | `CLAUDE.md` / `#` | `--continue` / `--resume` | **Recall** |
|---|---|---|---|
| What it is | Hand-written notes & rules | Reloads a prior conversation | Auto-captured session log + local summary |
| Upkeep | Manual | None (you pick the session) | None β written as you work |
| Holds | Instructions to follow | The full prior transcript | Goal, files, commands, where you left off, next steps |
| Cost to resume | Small | Large (replays full transcript) | ~1β2K tokens (compact digest) |
| Form | Markdown you edit | Local session state | Plaintext in `.recall/` β diffable & shareable |
| How Claude treats it | As instructions | As the conversation | Fenced as **untrusted reference data** |
In short: `CLAUDE.md` is *how I want you to work*; Recall is *here's what we did
last time and where we stopped* β produced offline, with no model tokens spent.
## How it works
| Moment | What happens |
|---|---|
| **During the session** | The `Stop` / `SessionEnd` hooks append new activity to `.recall/history.md`. Capture is incremental (only new turns) and fully local. |
| **At session start** | The `SessionStart` hook surfaces `context.md` and has Claude ask you two things: resume from the saved context? and keep logging this session? |
| **Before you wrap up** | You run `/recall:save`. The **local summarizer** reads `history.md` and (over)writes `context.md`. |
| **β¦or automatically** | Set `auto_save_context: "on_end"` and `context.md` regenerates every time a session ends β no `/recall:save` needed. |
There is no LLM call anywhere β the summary is produced by **TF-IDF + TextRank**
(extractive summarization) running locally.
## The summarizer
`scripts/summarizer.py` ranks the most central sentences of your session:
1. TF-IDF sentence vectors
2. a cosine-similarity graph between sentences
3. **TextRank** β PageRank power iteration over that graph β to score sentences
4. the top *N* are kept in original order
`context.md` wraps that summary with deterministic facts pulled straight from the
transcript and git: the goal (your first ask), files touched, commands run, where
you left off, and `git diff --stat`.
**No installs required.** The whole TF-IDF + TextRank implementation is vendored
in `summarizer.py`. If `numpy` happens to be importable it's used to vectorize the
math (faster on big sessions); if not, an identical pure-Python TextRank runs
instead. Same algorithm, same result β numpy is an optional accelerator, never a
requirement. The save output tells you which path ran.
## Commands
- `/recall:save` β run the local summarizer β (over)write `context.md`.
- `/recall:show` β print `context.md`.
- `/recall:log` β tail `history.md`.
## Configuration β `recall.config.json`
Drop this in your project root to override defaults:
| Key | Default | Purpose |
|---|---|---|
| `output_dir` | `".recall"` | Where `history.md` / `context.md` live. |
| `capture_history` | `true` | Append session activity to `history.md`. |
| `auto_save_context` | `"off"` | Regenerate `context.md` when a session ends: `"off"` or `"on_end"`. |
| `summary_sentences` | `8` | How many sentences the summary keeps. |
| `redact` | `true` | Strip obvious secrets before writing the md files. |
| `include_git` | `true` | Add `git diff --stat` + recent commits to `context.md`. |
| `max_input_chars` | `200000` | Cap on text fed to the summarizer (oldest dropped). |
**Pause logging** for a project without editing config: create
`.recall/.capture-paused`. Delete it to resume.
## Privacy & security
Recall makes **no network calls, uses no API key, and loads no third-party
model.** The summarizer is local Python; the hooks are stdlib-only (numpy is an
optional accelerator). It reads your session transcript and writes only under
`output_dir`. Concretely:
- **No credentials, ever.** The plugin has zero references to API keys, auth,
`ANTHROPIC_*`, or HTTP. If `claude` itself shows *"Invalid API key"*, that's the
CLI's own auth β usually a stale `ANTHROPIC_API_KEY` env var shadowing your
subscription login. `unset ANTHROPIC_API_KEY` (or run `env -u ANTHROPIC_API_KEY
claude β¦`). It has nothing to do with Recall.
- **Redaction.** A best-effort pass strips common secret shapes (API keys, tokens,
`.env` assignments, PEM keys) before writing, since `context.md` / `history.md`
may be committed. Best-effort, not a guarantee β review before committing.
- **Hardened git.** `git diff`/`log` are run with `core.fsmonitor`,
`diff.external`, hooks, and the pager disabled, so an untrusted cloned repo
can't use its own git config to execute code when Recall reads ground-truth.
Set `include_git: false` to skip git entirely.
- **Confined writes.** `output_dir` is forced to stay inside the project; a
project-shipped config can't redirect writes to an absolute path or `../..`.
- **Scoped transcript.** Recall only reads the transcript for the current
project (matched by cwd); it never falls back to another project's sessions.
- **Trust boundary for shared memory.** `context.md` is injected into the model at
session start. If you **commit `.recall/` as shared team memory**, treat it
like any other shared input: a teammate (or a bad actor with repo write access)
could craft a `context.md` to attempt prompt-injection. SessionStart fences the
content and labels it untrusted data, and Claude asks before relying on it β but
if you don't fully trust who can write the repo, keep `.recall/` git-ignored
(the default).
## Committing `.recall/`
Both are fine. Commit it for shared team memory, or git-ignore it for personal
memory (`.gitignore` ships ignoring it by default β flip the comment to commit).
## Install
**From the marketplace** (this repo is its own marketplace):
```
/plugin marketplace add raiyanyahya/recall
/plugin install recall@recall
```
**Local dev** (no install step):
```
claude --plugin-dir /path/to/recall
```
No `pip install` β the summarizer is vendored and stdlib-only (numpy used as an
optional accelerator if present). Work a session, run `/recall:save`, and open
a fresh session β Recall greets you with where you left off.
## Other harnesses
Claude Code is Recall's first-class harness β the plugin install above is the
primary, fully supported path, and nothing in this section affects it. But the
memory Recall writes is plain markdown in `.recall/`, not harness state, so
other coding agents can produce and consume the very same files. Harness
support is always **opt-in**: nothing activates unless you explicitly set it
up. A nice consequence: memory is **shared across harnesses** β a Claude Code
user and an opencode user on the same repo write to the same `history.md`,
and either side's `context.md` resumes the other's work.
### OpenCode
**How plugin installs work in opencode.** Unlike Claude Code, [opencode](https://opencode.ai)
has no plugin marketplace and no in-CLI install command β there's no
`/plugin install` to run inside it. Plugins are loaded from files and config:
anything in a project's `.opencode/plugins/` (or the global
`~/.config/opencode/plugins/`) is auto-loaded at startup, custom commands are
markdown files in `.opencode/commands/`, and session-start context comes from
the `instructions` list in `opencode.json`. Recall plugs into exactly those
mechanisms β a small installer generates the files **once**, and opencode
picks them up automatically from then on.
**Set it up once per project** β not per session, not per machine boot:
```
git clone https://github.com/raiyanyahya/recall ~/recall
python3 ~/recall/scripts/install.py --opencode --project /path/to/your/project
```
Keep the clone around β the generated plugin points at it. The only reason to
ever re-run the installer is if you move the clone (or want to uninstall).
The installer writes exactly three things, all inside the target project:
| File | Purpose |
|---|---|
| `.opencode/plugins/recall.ts` | the capture shim, auto-loaded by opencode at startup |
| `.opencode/commands/recall-save.md` | the `/recall-save` command |
| `opencode.json` | adds `.recall/context.md` to `instructions` |
**From then on, every session is automatic:**
- **At session start**, opencode loads `.recall/context.md` (via
`instructions`) β you resume where you left off, no re-explaining.
- **After every turn** (opencode's `session.idle` event), the shim appends new
activity to `.recall/history.md`. It reads the session through
`opencode export` β opencode's public CLI β never its internal storage.
- **Before wrapping up**, run `/recall-save` to regenerate `context.md` with
the local summarizer β or set `auto_save_context: "on_end"` in
`recall.config.json` and it regenerates after every turn on its own.
Same guarantees as the Claude Code path: fully local, no network, no API key;
capture honors `recall.config.json`, `.capture-paused`, and redaction; any
failure is a silent no-op so a session is never affected. Uninstall anytime
with `--opencode --uninstall` (your `.recall/` data stays), and the installer
never overwrites or deletes a file it didn't generate.
Two differences from the Claude Code path to be aware of:
- **Instructions, not fenced data.** opencode loads `context.md` through its
`instructions` mechanism, without the untrusted-data fencing the Claude Code
SessionStart hook applies. If you commit `.recall/` as shared team memory,
only do so with collaborators you trust.
- **Tracks opencode's public CLI** (`session list`, `export`, the plugin
events API), which is the stable surface β but opencode moves fast; if a
release changes these, capture degrades to a silent no-op. File an issue.
### Codex and others
Not supported yet. The adapter seam (`scripts/harness_opencode.py`,
`--harness` on `make_context.py`) is where a new harness plugs in β
contributions welcome.
## Development
```bash
python -m venv .venv && . .venv/bin/activate
pip install pytest ruff bandit numpy # numpy optional
ruff check scripts tests benchmarks # lint
bandit -c pyproject.toml -r scripts # security static analysis
pytest # run the suite (also test without numpy)
python benchmarks/bench.py # perf + quality numbers (human-readable)
python benchmarks/bench.py --check # assert quality invariants (the CI gate)
claude plugin validate . # official manifest validation
```
`benchmarks/bench.py` is a stdlib-only harness: alongside latency/throughput it
scores the summarizer's salient-sentence selection against lead/tail/random
baselines on a labeled fixture set and checks the numpy and pure-Python cores
select the **same** sentences. `--check` gates those quality invariants (it never
gates wall-clock timings). Redaction quality is covered by the unit suite
(`tests/test_redact.py`), so no secret-shaped fixtures live in the benchmark.
CI (`.github/workflows/`) runs lint + Bandit, the test suite across Python
3.9β3.13 **with and without numpy** (both summarizer paths), the benchmark
quality gate (both paths), CodeQL, secret scanning, and manifest JSON validation
on every push and PR. See
[CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md).
## Layout
```
recall/
βββ .claude-plugin/plugin.json # manifest
βββ hooks/hooks.json # SessionStart (ask/resume) Β· Stop+SessionEnd (capture)
βββ commands/ # /recall:save Β· show Β· log
βββ scripts/
β βββ summarizer.py # vendored TF-IDF + TextRank (numpy optional)
β βββ make_context.py # build/overwrite context.md (--harness claude|opencode)
β βββ capture.py # append session activity to history.md
β βββ session_start.py # surface context + ask the start questions
β βββ parse_transcript.py # transcript β events + renderers
β βββ harness_opencode.py # opencode adapter (public CLI only) + opencode_capture.py
β βββ install.py # flag-based installer for opt-in harnesses (--opencode)
β βββ config.py Β· common.py Β· redact.py
βββ integrations/opencode/ # generated-file templates (plugin shim, /recall-save)
βββ tests/ # pytest suite (summarizer, capture, security, β¦)
βββ benchmarks/bench.py # perf + quality harness (CI quality gate)
βββ .github/ # CI, CodeQL, secret scan, dependabot
βββ recall.config.json # config template / defaults
βββ pyproject.toml # ruff / pytest / bandit config (no runtime deps)
βββ LICENSE Β· SECURITY.md Β· CONTRIBUTING.md
βββ .gitignore
```
## Contributing & issues
Bugs and ideas are welcome β open an [issue](https://github.com/raiyanyahya/recall/issues/new/choose)
(bug-report and feature templates provided) or a pull request. See
[CONTRIBUTING.md](CONTRIBUTING.md) before submitting, and report security
vulnerabilities privately per [SECURITY.md](SECURITY.md) rather than in a public issue.