This is a formal protest, with receipts
On Jul 15 I ran /compact in a 948,822-token Claude Code session (game-dev project, hundreds of files written). Every stage of your pipeline reported success: PreCompact hook green, Morph API 200, the CLAUDE.md override suppressed Claude's native summary as designed, SessionStart injected the result in 931ms, state file cleaned up. Textbook run.
The continuation session knew essentially nothing about my project.
I spent the morning auditing this plugin end-to-end — source, real transcripts, the actual bytes injected. What I found is not one unlucky failure. It is a chain of independent defects, each on the critical path of a product whose only job is context preservation, and whose install flow deliberately disables Claude Code's native (working) summarizer via a CLAUDE.md override. When you replace a built-in, safety-critical feature, the bar is "provably at least as reliable as what you removed." This plugin does not clear it, and the failure mode is silent, unrecoverable data loss.
Environment: morph-compact 0.2.8 (06f96f0), Claude Code 2.1.208, macOS 15 (Darwin 24.6.0), Node 26.3.0.
Defect 1 — the transcript parser reads a field that does not exist; 100% of tool output is silently dropped
hooks/lib/transcript.js (same code in the old src/transcript.ts):
case "tool_result":
if (typeof block.input === "string" && block.input) {
parts.push(`[result]: ${block.input}`);
}
break;
tool_result blocks in Claude Code transcripts carry their payload in content (string or block array). They never have an input field — that belongs to tool_use. This branch is dead code. It has never executed, in any version, for any user.
Verified against real transcripts on my machine:
- A 23.5 MB session transcript: 402
tool_result blocks, all with content — 0 with input.
- My compacted session (pre-compact portion): 390 tool_result blocks totalling 431,987 characters — 100% dropped. What your API actually received was ~45,069 characters of user/assistant text plus bare
[tool: Name] markers. 90.6% of the extractable conversation was discarded before Morph ever saw it.
The resulting "summary" looks exactly like you'd expect. Verbatim excerpt from what was injected into my continuation session:
[tool: Write]
[tool: Bash]
[tool: Write]
[tool: Write]
[tool: Write]
继续:主题、立即模式 UI(控件层,游戏手感的核心)、应用契约。
[tool: Write]
[tool: Write]
...
For a coding agent, tool results are the session: file contents, build errors, test output, everything the work actually produced. A single fixture test — parse one real transcript, assert tool output is present — would have caught this before release. #9 was closed as "add test infra"; the most critical function in the codebase evidently still has no test that has ever seen a real transcript.
Fix: read block.content (handle string and block-array forms), truncate long results.
Defect 2 — the summary exceeded Claude Code's hook-output cap and arrived as a 2 KB stub of junk
Morph returned a 33.4k-char summary. Claude Code persists oversized hook additionalContext to disk and injects only a stub:
Output too large (32.6KB). Full output saved to: .../tool-results/hook-...-additionalContext.txt
Preview (first 2KB): <local-command-caveat>...</local-command-caveat> <command-name>/effort</command-name> ...
The first 2 KB of the summary were verbatim /effort and /goal command-wrapper boilerplate (see Defect 4), so the preview contained zero project information. I verified the continuation session never read the persisted file — not one reference to that path in the rest of the transcript. Net effective context transfer from a 948k-token session: a 2 KB stub of command junk.
The plugin neither enforces a byte budget on what it injects (compressionRatio is a ratio, not a budget), nor prefixes the injection with "the full summary is at <path> — read it before proceeding." Either one line would have mitigated this.
Defect 3 — a corrupt state file permanently disables compaction for the session, silently
The state file is written with a plain non-atomic writeFile, and both hooks JSON.parse it outside any try/catch (pre-compact.js:24, session-start.js:17). One truncated write — hook killed mid-write, full disk — and every subsequent PreCompact and SessionStart for that session throws at the parse, the corrupt file is never unlinked, and no summary is ever generated or injected again. No error surfaces to the user; compactions simply start destroying context.
Fix: guarded parse with unlink-on-corrupt; write via temp file + rename.
Defect 4 — query: lastUserMsg?.content steers retention with slash-command junk
At compact time, the most recent "user" message is typically the <local-command-*> wrapper of the /compact invocation itself. That is what gets passed to your API as the relevance query. Consistent with that, my summary led with preserved /effort//goal boilerplate instead of project content.
The design gamble that turns all of the above into data loss
The CLAUDE.md override that blanks the native summary is probabilistic — it's an instruction the compaction model may or may not follow. On this same machine, same plugin version: the Jul 9 compaction ignored it (native 24.9k-char summary survived, Morph summary injected on top — redundant but safe), the Jul 15 compaction obeyed it (placeholder only). When it is obeyed, every downstream link must work perfectly or the user's context is gone:
loadApiKey() throws at module scope (morph.js) — before the try/catch in pre-compact.js exists — so a missing key or failed npm install writes no state file at all: placeholder in, nothing out.
- Upstream default
COMPACT_TIMEOUT = 60000 equals the default 60s hook timeout — after npm install overhead the hook loses that race and is killed before the catch block can write the error state. Same silent outcome.
- Defects 1–3, above.
Related open issues — #15 (fail-closed compaction), #12 (no retry), #14 (npm install stall), #16, #13, #2 — show the same pattern. This architecture takes a feature that must fail open (worst case: a mediocre native summary) and rebuilds it to fail closed (worst case: everything is gone and nobody is told).
What I expect from a project that ships this
- Fix Defect 1 and commit a fixture test that parses a real Claude Code transcript and asserts tool-output coverage. This is the minimum bar for touching people's context.
- Enforce a byte budget on injected context, or instruct the continuation model to read the persisted file.
- Atomic state writes, guarded parses, unlink corrupt state.
- Make suppression of native compaction opt-in until the replacement path is demonstrably reliable. Defaulting to it is gambling with users' work.
- Until all of the above: a README warning that compaction may silently lose context.
All numbers above are reproducible; I'm happy to share the exact verification commands. I had already patched several of these locally (SDK-vs-hook timeout race, fallback tail on failure, stale-state TTL) — but Defect 1 alone means every summary this plugin has ever produced was built from ~10% of the conversation. I have uninstalled the plugin and will not recommend it until the fixes and the test land.
This is a formal protest, with receipts
On Jul 15 I ran
/compactin a 948,822-token Claude Code session (game-dev project, hundreds of files written). Every stage of your pipeline reported success: PreCompact hook green, Morph API 200, the CLAUDE.md override suppressed Claude's native summary as designed, SessionStart injected the result in 931ms, state file cleaned up. Textbook run.The continuation session knew essentially nothing about my project.
I spent the morning auditing this plugin end-to-end — source, real transcripts, the actual bytes injected. What I found is not one unlucky failure. It is a chain of independent defects, each on the critical path of a product whose only job is context preservation, and whose install flow deliberately disables Claude Code's native (working) summarizer via a CLAUDE.md override. When you replace a built-in, safety-critical feature, the bar is "provably at least as reliable as what you removed." This plugin does not clear it, and the failure mode is silent, unrecoverable data loss.
Environment: morph-compact 0.2.8 (
06f96f0), Claude Code 2.1.208, macOS 15 (Darwin 24.6.0), Node 26.3.0.Defect 1 — the transcript parser reads a field that does not exist; 100% of tool output is silently dropped
hooks/lib/transcript.js(same code in the oldsrc/transcript.ts):tool_resultblocks in Claude Code transcripts carry their payload incontent(string or block array). They never have aninputfield — that belongs totool_use. This branch is dead code. It has never executed, in any version, for any user.Verified against real transcripts on my machine:
tool_resultblocks, all withcontent— 0 withinput.[tool: Name]markers. 90.6% of the extractable conversation was discarded before Morph ever saw it.The resulting "summary" looks exactly like you'd expect. Verbatim excerpt from what was injected into my continuation session:
For a coding agent, tool results are the session: file contents, build errors, test output, everything the work actually produced. A single fixture test — parse one real transcript, assert tool output is present — would have caught this before release. #9 was closed as "add test infra"; the most critical function in the codebase evidently still has no test that has ever seen a real transcript.
Fix: read
block.content(handle string and block-array forms), truncate long results.Defect 2 — the summary exceeded Claude Code's hook-output cap and arrived as a 2 KB stub of junk
Morph returned a 33.4k-char summary. Claude Code persists oversized hook
additionalContextto disk and injects only a stub:The first 2 KB of the summary were verbatim
/effortand/goalcommand-wrapper boilerplate (see Defect 4), so the preview contained zero project information. I verified the continuation session never read the persisted file — not one reference to that path in the rest of the transcript. Net effective context transfer from a 948k-token session: a 2 KB stub of command junk.The plugin neither enforces a byte budget on what it injects (
compressionRatiois a ratio, not a budget), nor prefixes the injection with "the full summary is at<path>— read it before proceeding." Either one line would have mitigated this.Defect 3 — a corrupt state file permanently disables compaction for the session, silently
The state file is written with a plain non-atomic
writeFile, and both hooksJSON.parseit outside any try/catch (pre-compact.js:24,session-start.js:17). One truncated write — hook killed mid-write, full disk — and every subsequent PreCompact and SessionStart for that session throws at the parse, the corrupt file is never unlinked, and no summary is ever generated or injected again. No error surfaces to the user; compactions simply start destroying context.Fix: guarded parse with unlink-on-corrupt; write via temp file + rename.
Defect 4 —
query: lastUserMsg?.contentsteers retention with slash-command junkAt compact time, the most recent "user" message is typically the
<local-command-*>wrapper of the/compactinvocation itself. That is what gets passed to your API as the relevancequery. Consistent with that, my summary led with preserved/effort//goalboilerplate instead of project content.The design gamble that turns all of the above into data loss
The CLAUDE.md override that blanks the native summary is probabilistic — it's an instruction the compaction model may or may not follow. On this same machine, same plugin version: the Jul 9 compaction ignored it (native 24.9k-char summary survived, Morph summary injected on top — redundant but safe), the Jul 15 compaction obeyed it (placeholder only). When it is obeyed, every downstream link must work perfectly or the user's context is gone:
loadApiKey()throws at module scope (morph.js) — before the try/catch inpre-compact.jsexists — so a missing key or failednpm installwrites no state file at all: placeholder in, nothing out.COMPACT_TIMEOUT = 60000equals the default 60s hook timeout — afternpm installoverhead the hook loses that race and is killed before the catch block can write the error state. Same silent outcome.Related open issues — #15 (fail-closed compaction), #12 (no retry), #14 (npm install stall), #16, #13, #2 — show the same pattern. This architecture takes a feature that must fail open (worst case: a mediocre native summary) and rebuilds it to fail closed (worst case: everything is gone and nobody is told).
What I expect from a project that ships this
All numbers above are reproducible; I'm happy to share the exact verification commands. I had already patched several of these locally (SDK-vs-hook timeout race, fallback tail on failure, stale-state TTL) — but Defect 1 alone means every summary this plugin has ever produced was built from ~10% of the conversation. I have uninstalled the plugin and will not recommend it until the fixes and the test land.