Know whether your auto-memory index loaded whole. Reports the index's size against a limit you set at every session start, names the entries past the cut, and…

Know whether your auto-memory index loaded whole. Claude Code reads a project's MEMORY.md index at session start up to a limit, and a session can't tell whether it got the whole file, a truncated one, or nothing. A long thread on the tracker asks for the threshold to be visible and configurable. Until it is, this mod measures the file at every session start, after compaction, and after each edit to it, and names the entries past the limit you set.
/plugin marketplace add MDmubarak786/claude-mods
/plugin install memory-lint@modhub
Try it for one session without installing:
claude --plugin-dir ./mods/memory-lint
At session start, after compaction, /clear, or /resume, and after any edit to the index, the mod measures it. Under the limit, a dim line in the transcript gives the count. Over it, a status line under the prompt stays until it's fixed:
⚠ memory-lint: 208 lines, 8 entries past line 200 may not load. /memory-lint lists them.
| Command | What it does |
|---|---|
/memory-lint | Report lines, entries, size, and the entries past the limit. |
/memory-lint limit 300 | Set the line limit you believe applies. Saved. |
/memory-lint path <file> | Point at a different index file for this project. Saved. |
The default limit of 200 lines is a guess. The real threshold isn't documented, which is the point of the thread. Set it to what you observe.
The index is looked for at ~/.claude/projects/<project slug>/memory/MEMORY.md, where the slug is the project root with every character that isn't a letter or digit replaced by -.
From claude plugin validate ./mods/memory-lint:
hooks: session.start, classic.SessionStart{source=compact|clear|resume}, tool.call{tool=Edit|Write}, command.run{command=memory-lint}
calls: $.command.register, $.fs.exists (via measure), $.fs.read (via measure), $.process.run (via locate), $.session.root (via locate), $.store.get (via locate), $.store.set, $.ui.log, $.ui.status (via lint)
$.process.run runs printenv HOME once at session start to find your Claude directory. Nothing else is run.$.fs.read reads only the index file.tool.call on Edit and Write observes edits to the index path and passes every call through unchanged.$.store holds the limit and any path override.claude plugin validate --strict and claude plugin test pass. /memory-lint answered from a live claude -p session.- or *. Headings and prose lines count toward the line total but not the entry count.MIT, see the repository root.
hooks/register.ts 133 lines1// memory-lint: know whether your auto-memory index loaded whole.
2//
3// /memory-lint report the index: size, entries, and what's past the limit
4// /memory-lint limit 300 set the line limit you believe applies (default 200)
5// /memory-lint path <file> point at a different index file
6//
7// Claude Code reads the project's MEMORY.md index at session start, up to a
8// limit that isn't documented, and a session can't tell whether it got the
9// whole file. This mod measures the file at every start, after compaction,
10// and after each edit to it, and names the entries past the limit you set.
11
12const DEFAULT_LIMIT = 200
13
14let home = ''
15let root = ''
16let indexPath = ''
17let limit = DEFAULT_LIMIT
18
19type Report = { exists: boolean; lines: number; bytes: number; entries: number; past: string[] }
20
21function slug(path: string): string {
22 return path.replace(/[^A-Za-z0-9]/g, '-')
23}
24
25async function locate($) {
26 try {
27 root = await $.session.root()
28 } catch {
29 root = ''
30 }
31 try {
32 const saved = await $.store.get('limit')
33 if (typeof saved === 'number' && saved > 0) limit = saved
34 } catch {
35 limit = DEFAULT_LIMIT
36 }
37 try {
38 const override = await $.store.get('path:' + root)
39 if (typeof override === 'string' && override) {
40 indexPath = override
41 return
42 }
43 } catch {
44 // Fall through to the default path.
45 }
46 try {
47 home = (await $.process.run(['printenv', 'HOME'])).stdout.trim()
48 } catch {
49 home = ''
50 }
51 indexPath = home && root ? home + '/.claude/projects/' + slug(root) + '/memory/MEMORY.md' : ''
52}
53
54async function measure($): Promise<Report> {
55 const none: Report = { exists: false, lines: 0, bytes: 0, entries: 0, past: [] }
56 if (!indexPath) return none
57 try {
58 if (!(await $.fs.exists(indexPath))) return none
59 const text = await $.fs.read(indexPath)
60 const lines = text.split('\n')
61 const entries = lines.filter((l) => /^\s*[-*]\s/.test(l))
62 const past = lines.slice(limit).filter((l) => /^\s*[-*]\s/.test(l)).map((l) => l.trim().replace(/^[-*]\s+/, ''))
63 return { exists: true, lines: lines.length, bytes: text.length, entries: entries.length, past }
64 } catch {
65 return none
66 }
67}
68
69function summary(r: Report): string {
70 if (!r.exists) return 'No memory index at ' + (indexPath || '(unknown path)') + '.'
71 return 'MEMORY.md: ' + r.lines + ' lines, ' + r.entries + ' entries, ' + Math.round(r.bytes / 1024) + ' KB. Limit ' + limit + ' lines.' +
72 (r.past.length ? ' ' + r.past.length + ' entr' + (r.past.length === 1 ? 'y is' : 'ies are') + ' past the limit and may not have loaded.' : ' Under the limit.')
73}
74
75async function lint($, why: string) {
76 const r = await measure($)
77 if (!r.exists) return
78 if (r.past.length) {
79 $.ui.status('memory-lint: ' + r.lines + ' lines, ' + r.past.length + ' entries past line ' + limit + ' may not load. /memory-lint lists them.')
80 } else {
81 $.ui.status(undefined)
82 $.ui.log('memory index ' + why + ': ' + r.lines + ' lines, ' + r.entries + ' entries, under the ' + limit + '-line limit')
83 }
84}
85
86export function register(on) {
87 on('session.start', async ($, e, next) => {
88 await locate($)
89 try {
90 await $.command.register({ name: 'memory-lint', description: 'Check whether the memory index fits the load limit', argumentHint: '[limit N | path <file>]' })
91 } catch (error) {
92 $.ui.log('could not register /memory-lint: ' + error)
93 }
94 await lint($, 'at start')
95 return next(e)
96 })
97
98 on('classic.SessionStart', { source: ['compact', 'clear', 'resume'] }, async ($, e, next) => {
99 await lint($, 'after ' + e.source)
100 return next(e)
101 }).catch(async ($, e, next) => next(e))
102
103 // The index changed during the session: measure it again.
104 on('tool.call', { tool: ['Edit', 'Write'] }, async ($, e, next) => {
105 const result = await next(e)
106 if (indexPath && e.file_path === indexPath && !result.deny && !result.isError) await lint($, 'after an edit')
107 return result
108 }).catch(async ($, e, next) => next(e))
109
110 on('command.run', { command: 'memory-lint' }, async ($, e) => {
111 const args = e.args.trim()
112 const lim = /^limit\s+(\d+)$/.exec(args)
113 if (lim) {
114 limit = Number(lim[1])
115 await $.store.set('limit', limit)
116 await lint($, 'with the new limit')
117 return { text: 'Limit set to ' + limit + ' lines. ' + summary(await measure($)) }
118 }
119 const path = /^path\s+(.+)$/.exec(args)
120 if (path) {
121 indexPath = path[1].trim()
122 await $.store.set('path:' + root, indexPath)
123 return { text: 'Index path for this project: ' + indexPath + '. ' + summary(await measure($)) }
124 }
125 if (args) return { text: 'Usage: /memory-lint, /memory-lint limit N, or /memory-lint path <file>' }
126 const r = await measure($)
127 const lines = [summary(r)]
128 if (r.exists) lines.push('Path: ' + indexPath)
129 if (r.past.length) lines.push('Past the limit:', ...r.past.map((p) => ' - ' + p))
130 return { text: lines.join('\n') }
131 }).catch(async () => ({ text: 'memory-lint: the command failed.' }))
132}
133