You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
plugins/claude-code/hooks/stop.sh passes the summarizer prompt to claude -p as a
single command-line argument. Linux caps one argument at MAX_ARG_STRLEN, 32 pages,
131072 bytes on a 4 KiB-page system. When the parsed turn exceeds that, execve
fails with E2BIG and claude never starts. The failure is discarded by 2>/dev/null || true, so SUMMARY ends up empty, and the fallback then writes the entire raw payload into the user's memory journal:
# If claude is not available or returned empty, fall back to raw parsed outputif [ -z"$SUMMARY" ];then
SUMMARY="$PARSED"fi
The result is that the worst case, a turn too large to summarize, is also the one
case where the whole thing gets stored verbatim.
This is easy to hit in normal use. Loading a Claude Code skill injects that skill's
full documentation into the turn as one user text block. In our case the claude-api
skill contributed a single 828 KB block, making the parsed turn 831 KB
(~213,000 tokens).
plugins/claude-code/hooks/parse-transcript.sh applies no size cap of any kind.
What we expected
A turn too large to summarize is skipped, or truncated and then summarized. Either
way nothing unsummarized is written to memory, and the failure is visible somewhere.
What actually happened
One turn produced an 831 KB journal entry containing verbatim API reference
documentation. Across 725 stored entries in one project, 3 were raw dumps of this
kind, totalling 1,091,103 bytes, or 49.6% of the entire memory directory. A
normal healthy summary in the same directory is under 2.7 KB; the largest is 2,663 B.
Downstream effects:
Every re-index re-embeds the dump. With the local ONNX provider this pinned ~29 of
32 cores for minutes at a time, at a load average of 60, and burned 84 minutes of
CPU on 3 MB of notes.
The dump is fed back as recalled memory at session start, so it consumes context in
every subsequent session and pollutes search results.
The journals are usually in git, so the dumps end up in history too.
Steps to reproduce
Any project with the Claude Code plugin active and embedding.provider set
(we used onnx).
Start a session and do something that loads a large skill into the turn, or craft
a transcript whose last turn parses to more than 131072 bytes. A direct way:
Observe the journal file for today under .memsearch/memory/. It contains an entry
whose body begins === Transcript of a conversation between User and Claude Code ===
followed by the whole turn.
Minimal confirmation of the mechanism alone, no plugin needed:
$ /bin/true "$(head -c 131071 /dev/zero | tr '\0''x')";echo$?
0
$ /bin/true "$(head -c 131072 /dev/zero | tr '\0''x')";echo$?
bash: /bin/true: Argument list too long
126
We also reproduced it end to end by feeding the unpatched hook a crafted 1.5 MB turn:
the resulting journal file was 1,516,261 bytes, the hook's stdout was {}, and its
stderr was completely empty.
The regression
Introduced by commit 018a85f ("Fix native summary hook prompt handling", PR #563,
2026-06-01), which replaced a stdin pipe with an argv string:
It also bounds its empty-summary fallback to the last 800 characters rather than the
whole turn. MEMSEARCH_SUMMARY_MAX_CHARS appears exactly once in the codebase. The
Claude Code plugin never received the same treatment.
Suggested fix
Any one of these closes the hole; we applied all three locally:
Deliver the prompt on stdin.claude -p with no positional prompt reads the
prompt verbatim from stdin, adding no wrapper text (verified on Claude Code 2.1.220),
and --system-prompt still works alongside it if wanted. Pipes are not subject to MAX_ARG_STRLEN. Verified: 200,033 bytes as argv fails with exit 126; the same bytes
on stdin exit 0 and summarize normally.
Cap PARSED, as the Codex plugin already does. Placing the cap before the
branch protects the memsearch summarize path too, which pipes via stdin and so
never hit E2BIG, but currently has no cap at all and would ship the full payload
to a remote provider. For reference, across our 80 most recent transcripts the
parsed turn was p50 5,493 B, p95 6,922 B, max 19,487 B, so a cap in the tens of KB
is inert for real traffic.
Also worth surfacing the failure somewhere. 2>/dev/null here made a call that never
ran look identical to one that returned nothing, which is why this went unnoticed for
weeks. Precedent: #546 treated the same stderr suppression as a bug on Windows.
Description
plugins/claude-code/hooks/stop.shpasses the summarizer prompt toclaude -pas asingle command-line argument. Linux caps one argument at
MAX_ARG_STRLEN, 32 pages,131072 bytes on a 4 KiB-page system. When the parsed turn exceeds that,
execvefails with
E2BIGandclaudenever starts. The failure is discarded by2>/dev/null || true, soSUMMARYends up empty, and the fallback then writes theentire raw payload into the user's memory journal:
The result is that the worst case, a turn too large to summarize, is also the one
case where the whole thing gets stored verbatim.
This is easy to hit in normal use. Loading a Claude Code skill injects that skill's
full documentation into the turn as one user text block. In our case the
claude-apiskill contributed a single 828 KB block, making the parsed turn 831 KB
(~213,000 tokens).
plugins/claude-code/hooks/parse-transcript.shapplies no size cap of any kind.What we expected
A turn too large to summarize is skipped, or truncated and then summarized. Either
way nothing unsummarized is written to memory, and the failure is visible somewhere.
What actually happened
One turn produced an 831 KB journal entry containing verbatim API reference
documentation. Across 725 stored entries in one project, 3 were raw dumps of this
kind, totalling 1,091,103 bytes, or 49.6% of the entire memory directory. A
normal healthy summary in the same directory is under 2.7 KB; the largest is 2,663 B.
Downstream effects:
32 cores for minutes at a time, at a load average of 60, and burned 84 minutes of
CPU on 3 MB of notes.
every subsequent session and pollutes search results.
Steps to reproduce
Any project with the Claude Code plugin active and
embedding.providerset(we used
onnx).Start a session and do something that loads a large skill into the turn, or craft
a transcript whose last turn parses to more than 131072 bytes. A direct way:
Let the Stop hook run.
Observe the journal file for today under
.memsearch/memory/. It contains an entrywhose body begins
=== Transcript of a conversation between User and Claude Code ===followed by the whole turn.
Minimal confirmation of the mechanism alone, no plugin needed:
We also reproduced it end to end by feeding the unpatched hook a crafted 1.5 MB turn:
the resulting journal file was 1,516,261 bytes, the hook's stdout was
{}, and itsstderr was completely empty.
The regression
Introduced by commit
018a85f("Fix native summary hook prompt handling", PR #563,2026-06-01), which replaced a stdin pipe with an argv string:
A pipe has no size limit; an argument does. No cap was added to compensate. Still
present on
mainatb734a14.The fix already exists in this repo, in the sibling plugin
plugins/codex/hooks/stop.shuses the same argv pattern but bounds its input:It also bounds its empty-summary fallback to the last 800 characters rather than the
whole turn.
MEMSEARCH_SUMMARY_MAX_CHARSappears exactly once in the codebase. TheClaude Code plugin never received the same treatment.
Suggested fix
Any one of these closes the hole; we applied all three locally:
claude -pwith no positional prompt reads theprompt verbatim from stdin, adding no wrapper text (verified on Claude Code 2.1.220),
and
--system-promptstill works alongside it if wanted. Pipes are not subject toMAX_ARG_STRLEN. Verified: 200,033 bytes as argv fails with exit 126; the same byteson stdin exit 0 and summarize normally.
PARSED, as the Codex plugin already does. Placing the cap before thebranch protects the
memsearch summarizepath too, which pipes via stdin and sonever hit
E2BIG, but currently has no cap at all and would ship the full payloadto a remote provider. For reference, across our 80 most recent transcripts the
parsed turn was p50 5,493 B, p95 6,922 B, max 19,487 B, so a cap in the tens of KB
is inert for real traffic.
$PARSED. A marker line keeps the entry's anchor comment,so the turn stays recoverable through progressive disclosure without storing it.
Related: Stop hook writes Anthropic API rate-limit error string as memory summary content #527 is the same fallback writing an API rate-limit error string into
memory as a summary.
Also worth surfacing the failure somewhere.
2>/dev/nullhere made a call that neverran look identical to one that returned nothing, which is why this went unnoticed for
weeks. Precedent: #546 treated the same stderr suppression as a bug on Windows.
Documentation corrections
docs/platforms/claude-code/how-it-works.md, live athttps://zilliztech.github.io/memsearch/platforms/claude-code/how-it-works/, is
inaccurate on two points since
018a85f:claude -p --model haikuwith asystem prompt". It has been passed as argv, not piped, since June.
parse-transcript.shas a "Deterministic JSONL-to-textparser with truncation". It performs no truncation.
Environment
b734a142ea017657959dfe918ecfe9e1a16c6654getconf PAGESIZE= 4096, soMAX_ARG_STRLEN= 131072;ARG_MAX= 4194304gpahal/bge-m3-onnx-int8)