Claude Code Plugin¶
Memoir ships a first-class plugin for Claude Code. Drop it in and memoir becomes native to your coding sessions: context injected on session start, durable facts auto-captured at turn end, and a full suite of slash commands for everything in between.
The plugin lives in the repo at plugins/claude-code/.
Install¶
Inside a Claude Code session, run:
The first command registers the memoir GitHub repo as a plugin marketplace; the second installs the memoir plugin from that marketplace. Hooks take effect on the next session start.
Each project gets its own memoir store under ~/.memoir/<slug>/, derived from your cwd. Override by exporting MEMOIR_STORE=/path/to/store. If you use git worktree, all linked worktrees of one repo share a single store keyed on the main worktree's path — set MEMOIR_STORE per worktree to opt out.
What ships¶
| Component | Count | Role |
|---|---|---|
| Slash commands | 9 | Manual memory ops, admin, UI launch |
| Skills | 2 | Auto-invoked: recall + codebase onboarding |
| Lifecycle hooks | 4 | Context injection + auto-capture |
| Helper scripts | 3 | Store path, UI control, status line |
Slash commands¶
| Command | Purpose |
|---|---|
/memoir:onboard [--force] |
Populate or refresh the codebase:onboard snapshot. |
/memoir:remember <fact> |
Capture a memory. -p <path> skips classification. |
/memoir:recall <query> |
Recall from prior sessions (delegates to the memory-recall skill). |
/memoir:status |
Branch, commit count, memory count, namespaces. |
/memoir:ui |
Launch or re-open the web UI (readonly, LLM off by default). |
Admin operations (forget, taxonomy, unmerged, sync-branch) are available via the memoir CLI directly — they were dropped from the slash-command surface to keep the in-session UX focused on the five everyday actions.
Skills¶
| Skill | Namespace | Role |
|---|---|---|
memory-recall |
default |
User-captured facts. Picks taxonomy paths with summarize, batches get, never invokes nested LLMs. Runs in a forked context. Configured default on with aggressive triggering so remembered preferences are never silently skipped. |
memoir-onboard |
codebase:onboard |
Maintains a high-level repo snapshot that seeds future sessions via SessionStart injection. |
The split is deliberate: recall owns user-captured facts; onboard owns codebase structure.
Read/write asymmetry¶
By design:
- Reads are auto-triggered via skills. The agent pulls context when it thinks it might need to, without the user asking.
- Writes are an explicit slash command.
/memoir:rememberstays as a command — not a skill — because theStophook already handles auto-capture; this command is the manual escape hatch. Deletion (memoir forget) lives on the CLI rather than as a slash command, kept explicit for safety.
Lifecycle hooks¶
Configured in plugins/claude-code/hooks/hooks.json:
| Event | Script | Timeout | Async | Purpose |
|---|---|---|---|---|
SessionStart |
session-start.sh |
15s | — | Inject store status, branch/commit state, onboard snapshot, and "memory available" hints. |
UserPromptSubmit |
user-prompt-submit.sh |
10s | — | Surface matching memory hints for the current prompt. |
Stop |
stop.sh |
180s | yes | Parse the transcript and auto-capture durable facts into the taxonomy. |
SessionEnd |
session-end.sh |
5s | yes | Cleanup. |
Shared helpers: hooks/common.sh, hooks/parse-transcript.sh.
Helper scripts¶
| Script | Role |
|---|---|
derive-store-path.sh |
Maps the current cwd to ~/.memoir/<slug> (linked worktrees collapse onto the main worktree's slug). Respects $MEMOIR_STORE. |
memoir-ui-ctl.sh |
start / stop / status for the web UI, with pidfile bookkeeping so repeated /memoir:ui calls reuse the same server. |
statusline.sh |
Renders memoir state into Claude Code's status line, e.g. memoir: feature/foo · 14 memories. |
Lifecycle¶
A session flows through four hook events. Steps 2–4 loop once per user prompt; step 5 runs once at the end.
sequenceDiagram
actor You
participant Claude as Claude Code
participant Plugin as memoir plugin hooks
participant Store as memoir store
rect rgb(30, 50, 70)
Note over Claude,Store: 1. SessionStart — session-start.sh (sync)
Plugin->>Store: read status, branch/commit, codebase:onboard
Store-->>Claude: inject snapshot + "memory available" hints
end
rect rgb(30, 55, 45)
Note over You,Store: 2–4. Per user prompt (loops)
You->>Claude: prompt
Plugin->>Store: 2. UserPromptSubmit — recall determine (sync)
Store-->>Claude: "[memoir] memory available" hints
Claude->>Store: 3. memory-recall skill (forked, on-demand)
Store-->>Claude: summarize → pick prefixes → get → facts
Claude-->>You: response
Plugin->>Store: 4. Stop — classify + auto-capture (async)
end
rect rgb(60, 45, 30)
Note over Claude,Store: 5. SessionEnd — session-end.sh (async)
Plugin->>Store: cleanup
end
Two properties to notice:
- Reads happen eagerly, writes happen lazily. Every prompt passes through
UserPromptSubmit(step 2) and potentially firesmemory-recall(step 3) — the agent pulls context without the user asking. Auto-capture is deferred toStop(step 4), which is async so it never blocks the turn. - Namespaces split along read/write paths.
memory-recallworks againstdefault(user-captured facts, written by theStophook or/memoir:remember).memoir-onboardworks againstcodebase:onboard(repo snapshot, written by/memoir:onboard, replayed by theSessionStarthook). Two namespaces, two lifecycles, no overlap.
The admin surface — /memoir:ui, /memoir:status (slash commands), plus memoir taxonomy, memoir unmerged, and memoir sync-branch (CLI) — sits outside this lifecycle: it's explicit user invocation, not hook-driven.
Session context injection¶
SessionStart writes a single JSON object to stdout that Claude Code reads as the session preamble: {"systemMessage": ..., "hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": ...}}. The additionalContext block is assembled in order from up to four parts, each conditional on having something to say. Every part comes from raw memoir get/summarize calls — no LLM is invoked at session start.
| Block | Source | Always on? |
|---|---|---|
| Store summary | memoir status + memoir summarize taxonomy |
Yes — branch, user-memory count, namespace counts. |
| Default-namespace keys | memoir summarize --keys "*" -n default |
Yes (when default has keys). Capped at 200, grouped by L1 prefix. |
| Unmerged-branch detector | git for-each-ref on refs/heads/memoir/* |
Only when the code branch is main. Mid-flight on a feature branch, other branches' unmerged work is noise. |
| Codebase snapshot | memoir summarize --keys "*" -n codebase:onboard + batched get |
Default on (MEMOIR_ONBOARD_INJECT=1). See Codebase snapshot below. |
The status line itself follows the same conditional shape: [memoir] <branch> · <N> memories [· capture disabled] [· N branches unmerged] [· concurrent session warning].
Shape of the default-keys block:
# default namespace keys
(12 keys, grouped by L1 prefix)
knowledge (7):
- knowledge.technical.branching
- knowledge.technical.merge
...
preferences (1):
- preferences.communication.tone
metrics (1):
- metrics.turn.main
This is just the index — agents memoir get <key> the ones they care about, paying for content only on demand.
Codebase snapshot (codebase:onboard)¶
A persistent, high-level repo overview written by /memoir:onboard and replayed by SessionStart so fresh sessions start warm with a structured map of the codebase.
Layout. Keys live in the codebase:onboard namespace, grouped by L1 root:
| Root | What it captures |
|---|---|
goal.primary / goal.non_goals |
What the project is and isn't. |
structure.entrypoints |
CLIs, servers, main functions. |
structure.modules.<fs_path> |
One key per major module (src_memoir_cli, plugins_claude_code, …). 1–3 line role summary. |
test.strategy |
Test layout + how to run. |
debug.common |
How to reproduce common failure modes. |
deploy.targets |
How the code ships (CI workflows, packaging). |
document.sources |
Where canonical docs live. |
rules.* |
Project rules beyond CLAUDE.md, one key per rule. |
lessons.* |
Hard-won facts from prior incidents, one key per lesson. |
references.* |
External links / upstream conventions. |
_meta.last_onboard.{commit,date,memoir_commit,mode} |
Staleness anchors. |
Each value is ≤ ~500 chars; the SessionStart compact view takes the first sentence and caps a root's joined children at 140 chars.
Refresh paths. /memoir:onboard probes _meta.last_onboard.commit and picks one of three:
- cold — no prior snapshot. Full scan:
ls -d */, skimCLAUDE.md/README*/pyproject.toml/Makefile/.github/workflows/*.yml,git log --oneline -20. Write everygoal.*/structure.modules.*/rules.*/lessons.*/references.*key withmemoir remember -p <path> -n codebase:onboard(the-pflag bypasses the LLM classifier — fast and deterministic). - warm — code HEAD has moved.
git log --stat <last_sha>..HEADenumerates changed paths; only the affectedstructure.modules.*keys and any new lessons are rewritten. Typically 1–5 keys per pass. - meta-only — code HEAD unchanged. Bumps
_meta.last_onboard.dateso the staleness indicator renders fresh; no narrative keys touched.
--force always uses the cold path.
Branch behavior. The snapshot lives in the codebase:onboard namespace, not default — BranchService.promote_branch only carries the default namespace, so codebase:onboard stays per-branch. This is intentional: a feature branch can carry its own structural notes without leaking them to main until the user explicitly chooses to.
Staleness. SessionStart flags the snapshot stale="true" when _meta.last_onboard.date is more than 30 days old, and appends a (snapshot is stale — run /memoir:onboard to refresh) hint. memoir sync-branch calls update_onboard_meta_after_sync on the merged branch so the meta keys stay truthful even when the user hasn't re-run /memoir:onboard.
Non-git folders (project:onboard)¶
The plugin treats non-git folders as a first-class case rather than a degraded git mode. This is the home for running Claude Code over non-code projects — writing (drafts, manuscripts, research notes), video editing (clips, transcripts, project files), bookkeeping (statements, receipts, spreadsheets), and similar mixed-media folders.
Contract.
| Surface | Git folder | Non-git folder |
|---|---|---|
| Branches | Auto-tracks code branch | Locked to main |
| Status line | [memoir] <branch> · N memories |
[memoir] main · N memories |
| Stop auto-capture | Captures to current memoir branch | Captures to main |
memoir sync-branch, memoir unmerged (CLI) |
Operate normally | Short-circuit with "non-git folder: only main exists" |
/memoir:onboard |
codebase:onboard cold/warm based on code SHA |
project:onboard cold/warm based on filesystem snapshot hash |
SessionStart injection |
Renders codebase:onboard block |
Renders project:onboard block |
Stats / memoir log, graph, tree |
Identical | Identical |
project:onboard namespace layout.
| Key | Contents |
|---|---|
summary.overview |
2–4 sentences auto-composed by a deterministic shape detector (writing / bookkeeping / video-editing / mixed). |
structure.shape |
One of writing-shape, bookkeeping-shape, video-editing-shape, mixed. |
structure.tree |
Pruned directory tree (depth ≤ 3). |
structure.totals |
JSON: {file_count, dir_count, total_bytes, kind_histogram}. |
files.<sanitized_path>.meta |
{size, mtime, ext, kind} per file (/ and . → _ for the path segment). |
files.<sanitized_path>.summary |
Structured key=value blob from a per-kind extractor. Always begins with kind=…. |
_meta.last_onboard.{date,mode,snapshot_hash,memoir_commit,file_count} |
Refresh anchors. |
Deterministic extractors — no LLM at index time. The skill runs plugins/claude-code/skills/memoir-onboard/extractors.py (stdlib-only Python) once per file. One function per kind, all bounded:
- prose / markdown — frontmatter
title:→ first H1 → first non-empty line; first 50 / last 20 words; word count; top non-stopword terms. - csv / tsv — sniffed delimiter, columns, 16-row sample, streamed row count, numeric columns. Adds
shape=ledgerwhen columns include date+amount+category-like patterns. - office-zip (
.docx,.pptx,.xlsx,.epub) — stdlibzipfile+xml.etreereadsdocProps/core.xml, sheet names, slide count, paragraph count, EPUB manifest. - pdf — metadata-only at v1 (file size, magic bytes, version). Real text extraction is a tool entry.
- video-project (
.fcpxml,.kdenlive,.prproj,.aep) — XML parse for project name, clip count, duration; binary.aepis metadata-only. - json / yaml — top-level keys, max depth, item count.
- srt / vtt — first cue, last cue, cue count, total duration.
- image / audio / video — extension-derived
kindplus stdlib-cheap header parses (PNG dimensions from IHDR, WAV duration, etc.). Anything that needs a real codec stays metadata-only.
Files larger than 50 MB get metadata-only treatment regardless of kind, so raw video and audio never enter the snapshot.
Cold / warm / meta-only paths. Same shape as codebase:onboard, but keyed off a filesystem snapshot hash (sha256 over sorted (path, size, mtime_ns) tuples) instead of a code SHA:
- cold — no prior snapshot. Walk → run every extractor → write
files.*keys,summary.overview,structure.tree,structure.totals,structure.shape→ stamp_meta.*. - warm — snapshot hash differs. Diff path-by-path: added → run extractor + write; deleted →
memoir forget; modified → re-run extractor + write; unchanged → skip. Refresh aggregate keys and re-stamp meta. Falls through to a full cold rewrite when more than ~30% of files changed. - meta-only — snapshot hash unchanged. Bump
_meta.last_onboard.dateonly.
Pluggable tool registry. v1 ships with zero tool entries — every cold and warm pass is free, offline, and stdlib-only. To add an external tool (e.g. Whisper for audio transcription, ExifTool for images, a vision LLM for image captioning), drop a YAML or JSON config:
# ~/.memoir/onboard-tools.yaml (user-global)
# <project>/.memoir/onboard-tools.yaml (project-local; merged after global)
audio:
- name: whisper
command: "whisper {path} --output-format json"
timeout_s: 60
image:
- name: claude-vision
command: "vision-caption.sh {path}"
timeout_s: 30
Per kind, the stdlib extractor always runs first; configured tools then run and merge their JSON output under extractor.<tool_name>.<field> keys, so the consumer LLM can tell deterministic fields apart from tool-derived ones via the extractor.stdlib.fields=[…] provenance line. Results are cached at <store>/.git/plugin-extractor-cache/<sha256-of-file-content>.<tool>.json so warm-mode reuses tool output when file content is unchanged. Failures are silent — tool errors and timeouts are logged to /tmp/memoir-hook.log; the blob is emitted with stdlib fields only.
Excludes. Default exclusion globs cover OS / editor cruft (.DS_Store, ~$*), code build artifacts (node_modules, __pycache__, dist, .venv), and video / audio editor caches (Adobe Premiere Pro Auto-Save/, *.fcpcache/, Render Files/). Add project-local entries via .memoir/onboard-excludes.txt (gitignore syntax, one glob per line).
Store-mode drift guardrail (warning-only). A folder that flips between non-git and git states (running git init on an existing project, or rm -rf .git-ing a tracked folder) keeps the same store path — they share ~/.memoir/<slug>. The plugin records the mode at first store creation in <store>/.git/plugin-store-mode. Subsequent SessionStarts compare the marker to the current state; on mismatch, it surfaces a one-block warning alongside the normal status line:
[memoir] note: store mode drift
This store was created in `non-git` mode; the project directory is now `git`.
Captures continue, but branch auto-matching and the SessionStart onboard
injection now use the new mode — earlier non-git-mode data may be on a
different memoir branch (run `memoir branch list` to inspect).
To suppress: `memoir checkout main` and update the marker with
`echo git > <store>/.git/plugin-store-mode`.
Captures keep working through the warning — it's informational, not enforced. The marker is auto-backfilled the first time an old (pre-guardrail) store is observed, so no warning fires for stores that pre-date this feature.
How auto-capture decides what to remember¶
The Stop hook's capture stage (stop.sh:69-end, gated by MEMOIR_NO_CAPTURE=1) makes one haiku call per turn that does both extraction and classification in one shot — emitting taxonomy-classified durable facts directly, bypassing memoir's internal LLM classifier chain. Net effect: ONE haiku call per turn instead of 1 (extract) + N×4-5 (classify + decide + metadata for each fact) — typically 25–30× faster end-to-end.
Pipeline.
- Pre-flight guards. Skip silently if the transcript has fewer than 3 lines, or if
parse-transcript.shreturns(empty transcript),(no user message found), or(empty turn). Anchors on the most recent user message — only the trailing turn is sent to haiku. - Taxonomy resolution. Read the cached taxonomy block populated at
SessionStart. Falls back to a hardcoded hint sheet (top-level + common second-level paths) if the store has no taxonomy loaded — so first-run sessions still classify correctly. - System prompt. Load
plugins/claude-code/hooks/prompts/stop_capture.tmpl(single source of truth, testable via the prompt-harness) and substitute${TAXONOMY_BLOCK}in bash. TheCATEGORIES+EXAMPLESblocks come from the store's persisted taxonomy (taxonomy:v1:*), so auto-capture classifies against the same taxonomy as explicit/memoir:remember "fact"(without-p). -
Worthiness gate — the silent default. The system prompt instructs haiku that the default answer is nothing. A line is emitted only when a fact passes four hard gates:
- Durable — still relevant in weeks or months.
- A future session would genuinely benefit from knowing it.
- Not already discoverable from the code, git log,
CLAUDE.md, orREADME. - A senior engineer would write it down in onboarding notes.
Always-capture triggers override the silent default: standing rules ("from now on…", "always X", "never X"), stated preferences ("I prefer X over Y"), architectural / tooling decisions just resolved this turn (with rationale), project facts not yet in the repo, and non-obvious technical invariants. Most turns produce zero lines — that is the expected, high-quality result. 5. One-shot LLM call.
claude -p --model haiku --no-session-persistence --no-chrome --system-prompt "$STOP_SYSTEM_PROMPT"with the parsed transcript on stdin.MEMOIR_NO_CAPTURE=1 CLAUDECODE=is set on the subprocess to prevent recursion into the host's plugin hooks. 6. Output format.<path>[,<path>...]<TAB><fact>per line. Comma-separated paths in column 1 mean "write the same fact to multiple paths in one call" — each blob'srelated_keysfield records the siblings (handled memoir-side). 7. Line-format validation. A line is rejected if any of: empty path or fact, fact under 8 characters, or paths don't match^[a-z][a-z0-9_]*(\.[a-z0-9_]+){1,3}(,…)*$. Guards against haiku going rogue with preamble text or malformed taxonomy paths. 8. Write. For each surviving line, build-p p1 -p p2 …argv from the comma list and callmemoir remember "$fact" -p …— bypassing memoir's internal classifier entirely. 9. Statusline refresh. After captures land, recompute the user-memory count and rewrite the statusline cache so the displayed count ticks up immediately.
Toggles & failure mode. MEMOIR_NO_CAPTURE=1 disables only the capture stage; the metrics stage still runs. The whole pipeline is fail-silent — every subprocess uses 2>/dev/null || true, malformed haiku output is filtered out by the line-format check, and the hook always exits 0. A failed turn is silent, not loud.
Testing. The system prompt is a first-class artifact — extracted to hooks/prompts/stop_capture.tmpl so the prompt-harness can test it in isolation:
# Full LLM suite (~60s, costs LLM tokens)
plugins/claude-code/tests/prompt-harness/runner.py run --prompt stop_capture --model haiku
# One case
plugins/claude-code/tests/prompt-harness/runner.py case stop_capture/<case>.yaml --model haiku
The harness drops system.txt, input.txt, output.txt, result.json, and a replayable command.sh per case under /tmp/memoir-prompt-tests/<UTC-timestamp>/. Read summary.md for pass/fail. Assertion DSL: plugins/claude-code/tests/prompt-harness/README.md.
Code-change audit log (metrics.code.<branch>)¶
After the auto-capture stage, the Stop hook runs a third pass that detects file edits this turn and appends a one-line summary to a per-branch audit log. The goal is a chronological, human-readable record of what changed in the code, browseable later via memoir get metrics.code.<branch> (or the UI).
Pipeline.
- Toggle gate. Skip if
MEMOIR_NO_CODE_SUMMARY=1. - Transcript scan.
hooks/collect-edits.shwalks the most recent turn'stool_useblocks forEdit,Write,MultiEdit, andNotebookEdit. Returns a JSON object{user_prompt, edits: [{tool, file_path, snippet}, …]}with each snippet truncated to 300 chars and the user prompt truncated to 2000 chars. Empty stdout = no file edits → skip the LLM call. - Build prompt input. Prepend an optional
[User prompt]\n<text>\n---header (the why — gives haiku the user's stated intent), then concatenate edits as<tool> <file_path>\n<snippet>\n---, capped at ~8 KB so multi-file refactors stay well under haiku's context window. - One haiku call.
claude -p --model haiku --no-session-persistence --no-chrome --system-prompt "$CC_SUMMARY_PROMPT"with the assembled block on stdin. Recursion-prevention env:MEMOIR_NO_CAPTURE=1 MEMOIR_NO_METRICS=1 MEMOIR_NO_CODE_SUMMARY=1 CLAUDECODE=. Prompt template:hooks/prompts/code_change_summary.tmpl. Haiku uses the user prompt as the highest-signal source for why; the snippets confirm what. - Output validation & cleanup. Strip surrounding quotes / preambles (
Here is,Summary:, leading bullets), collapse to the first non-empty line, truncate to 1000 chars (haiku is asked for ≤100 words, so the cap is a safety belt for run-on output). If the result is exactlytrivial(case-insensitive), skip the write — trivial-edit suppression is decided by haiku in-prompt. - Branch lookup.
memoir_json status→ current memoir branch (fallbackunknown). - Read-merge-write append.
memoir remember -preplaces by path, so the hook owns the append (mirrors themetrics.turn.<branch>flow). Reads the existing JSON value, parsesentries[], appends{timestamp, summary}, writes the merged accumulator back.
Key shape: metrics.code.<branch> — sibling to metrics.turn.<branch>. Branch names with / (e.g. feature/x) are kept literal.
Value shape:
{
"schema_version": 1,
"entries": [
{"timestamp": 1714800000.0, "summary": "Refactored auth middleware to use JWT; added unit tests for token expiry."},
{"timestamp": 1714801200.0, "summary": "Renamed getUser → getCurrentUser across 7 callers; updated docstrings."}
]
}
Entries are append-forever — no decay. Bash-induced edits (sed -i, mv, refactor scripts) are not detected by design; the log reflects what the agent itself did via Edit/Write/MultiEdit/NotebookEdit.
Toggles & failure mode. MEMOIR_NO_CODE_SUMMARY=1 disables only this stage; capture and metrics still run. Every subprocess uses 2>/dev/null || true and the hook always exits 0 — same fail-silent design as the other stages. A failed turn is silent, not loud.
Branch identity & merge. Source-branch identity lives in the key fragment, not the value. BranchService.promote_branch carries metrics.code.feature/x to main automatically when the user runs memoir sync-branch feature/x, preserving the per-source-branch view.
Testing. Unit + integration + prompt-harness cover the three layers separately:
# Transcript-scan unit tests (no memoir, no haiku)
bash plugins/claude-code/tests/test_collect_edits.sh
# Stop-hook integration (read-merge-write append, gating; no haiku)
bash plugins/claude-code/tests/test_stop_code_summary.sh
# Prompt cases (haiku-driven, costs LLM tokens)
plugins/claude-code/tests/prompt-harness/runner.py run --prompt code_change_summary --model haiku
Per-branch turn metrics (metrics.turn.<branch>)¶
The Stop hook accumulates per-turn statistics into one key per branch in the default namespace, alongside auto-captured memories.
Key shape: metrics.turn.<branch> — for example metrics.turn.main, metrics.turn.feature/x. Branch names with / are kept literal (memoir's remember -p accepts arbitrary path strings).
Value shape:
{
"schema_version": 1,
"tokens": null,
"llms": null,
"turns_count": 42,
"total_output_chars": 198432,
"total_tool_input_chars": 24561,
"total_tool_result_chars": 884201,
"total_tool_calls": 187,
"total_tool_errors": 6,
"total_repeated_tool_calls": 12,
"total_latency_ms": 1845300,
"latency_samples": 31
}
Each turn the hook reads the existing accumulator, folds in deltas computed by collect-metrics.sh, and writes it back via merge-metrics.py. tokens and llms are reserved null until Claude Code exposes per-turn usage to hooks; today the proxies are char-count and tool-count fields. latency_samples only ticks for turns whose transcript line carries a user-message timestamp.
Toggles & failure mode. MEMOIR_NO_METRICS=1 disables the metrics path independently of MEMOIR_NO_CAPTURE — either can fail without affecting the other. The whole block is wrapped in 2>/dev/null || true, matching the rest of the Stop hook's fail-silent design.
Branch identity & merge. Source-branch identity lives in the key fragment, not the value — so BranchService.promote_branch (default-namespace only) carries metrics.turn.feature/x to main automatically when the user runs memoir sync-branch feature/x. After promotion, main retains its own metrics.turn.main untouched; metrics.turn.feature/x rides along, preserving the per-source-branch view.
UI surface. The /memoir:ui Statistics modal grows two conditional tabs after Overview:
- Codebase — renders
codebase:onboardkeys grouped by L1 prefix, first sentence per child, with a header showinglast_onboard <commit> · <date> · <mode>. Same compact rendering shape as the SessionStart inject. - Metrics — table view with rows = branches and columns = accumulator fields (
Turns,Calls,Errors,Avg latency (ms),Output chars, …). Three bar charts below show avg-latency / output-chars / tool-result-chars distributions across branches.
Both tabs only appear when their data exists. Both fetch via raw GET /api/onboard and GET /api/metrics — no LLM.
Environment variables¶
All optional.
| Variable | Default | Effect |
|---|---|---|
MEMOIR_STORE |
~/.memoir/memoir_<hash> |
Override the per-project store path. |
MEMOIR_NO_CAPTURE |
unset | 1 disables Stop-hook auto-capture (haiku classification + memory writes). Metrics still record. |
MEMOIR_NO_METRICS |
unset | 1 disables the per-branch turn-metrics accumulator. Auto-capture still runs. |
MEMOIR_NO_CODE_SUMMARY |
unset | 1 disables the per-branch code-change audit log (metrics.code.<branch>). Capture and metrics still run. |
MEMOIR_ONBOARD_INJECT |
1 |
0 suppresses the codebase:onboard block in SessionStart's additionalContext. |
MEMOIR_LLM_MODEL |
haiku |
Model used for the Stop hook's fact extractor. Override only if you've validated alignment with the prompt-test harness. |
MEMOIR_MAX_RESULT_CHARS |
1000 |
Per-tool-result truncation in parse-transcript.sh. |
Where to set them¶
Pick the scope you want — Claude Code merges settings from all three layers, with project-local winning over user-global winning over the shell environment.
1. User-global (every Claude Code session, every project) — ~/.claude/settings.json:
2. Per-project, committed (every collaborator on this repo) — <repo>/.claude/settings.json:
3. Per-project, local only (just you, gitignored) — <repo>/.claude/settings.local.json. Same shape as above. Use this for personal toggles you don't want to commit (e.g. MEMOIR_STORE pointing at a custom path).
4. One-off / shell session — export before launching claude:
Persist by adding the export line to ~/.zshrc / ~/.bashrc, or use direnv with a per-project .envrc.
After editing a settings.json, restart the Claude Code session for the change to take effect (the plugin reads env at hook fire time, but Claude Code reads settings at session start).
Manifest¶
plugins/claude-code/.claude-plugin/plugin.json:
{
"name": "memoir",
"version": "0.1.0",
"description": "Git-versioned, taxonomy-structured memory for Claude Code — recall by path, branch to isolate, time-travel to audit."
}
See also¶
- CLI — the underlying
memoircommands the plugin wraps. - API — the Python library for programmatic use.
- Architecture — how memoir is structured under the hood.