This is the one document to read before touching /stash or /remember (and the
friction.js sensor that /remember runs). It explains what each step does, how they
complement each other, and the design decisions behind the current behaviour so we don't
have to reverse-engineer it again.
Hot memory is .claude/memory/MEMORY.md, injected into CLAUDE.md between
<!-- MEMORY:START --> / <!-- MEMORY:END --> as @MEMORY.md. Claude loads it into
every future session in the project. So whatever lands there steers all later work —
which is exactly why the bar for writing to it is deliberately high.
Two project-local commands feed it — and friction runs as a step inside /remember, not
as a separate command. None call an external service; the whole thing is markdown files in
your repo.
/stash ┐ snapshots you write .claude/memory/MEMORY.md
├─► /remember ──► Facts / Episodes / Antigens ──► @MEMORY.md ──► HOT
│ └─ runs friction.js first: antigens mined from your logs (every session)
/stash— you snapshot a session's context (before compaction, handoff, or a break). Once a few unprocessed stashes pile up it nudges you to run/remember./remember— runs thefriction.jssensor first (mining all your session logs for moments you had to correct the agent), then consolidates stashes + friction antigens intoMEMORY.mdand wires up@MEMORY.md.
The two sources complement each other by source and trust: stashes are what you
deliberately wrote down; friction is what the agent did wrong that you reacted to,
recovered automatically from logs. /remember is the step that writes hot memory, and it
treats the two sources differently (below).
- Writes
.claude/stash/<name>.mdwith the active plan, decisions, and findings. - Use it before compaction, handoffs, or ending complex work.
- It is a clean-start habit, not a distress signal. You stash frequently just to start fresh. Friction treats a bare stash as a checkpoint (ignored) — it only matters when a real frustration preceded it (see §3, fix #1).
node friction.js <sessions-dir> (e.g. ~/.claude/projects/), invoked automatically by
/remember against your global sessions root. Two stages, seven output files in
.claude/friction/.
What it is: the sensor. It reads raw session logs (which an LLM can't cheaply do — hundreds of multi-MB transcripts), detects where you had to correct the agent, and emits short, evidence-tagged antigen candidates.
What it is NOT: a productivity tracker, an engagement meter, or a project-health dashboard. Its only job is detecting recurring agent↔user mismatch. (If a marker measures activity/volume instead of mismatch, it does not belong in friction.)
The signal model (the core idea): trust what the user did, not what the machine guessed. An antigen is a triad:
[agent action + result] → [user reaction] → [unresolved marker]
the trigger (#4) the seed (observed) stash/abandon/silence (#1/#2)
- Seeds (observed, high-trust):
user_correction("no, wrong…"),user_curse,interrupt_cascade. Only these can create an antigen candidate. - Corroboration (inferred, low-trust): exit codes,
false_success,user_intervention(/stash),session_abandoned,long_silence. These never seed — they only add context or escalate, and only when they actually surround a real reaction.
Outputs (.claude/friction/):
| file | contents |
|---|---|
antigen_clusters.json |
the contract /remember reads — clusters with theme, suggested_artifact, confidence, severity, sessions, projects, contexts (verbatim quotes), preceding (trigger), self_suspect |
antigen_review.md |
human-readable version of the clusters |
antigen_candidates.json |
raw per-reaction candidates before clustering |
friction_raw.jsonl |
every detected signal |
friction_analysis.json / friction_summary.json / report.md |
per-session texture + aggregate dashboard (kept, but not the antigen pipeline) |
- Runs
friction.jsfirst (best-effort) against the global sessions root, regenerating.claude/friction/so the antigen data below is always fresh. If no sessions root resolves it says so out loud and consolidates stashes only — never silently skips. - Reads
.claude/stash/*.md→ Facts + Episodes (via sonnet, skipping already-processed stashes). - Reads
.claude/friction/antigen_clusters.json→ Antigens (step 4):- Classify target — sonnet decides agent-directed vs self-correction; drops the latter.
- Semantic-merge — sonnet groups same-complaint-different-words quotes friction left split.
- Tier by recurrence — High (5+ sessions, loads hot), Medium (3-4, recorded), Low (<3, episode).
- It works only from friction's short quotes — never the raw logs.
- Writes
MEMORY.md(Facts / Episodes / Antigens) and injects@MEMORY.mdintoCLAUDE.md.
The original tool trusted machine proxies and graded whole sessions BAD, which poisoned
memory with noise (on a 253-session corpus, 15 false high-confidence preferences, all
built from exit-codes and /stash false positives — including the /stash help text
mistaken for user feedback). The redesign inverts that into a two-barrier funnel:
Guard the signal → require reinforcement before anything becomes a hot antigen:
observed reaction → agent-directed → corroborated-in-context → recurring → LLM-confirmed → (5+ sessions) hot
Precision over recall, on purpose. A false antigen (a wrong rule applied to every future session) is far costlier than a missed faint one — and a real issue recurs and gets caught next time. So friction under-detects rather than over-writes.
The four corrections (from real-usage feedback):
- Stash is context-gated — a clean-start stash is a checkpoint (weight 0); it only
counts as friction when a
user_correction/user_cursepreceded it. - Abandonment/silence are context-gated the same way — they're mixed signals (open tabs, context-switching), so they're zero unless they follow an unresolved reaction.
- Agent-directed vs self-correction — not every "no, wrong…" is an antigen. "Wrong
project, abort" is you redirecting yourself; friction flags it
self_suspectand the LLM confirms/drops it. (Friction stopped auto-marking every correction severe.) - The trigger is kept — the agent's preceding action + result (often a claimed exit-0 success the user is contradicting) is attached to each reaction, so an antigen carries both halves: what the agent did and what you said about it.
Where the LLM lives: lexical matching catches verbatim repetition ("wrong project" ×3)
but cannot merge paraphrases ("nothing landed" vs "says pushed but none got it") — that's a
semantic judgment. So the split is: friction detects + cheaply pre-groups (precise); the
LLM in /remember does the final merge + target-classification (on the short quotes only).
This keeps friction dependency-free and fast, and puts the semantic call where an LLM
already runs.
- The redesign is shipped in all four packages (
packages/{claude,opencode,ampcode, droid}/commands/friction/friction.js). Validation on 253 sessions: false hot preferences 15 → 0, antigen candidates now 100% observed user reactions (was 100% machine-inferred). /frictionis no longer a standalone command. It was collapsed into/remember, which runsfriction.jsautomatically (best-effort) against the global sessions root before consolidating. Rationale: friction was a thin script-wrapper rarely run on its own, and bundling guarantees the antigen data is fresh — without silently skipping (a no-sessions miss is surfaced loudly). The script is still directly runnable for inspection (§5).- The per-session dashboard (
report.md/friction_summary.json, the BAD-rate) is intentionally unchanged — it's a separate concern from the antigen pipeline. - Full design history:
.claude/stash/2026-05-25-friction-redesign-experiment.mdand.claude/stash/2026-06-16-command-consolidation-shipped.md.
/remember runs friction automatically, but you can invoke the sensor directly to inspect
its output without consolidating:
# run friction over all projects (what /remember does for you)
node friction.js ~/.claude/projects/
# the antigen contract /remember consumes:
cat .claude/friction/antigen_clusters.json
# human-readable:
cat .claude/friction/antigen_review.md