Claude Code memory¶
One command wires a Particles store into Claude Code as managed agent memory: a small, ranked digest of standing context is pushed into every session's context window at session start — the top beliefs by effective confidence, bounded to a few thousand tokens, never the whole store — and everything a session produced is harvested into the corpus at session end. No agent cooperation required. The agent is stateless compute; the store is managed storage; the integration moves data between them on lifecycle events the agent does not control.
If you want the store exposed as tools an agent calls instead, the two other routes are the native MCP server (Querying → MCP server) and the reference memory-server swap.
What this buys you¶
A memory file accumulates lines; the store accumulates claims — and the difference shows up exactly where file memory hurts:
- Rules born from incidents keep their provenance. "Commit, push, and deploy are three separate go-aheads" is a typical agent memory: a rule decreed after something went wrong once. As a particle it carries when it was asserted and which session it came from, so a future session asking "does this still apply, and what was it protecting against?" is one hop from the answer instead of archaeology. And if a later session writes "push automatically after each commit," that isn't a silent second line in a file — it's a detected contradiction, queued for your ruling.
- Flaky-infrastructure facts age honestly. "Sometimes the tailscale link
between the two servers gets stuck in a slow mode" is true the day it's
written and misleading the month after it's fixed. In a file it lives
forever; here its effective confidence decays with age,
lintflags it stale, and the fix supersedes it — the old claim retired with a pointer to what replaced it, not erased. - The store can audit what your files can't. The first run reads the memory you already have and reports what's lurking in it — contradictions, likely duplicates, probably-stale facts, cited-but-never-captured sources — see The memory audit. Those are questions a directory of markdown cannot answer about itself.
Install¶
That one command:
- Merges two hook entries into
~/.claude/settings.json— aSessionStarthook runningparticles hook session-start --store <handle>and aSessionEndhook runningparticles hook session-end --store <handle>, both with the absolute path of yourparticlesexecutable so they work regardless of shell PATH. The merge is marker-owned: your existing settings and hooks are preserved byte-for-byte semantically; re-runninginitreplaces exactly the Particles-owned entries (repair/upgrade); an unparseable settings file is an error, never a rewrite. - Selects the memory store. If
mcp.write.enabled_storesnames exactly one store, that's the one. Several → pass--store <handle>. None (fresh install) →initcreates and enables amemorystore: it initialises the store database (default~/.particles/memory.db) and appendsstorage.stores.memory+mcp.write.enabled_storesto yourconfig.yamlunder the same never-clobber discipline (parse, preserve everything else — comments included — append, verify). - Provisions the state directory
~/.particles/claude-code/(the hook log, the projection manifestmemory.yaml, its render snapshot, the one-deepMEMORY.md.pre-renderbackup, and the fold archive live there), writes the defaultmemory.yamlwhen absent, and seeds the projected region into any existing~/.claude/projects/*/memory/MEMORY.md(an empty sentinel pair at the top of the file; all your content is preserved below it — see § The MEMORY.md projection). - Installs the agent-onboarding skill files into
~/.claude/skills/particles/— three short Markdown files telling the agent which write verb to reach for, how to read effective confidence and the contested marker, and that ruling on a contradiction is your call, not its. Skip with--no-skills; manage them separately withparticles skills install/particles skills list. - Runs the first-run memory audit over your existing
~/.claude/projects/*/memory/directories: harvest → extract → one census report of what your agent's memory already contains — potential contradictions, likely-duplicate beliefs, probably-stale facts — with the cost estimate printed and confirmed first (see § The memory audit). Skip with--no-audit; a declined estimate or missingANTHROPIC_API_KEYnever fails the install.
Options: --project installs into the current repo's
.claude/settings.local.json (the gitignored local file — never the
committed .claude/settings.json, because the entries embed your store choice
and executable path) · --dry-run prints every file it would write ·
--command <path> overrides the hook executable for non-standard installs ·
--json prints a machine-readable result instead of prose.
Letting the agent install it¶
--json exists so an agent can run the installer itself and report the
outcome rather than you reading over its shoulder:
stdout carries only the result object — the store, the scope, what was created,
the installed hook commands, and a next_steps list of what is left for you.
Because it is non-interactive it implies --no-audit, and names the standalone
particles audit under next_steps rather than skipping it silently.
This is deliberately the local half of self-onboarding. The agent mints no
credential and grants itself nothing: the write allowlist
(mcp.write.enabled_stores) is untouched, and everything it writes still lands
at agent trust, below yours. A network-exposed MCP write transport would need
its own auth model, which is a separate decision that has not been made.
What the hooks do¶
Session start — the digest push. hook session-start renders the store's
memory digest (one line per
ACTIVE belief, ranked by effective confidence, contested beliefs flagged) and
injects it as additionalContext, so the agent's standing knowledge is in
the context window before the first prompt — not behind an MCP tool it may
forget to call. A resume session already replays its prior context, so the
push is skipped there; startup, clear, and compact get a fresh render.
Two budgets bound the injection: mcp.recall.digest_max_beliefs (default 200)
and claude_code.digest_max_bytes (default 24 000, truncated on a line
boundary with a disclosed footer).
The push is deliberately small because rules and facts want different treatment. A rule ("commit, push, and deploy are separate go-aheads") wants to be in the prompt every session — that reliable presence is the digest's job, and why it ranks standing, high-confidence beliefs first. A fact (the port a service ran on in May) wants to be retrievable, not resident; that's the MCP query path, and it's why the digest never tries to carry the store. Ranking composes effective confidence with usage — beliefs that keep earning recall rise — so the budget is spent on what sessions actually use, not on whatever was written most recently.
Session end — the harvest. hook session-end deposits two kinds of
material — harvest, don't ask; the agent took no action to be remembered:
- The session transcript, distilled — never raw. A deterministic, LLM-free
pass keeps user/assistant turns verbatim, elides each tool call to one line
(
[tool: Bash — git status]), and drops tool results (where payloads and secrets concentrate). A redaction pass then masks common credential shapes (sk-…keys, AWS access key IDs,Bearerheaders, PEM blocks). The result lands as one corpus entry per session (claude-code://session/<id>,CONVERSATION,APPEND_ONLY) — a grown transcript appends a snapshot; an unchanged one is a content-hash no-op. - Changed memory files. Each
*.mdunder the project's auto-memory directory deposits asLOCAL_MARKDOWN/MUTABLE, so an editedMEMORY.mdis re-extracted with the right staleness semantics. Claude Code's own auto-memory stays enabled — the integration harvests it rather than fighting it. - Catch-up sweep. SessionEnd doesn't fire on a crash or SIGKILL, but the
transcript persists on disk — so after handling the current session the hook
re-checks up to
claude_code.harvest.catchup_limit(default 5) recent transcripts and harvests any whose content moved. The corpus itself is the harvest state; a session missed because the store was unreachable is simply retried at the next session end.
Extraction is deferred by default. Deposits are LLM-free; beliefs
materialise when extraction runs — particles extract --all-pending, the
first-run audit, or (opt-in) claude_code.harvest.extract_inline: true
extracts inside the hook, bounded by
claude_code.harvest.max_extract_entries_per_session (default 3). Until then,
a belief learned in session N appears in the session-N+1 digest only if
extraction ran in between.
The MEMORY.md projection¶
With the hooks installed, MEMORY.md stops being an append-only scratch file:
a sentinel-delimited region at the top of the file is regenerated from the
store at the tail of every harvest cycle, so what the agent recalls
at session start is the reconciled, ranked, decayed, contradiction-flagged
store — not the raw accumulation:
<!-- BEGIN PROJECTED: memory-index (manifest: ~/.particles/claude-code/memory.yaml) -->
- Owner prefers general mechanisms over per-genre extractor defaults `p-3f9a2c1d`
- DCO is enforced; every commit needs `git commit -s` `p-71b0de00`
- ⚠ contested — CI floors at Python 3.11 (vs. p-9c447100) `p-08d3e100`
<!-- sources: p-08d3e100, p-3f9a2c1d, p-71b0de00 -->
<!-- END PROJECTED: memory-index -->
What the region is. A deterministic ranked-bullet view — one line per
ACTIVE belief, ordered by effective confidence, contested beliefs flagged
rather than hidden, each line carrying its p-<shortid> drill-down handle
(resolve it with the MCP particle_show tool for full provenance). No LLM is
involved in the render: it is free, offline-capable, and byte-stable for a
given store, which is what lets the harvest recognise its own output and never
re-ingest it. Everything outside the region stays yours and Claude Code's:
the agent keeps appending memories below; the harvest picks them up. If you
edit inside the region, nothing is lost — the edited region is deposited as
authored input on the next cycle and reconciled into the store before the
region is re-rendered.
Fold-and-archive (default-on). Once agent-authored lines outside the
region have been harvested, the next cycle moves them — never deletes — to
the append-only archive ~/.particles/claude-code/MEMORY.archive.md (itself
harvested as corpus input), leaving one pointer line behind. The file thus
converges to the projected region + not-yet-harvested lines + the pointer, and
duplication between an authored line and its projected consolidation is
bounded to one cycle. Opt out with
agent_memory.projection.fold_authored_lines: false in config.yaml; a
harvest that did not succeed never triggers a fold, and every folded line is
recoverable from the archive or the corpus.
Editing the manifest. The region is driven by a standard projection
manifest at ~/.particles/claude-code/memory.yaml — yours to edit:
name: memory-index
sections:
- title: "Memory index"
query: null # no semantic refinement — rank purely by eff. conf.
top_k: 60
min_confidence: 0.30 # the noise floor
render: bullets # deterministic ranked bullets — never LLM prose
max_lines: 120 # document budget — headroom under the 200-line load cap
max_bytes: 16384
Add per-topic sections (tags: / subjects: per section), pin claims the
ranking misses with select.allow, exclude noise with select.deny
, or tighten the floor and budgets. max_lines / max_bytes
truncate in rank order — lowest effective confidence dropped first,
select.allow pins exempt. You can re-render on demand with
particles project ~/.particles/claude-code/memory.yaml
~/.claude/projects/<project>/memory/MEMORY.md --splice memory-index
--without-synthesis.
Safety posture. The splice runs only after the same cycle's harvest of
MEMORY.md succeeded; the write is atomic (temp file + rename); the pre-splice
file is backed up one-deep to ~/.particles/claude-code/MEMORY.md.pre-render;
and damaged sentinels (a deleted END line, a duplicated pair) make the cycle
refuse and skip rather than regenerate your file. Deleting the region
opts that file out — re-run particles init claude-code to re-seed it. With
the projection active, the session-start digest push checks the region's
<!-- sources: … --> trailer first: if the loaded file already is the
current view it injects nothing, and if the store moved since the last render
it injects only the difference. Disable the whole feature with
agent_memory.projection.enabled: false.
Git-versioned history (optional, off by default). If you keep your memory
directory under git — git init ~/.claude/projects/<project>/memory — and set
agent_memory.projection.git.enabled: true, each render that changes the file
is committed for you with a structured message: a run id plus a ranking-delta
summary (which beliefs entered or left the index, and whether the top belief
changed). The result is a diffable, rollback-able history of the view — the
Letta-MemFS ergonomic — while the store stays the source of truth: a
git revert only rewinds the file, and the next render re-projects from the
store. The commit is a bonus, never a requirement: any git problem — not a
repo, nothing changed, no configured identity, a signing failure — is logged at
debug and skipped, and never affects the projection itself. Signing is off by
default (--no-gpg-sign) so an unattended session-end commit can't block on a
signing agent; set git.sign: true to respect your own commit.gpgsign, and
git.author_name / git.author_email to stamp a specific identity. Only files
under the memory directory are staged (never git add -A), and the internal
backup / snapshot / archive live outside it, so they never end up in your
history. See config.yaml.sample (or the operator guide's Agent-memory
projection
section) for the full knob set.
The memory audit¶
Rot prevention (the hooks, the projection) pays off over weeks; rot detection on the memories you already have is immediate:
particles audit ~/.claude/projects/<project>/memory # harvest + extract + report
particles audit # re-audit the store (no harvest)
The audit is a first-run census. To keep it running unattended — and to run the reconcile, curation and projection passes alongside it — see Operator guide → scheduled consolidation. Contradictions and duplicates it names are resolved with lint and review and co-evidential curation.
Audited 23 memory files → 212 beliefs about 58 subjects.
4 potential contradictions (2 cross-file, 2 contested at extract time)
11 likely-duplicate belief pairs (unjudged similarity candidates; --judge to verify)
7 probably-stale facts (5 aged past their source's decay horizon, 2 expired)
Also: 3 cited sources never captured · 6 beliefs have no resolvable subject
The audit composes the existing finders — lint, the store-wide
contradiction probe, links suggest duplicate candidates, the quality
dashboard — into complete per-class counts with a few leverage-ranked
exemplars each (claim text included), and every class ends with its next verb
(particles review, particles curate --kind …, particles links suggest
--judge, particles deposit <url>). The counts are hedged on purpose:
duplicates are unjudged cosine candidates until --judge runs the LLM
verdict pass, contradiction counts are LLM judgments over similarity-gated
candidates, and extractions from memory files carry self-reported (capped,
not benchmark-calibrated) confidence — the report says all of this rather
than overstate.
What to know before running it:
- The deposits become your real store — the audited corpus is the same
store the hooks append to and
MEMORY.mdprojects from. Re-running (or runninginitafteraudit, in either order) re-processes nothing: corpus dedup skips unchanged content and extraction skips COMPLETE snapshots. - The cost estimate always prints first. Above
audit.confirm_call_thresholdestimated extraction calls (default 50) the CLI asks before spending;--yespre-confirms,--estimateprints and exits without depositing anything, and a non-interactive run without--yesaborts with the estimate shown. - Transcripts are opt-in.
--transcripts <dir>harvests session*.jsonltranscripts newest-first, capped ataudit.transcript_max_entries(default 20;--max-entriesoverrides) — they are large, LLM-priced, and lower-signal than the distilled memory files, so they never ride the first run silently. - No key, no silent clean bill. With no
ANTHROPIC_API_KEY, a harvest audit refuses before touching the store (extraction is the audit's substance); a re-audit of a populated store still runs the structural finders and duplicate candidates but sayscontradiction check skipped: no API keyin the report. - The projection renders at the end. When the MEMORY.md projection is
enabled, a successful harvest+extract pass finishes by re-rendering the
memory-indexregion — the activation moment leaves yourMEMORY.mdalready consolidated.
--output report.md also writes the report to a file; --format json dumps
the full model; --store <handle> audits a named store. Presentation knobs
live under audit: in config.yaml (exemplars_per_class,
transcript_max_entries, confirm_call_threshold); detection thresholds
stay with their finders.
Degradation and debugging¶
The hook verbs never break a session: on any failure — database missing,
engine unreachable, write-lock contention, or the internal
claude_code.hook_deadline_seconds deadline (default 10 s) — they log and
exit 0 with no output. A memory outage costs you an empty digest, not a hung
session start.
Every hook invocation appends one JSONL line (timestamp, event, session id,
outcome counts, duration, error) to the hook log at
~/.particles/claude-code/hooks.jsonl (claude_code.hook_log_path
overrides). Transcript content is never logged.
particles hook log --tail 20 # is this thing on?
particles hook session-start --store memory < sample.json # debug loop
Privacy posture¶
- Local-only by default. The hooks read local files and write the local
store. With a remote engine configured (
engine.base_url),session-startuses the remote digest freely (read-only), butsession-endrefuses to ship transcripts off-machine unless you setclaude_code.harvest.allow_remote: true; refusals are logged and the catch-up sweep back-fills once enabled. - Distill-then-redact. Only the distilled rendering is deposited — tool results never are. The pattern redaction is best-effort defence in depth, not a guarantee: review what scrolls through your sessions.
- Stored, not ephemeral. Deposits use normal archived mutability classes so
excerpt-level provenance ("where did I learn this?") works. Prefer
transcript-free beliefs? Set
claude_code.harvest.transcripts: false(memory-file harvest only).
All hook knobs live under the claude_code: section of config.yaml, and the
projection's under agent_memory.projection: — see config.yaml.sample and
Operator guide → configuration.
One gap this integration does not close on its own: your project's rule
files (AGENTS.md, CLAUDE.md) are frozen at deposit unless you opt them in,
so the store can keep asserting a rule you have since changed. See
Refreshing mutable local sources.
Uninstall¶
Removes exactly the Particles-owned hook entries (everything else in your
settings survives) and reverts the store auto-create only while the store is
still empty — a store holding data is never deleted. It also deletes the
~/.claude/skills/particles/ subdirectory and nothing beside it, so any skill
files of your own in that directory are untouched. The state directory
(hook log history) is kept; delete it manually if you want it gone.