[FEATURE] Hierarchical memory to prevent silent loss at 200-line MEMORY.md limit

Status Closed — not planned
Maintainer reply None cached
Activity 11 comments · opened Mar 29, 2026 · closed May 11, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

Claude Code's auto memory stores entries in a flat MEMORY.md index capped
at 200 lines / 25KB. When a power user accumulates enough memories, entries
at the bottom of the index are silently truncated on reload. The topic files
still exist on disk but become orphaned — Claude can't find them because their
pointers are gone.

Auto Dream consolidates entries but maintains the flat structure. It doesn't
create hierarchy or push detail into sub-indices. For users with complex,
long-running projects, the flat model hits its ceiling and memories are
quietly lost.

Proposed Solution

Replace the flat index with a self-balancing tree. When MEMORY.md exceeds
a threshold (e.g. 150 lines), entries are grouped by memory type and pushed
into category index files in an _index/ subdirectory. Each category pointer
in MEMORY.md becomes a single line with a count and summary. If a category
index also overflows, it splits further by topic keyword. The tree grows in
depth, not width — same discipline at every level.

I built this as an external tool that runs via hooks:
https://github.com/j-p-c/alzheimer

It works today with all Claude Code models (Opus, Sonnet, Haiku) and requires
no dependencies beyond Python 3.6+ stdlib. Installation is one sentence to
Claude: "Install the alzheimer memory rebalancer from github.com/j-p-c/alzheimer."

Key design points:

  • Root stays small: MEMORY.md never exceeds 150 lines (headroom below the 200-line cap)
  • Detail pushes down, summaries push up — Claude gets enough context from the

summary line to decide whether to read deeper

  • Self-balancing via PostToolUse, SessionStart, and PreCompact hooks
  • Compatible with Auto Dream (rebuilds if Dream flattens the tree)
  • Configurable limits via .alzheimer.conf for forward-compatibility
  • 50 tests, MIT-0 license

Why This Matters

The 200-line limit means Claude Code's memory system has a hard ceiling on
complexity. Users who rely on memory for project context, preferences, and
workflow instructions hit this wall and lose information without warning.
A hierarchical approach lifts this ceiling without requiring changes to
Claude Code itself.

The tool is MIT-0 (no attribution required) — happy for the ideas or code
to be adopted natively if useful.

View original on GitHub ↗

12 Comments

github-actions[bot] · 5 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/40210
  2. https://github.com/anthropics/claude-code/issues/40245
  3. https://github.com/anthropics/claude-code/issues/27298

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

yurukusa · 5 months ago

You can work around the 200-line MEMORY.md limit with a UserPromptSubmit hook that loads memory from a larger index:

INPUT=$(cat)
PROMPT=$(echo "$INPUT" | jq -r '.user_prompt // empty' 2>/dev/null)
MEMORY_DIR=".claude/memory"
[ ! -d "$MEMORY_DIR" ] && exit 0
TOTAL=$(find "$MEMORY_DIR" -name "*.md" -not -name "MEMORY.md" | wc -l)
RELEVANT=""
if [ -n "$PROMPT" ]; then
    TERMS=$(echo "$PROMPT" | tr ' ' '\n' | grep -E '^[a-zA-Z]{3,}' | head -5)
    for term in $TERMS; do
        MATCHES=$(grep -ril "$term" "$MEMORY_DIR"/*.md 2>/dev/null | head -3)
        for match in $MATCHES; do
            NAME=$(grep -m1 '^name:' "$match" | sed 's/name:\s*//')
            DESC=$(grep -m1 '^description:' "$match" | sed 's/description:\s*//')
            [ -n "$NAME" ] && RELEVANT="$RELEVANT\n- $NAME: $DESC"
        done
    done
fi
if [ -n "$RELEVANT" ]; then
    echo "{\"hookSpecificOutput\":{\"additionalContext\":\"Relevant memories ($TOTAL total on disk):$RELEVANT\"}}"
fi
exit 0
MEMORY_DIR=".claude/memory"
[ ! -d "$MEMORY_DIR" ] && exit 0
INPUT=$(cat)
PROMPT=$(echo "$INPUT" | jq -r '.user_prompt // empty' 2>/dev/null)
CONTEXT=""
for category in user feedback project reference; do
    COUNT=$(find "$MEMORY_DIR" -name "*.md" -exec grep -l "type: $category" {} \; 2>/dev/null | wc -l)
    if [ "$COUNT" -gt 0 ]; then
        RECENT=$(find "$MEMORY_DIR" -name "*.md" -exec grep -l "type: $category" {} \; 2>/dev/null | xargs ls -t | head -3 | xargs -I{} grep -m1 'name:' {} | sed 's/name:\s*//')
        CONTEXT="$CONTEXT\n$category ($COUNT): $RECENT"
    fi
done
[ -n "$CONTEXT" ] && echo "{\"hookSpecificOutput\":{\"additionalContext\":\"Memory overview:$CONTEXT\"}}"
exit 0
{
  "hooks": {
    "UserPromptSubmit": [{
      "hooks": [{ "type": "command", "command": "bash ~/.claude/hooks/memory-loader.sh" }]
    }]
  }
}

The hook reads memory files directly from disk — it's not limited by MEMORY.md's 200-line cap. It searches all .md files in the memory directory and injects relevant ones as additionalContext. The individual memory files remain the source of truth; MEMORY.md becomes just one of many access paths.

j-p-c · 5 months ago

Thanks for flagging these. I've reviewed all three — this issue is related but not a duplicate of any of them.

#40210 reports the same underlying problem (bottom-truncation losing newest memories) but is a bug report without a solution. This issue provides a working solution.

#40245 proposes a fundamentally different architecture — merging memory and CLAUDE.md into hierarchical summary.md files inside the project folder. That's a much larger redesign of how memory works. Alzheimer works within the existing memory system, requiring no changes to Claude Code itself.

#27298 is the closest — a layered memory proposal with keyword search on every prompt. The key difference is approach: #27298 bypasses MEMORY.md by searching files on every UserPromptSubmit, adding per-prompt overhead. Alzheimer maintains the index structure so Claude's native memory loading continues to work unchanged — it just ensures the index never overflows by pushing detail into sub-indices automatically.

To summarize: this is a drop-in tool that solves the truncation problem today, within the existing memory architecture, with no changes to Claude Code required. The repo is MIT-0 and the ideas are free to adopt.

j-p-c · 5 months ago

@yurukusa Thanks for sharing this — clever approach to bypass the index entirely by searching files on disk at prompt time.

Alzheimer takes a different angle: instead of bypassing MEMORY.md, it keeps the index working by restructuring it into a tree before it overflows. Claude's native memory loading continues to work unchanged, and there's no per-prompt hook overhead.

Two different solutions to the same problem — yours would have been useful before we built this!

mikeadolan · 5 months ago

This is the problem. I built claude-brain to solve it. Full lossless capture to local SQLite, no silent loss, searchable across all projects, imports from ChatGPT and Gemini. Free and open source. github.com/mikeadolan/claude-brain

mikeadolan · 5 months ago

This is exactly why claude-brain uses a centralized SQLite database instead of path-dependent memory files. Your memory isn't tied to a folder location. It's stored in one local database, keyed by project prefix, searchable across all projects. Move your project folder, rename it, work from a different machine -- your memory stays intact.

If you're running into the orphaned memory problem described in #41283, claude-brain solves it. One command install, fully local, works alongside Claude Code's built-in memory. https://github.com/mikeadolan/claude-brain

j-p-c · 5 months ago

@mikeadolan Interesting approach with SQLite — solves the path-dependency problem that #41283 highlights. Different trade-off from Alzheimer's file-based tree: yours centralizes storage (one DB, no orphans possible), ours keeps memory as plain markdown files that Claude's native loading can read without any additional tooling.

Both solve the core truncation problem from different angles. The more solutions people have to choose from, the better — this clearly isn't a niche issue.

mikeadolan · 5 months ago

Appreciate the comparison. You're right that the tradeoffs are different. Centralized SQLite means zero orphans and full search (keyword, semantic, fuzzy) across everything, but it does require the MCP server or hooks to surface context. The markdown approach has the advantage of Claude reading files natively. Both are solving a real gap. Good to see multiple approaches out there.

prodan-s · 5 months ago

Heads up on a potential issue in Alzheimer's hook output routing that might affect SessionStart context delivery.

In rebalance.py (lines 1999-2012), SessionStart content is routed to systemMessage instead of hookSpecificOutput.additionalContext:

hso_supported = ("PostToolUse", "UserPromptSubmit")
if args.hook_event and args.hook_event in hso_supported:
    output["hookSpecificOutput"] = { ... }
else:
    output["systemMessage"] += "\n" + additional_text

After investigating the CC v2.1.90 binary, I found that for sync command hooks, systemMessage is rendered to the user's terminal only — it does not get injected into Claude's context. The code path: systemMessagehook_system_message attachment type → normalizeAttachmentForAPI returns [].

However, hookSpecificOutput with additionalContext does work for SessionStart. The Zod schema explicitly includes it:

x.object({hookEventName: x.literal("SessionStart"), additionalContext: x.string().optional()})

And the processing function handles it identically to PostToolUse/UserPromptSubmit:

case "SessionStart":
    I.additionalContext = A.hookSpecificOutput.additionalContext;
    break;

This means glossary update instructions sent via systemMessage on SessionStart may never reach Claude. Switching to hookSpecificOutput with hookEventName: "SessionStart" should fix it.

Our own SessionStart hook uses this exact format and it works reliably across v2.1.45 through v2.1.90:

{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"..."}}

Happy to share more details from the binary analysis if useful.

j-p-c · 5 months ago

This is an outstanding catch — thank you. We've pushed the fix in 0e52c34.

The one-line change: SessionStart and PreCompact are now routed through hookSpecificOutput.additionalContext instead of being folded into systemMessage. This means glossary update instructions, update-available messages, and warning details on these events will actually reach Claude's context for the first time.

Your binary analysis of the systemMessagenormalizeAttachmentForAPI[] path was exactly the evidence we needed to confirm the bug. We'd been attributing some intermittent "Claude didn't act on the glossary instruction" behavior to other causes — this was likely the real culprit all along.

If you're interested, the relevant code is in rebalance.py lines 2001-2012. We also updated the test that was asserting the old (wrong) behavior.

github-actions[bot] · 3 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

github-actions[bot] · 2 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.