claude-memory-tidy

August 14, 2026 · View on GitHub

Your always-loaded memory index is rent you pay on every single turn — and when it overflows, it fails silently.

Claude Code loads MEMORY.md into every session. The harness truncates it past a fixed size, and nothing tells you: no error, no warning. The index just gets shorter at the bottom and your agent quietly stops knowing things it "remembers". Meanwhile the notes on disk are still there, orphaned — present but unreachable, which at recall time is the same as deleted.

This is a small, boring, deterministic toolkit that keeps that index healthy. macOS and Linux.

sh scripts/memory_tidy.sh --dry-run    # what would happen
sh scripts/memory_tidy.sh              # do it

Real numbers from the machine it was built on, one run:

beforeafter
index size26 275 B8 606 B
index lines16452
orphaned notes1120
unreachable notes1120

Nothing was deleted. Every note is still on disk and still reachable.


The idea: hot dispatcher, warm hubs, cold archive

Three tiers, and the only expensive one is the first:

  • MEMORY.md — hot dispatcher. Loaded every turn. Aim for 60–100 lines. One line per pointer, - [Title](file.md) — hook, ≤150 chars. This is the only file that costs you tokens continuously.
  • hub-*.md — warm sub-indexes. Not auto-loaded. A domain with ≥3 related lines gets folded into a hub; the index keeps ONE vocabulary-rich line pointing at it.
  • MEMORY-archive.md — cold. Not auto-loaded, grep-only. Done work, closed lessons, and pointers for otherwise-orphaned notes. Coverage here costs zero live budget.

What the tools do

tooljobLLM?
memory_guard.pyjudge: size, line count, over-length entries, orphans, dead pointers, duplicates, sync-conflictsno
memory_orphan_cover.pygive every orphaned note a pointer in the archive, using the note's own description:no
memory_fold.pymove a domain into a hub verbatim, then verify every folded slug is still reachableno
memory_scope.pydecide which indexes this machine is allowed to writeno
memory_tidy.shrun the above, then call the model only if the detectors say there is workyes, gated

memory_fold.py puts the hub lines under a ## 🗂 Hubs heading. If your index already keeps them somewhere else, put that heading verbatim in the mapping file as "hub_section_marker" — the match is exact, and a marker that does not match is silent: you get a second hub section instead of an error.

The split is deliberate: mechanical work goes to scripts, judgement goes to the model. Covering 112 orphans is transcription — a script copies what each note already says about itself. Deciding what deserves always-loaded budget is judgement. Sending the first job to an LLM burns tokens and invents hooks that drift from the notes.

Install

git clone https://github.com/tonydzi/claude-memory-tidy
cd claude-memory-tidy
cp scripts/*.py scripts/*.sh prompt/*.md ~/.claude/scripts/
cp scripts/memory_scope.example.json ~/.claude/scripts/memory_scope.json

python3 ~/.claude/scripts/memory_scope.py --explain      # declare what this machine owns
python3 tests/test_memory_tidy.py                        # 10 checks, ~1s, no LLM, no network
sh ~/.claude/scripts/memory_tidy.sh --dry-run

Optional: install SKILL.md as a Claude Code skill so you can just say /memory-tidy.

mkdir -p ~/.claude/skills/memory-tidy && cp SKILL.md ~/.claude/skills/memory-tidy/

Schedule it with launchd (macOS) or cron (Linux); a sample plist is in examples/. Note that launchd has no anacron catch-up — a calendar job whose moment passed while the machine slept is skipped silently, so schedule two slots. The second one is free: on a quiet day the detectors return "nothing to do" and the model is never called.

Five failure modes this was built around

Each of these cost a real debugging session. They are the reason the code looks the way it does.

1. Two writers on one file. ~/.claude/projects/ mixes indexes this machine owns with indexes synced byte-identical from another machine. A tool that globs projects/*/memory/MEMORY.md and writes to everything it finds will produce sync-conflicts and lose somebody's memory. Ownership is declared in memory_scope.json, never inferred. An undeclared machine exits 3 loudly rather than tidying nothing — a node that silently does nothing looks exactly like a healthy one.

2. An alarm that can never be cleared. A shared pin-list named memories that existed on only one machine. Everywhere else the "a pinned rule fell out of the index!" tripwire fired forever, and "restoring" the pointer would have created a dead pointer — red for a new reason. A watchdog that is always red teaches everyone to ignore red. A pin now applies to an index only if the note is actually next to it.

3. Counting generated files as memories. The detector writes a hints file next to the index; the guard counted it as an orphan and demanded a pointer to a file that gets overwritten on the next run. An eternal chore nobody can close is a bug in the checker, not a task.

4. Summary-grounding when folding. The documented way folding goes wrong: a model rewrites the hook "while it's in there", and the detail that made the pointer findable evaporates. So folding moves bytes — memory_fold.py never paraphrases, and it refuses to write at all if any folded slug would lose reachability.

5. A logged-out CLI that exits zero. Headless claude -p on a logged-out machine printed an error and exited 0. Routines stamped green heartbeats and produced nothing. Check claude auth status before trusting any headless run, and make your wrapper turn that into a non-zero code.

Design rules

  • Detectors before the model. Two cheap deterministic checks gate the expensive call, so a quiet day costs ~0 tokens.
  • The tool never grades its own homework. After the model edits, the guard runs again and must print GREEN. The model's own summary is not evidence.
  • Nothing is deleted, ever. The only destructive-looking operation is moving a pointer line between files. Notes stay on disk.
  • Back up outside the synced tree. A .bak next to the index rides your sync and gets committed; backups go to a local-only directory.
  • When unsure whether something is still live, keep it live. Over-pruning costs more than under-pruning.

Not included

No telemetry, no network calls, no dependencies beyond the Python standard library. The tidy step shells out to whatever claude CLI you already have; everything else runs offline.

License

MIT — see LICENSE.

About & contact

Built and battle-tested at Palo Alto AI Research Lab — a fleet of Claude Code machines running 24/7 as a second brain and synthetic cofounder. The five failure modes above are incidents that hit that fleet first; none was invented for the repo.

Hit a sixth failure mode, or think one of the budgets is set wrong? Say so — a reproducible case from someone else's index is the most useful thing anyone can send us.