iTranslated by AI
Solving Claude Code's Sudden Context Loss with Custom Hooks
Working with Claude Code for about two hours often leads to a sudden "Wait, what were we talking about?" state. Here is a story about how I improved this by building an automatic restoration mechanism using hooks.
When "We Just Talked About This, Right?" Doesn't Work
When writing code while discussing design with Claude Code, there comes a moment where Claude's responses suddenly start to drift.
"Do it with that policy we decided on earlier."
→ "Which policy are you referring to?"
This is context compression (compact).
| What Happens | Result |
|---|---|
| Conversation gets long | Claude Code automatically summarizes and compresses the old conversation |
| During summarization | Recent design decisions and work context are heavily dropped |
| Experience | Your partner in a 2-hour collaboration suddenly gets amnesia |
Honestly, it's exhausting to re-explain every time: "This is the current situation, we decided on this earlier, and next we're trying to do this..."
Using hooks to "Save Before Compression → Restore After Compression"
Claude Code has a mechanism called hooks. You can automatically execute scripts at specific events (before compression, at session start, etc.).
Using this, I created the following flow:
What it does is simple. It just backs up "what we were talking about" before it gets compressed and brings it back after compression.
First Failure: Saving as-is Resulted in Too Much Noise
Initially, I tried saving the transcript (conversation log) as it was. I used tail to save the last 200 lines and injected them during restoration.
It didn't work at all.
The JSONL transcript contains a massive amount of data invisible to humans:
- thinking block — Claude's internal reasoning. This can be thousands of tokens.
- signature — Cryptographic signature data. Just a string of gibberish.
- tool results — File contents or command outputs. These are huge.
There is a limit to the size you can inject (around 20KB). If you fill this space with noise, the essential "what we were talking about" hardly fits in at all.
Solution: Parse the JSONL and Extract Only the Conversation
I changed my approach. Instead of saving the transcript as-is, I parse it and extract only the necessary parts.
| Extract | Discard | |
|---|---|---|
| User | Speech text | — |
| Claude | Text response | thinking block, signature |
| Tool | Tool name only | Execution results (file contents, command output) |
With this, I can now fit several times more conversation content into the same 20KB limit.
The Code
It consists of 2 files + settings. Zero external dependencies. Place them in your project as follows:
.claude/
├── hooks/
│ ├── save-context.js # Save side
│ └── restore-context.js # Restore side
└── settings.json # Hook settings
Save Side: save-context.js (PreCompact hook)
Runs just before compression. Parses the transcript to extract the conversation and saves it to .claude/CONTEXT-SNAPSHOT.md.
save-context.js (Click to expand)
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
// Extracts "who said what" from a single line of JSONL
function extractEntry(line) {
try {
const data = JSON.parse(line);
if (!data.message || !data.message.content) return null;
const role = data.type;
const content = data.message.content;
// User speech -> take only text
if (role === "user") {
if (data.toolUseResult) return null; // Discard tool results
const texts = [];
if (typeof content === "string") texts.push(content);
else if (Array.isArray(content)) {
for (const item of content) {
if (typeof item === "string") texts.push(item);
else if (item.type === "text" && item.text) texts.push(item.text);
}
}
return texts.length ? `[User] ${texts.join("\n")}` : null;
}
// Claude response -> take only text + tool names (ignore thinking/signature)
if (role === "assistant") {
const parts = [];
if (!Array.isArray(content)) return null;
for (const item of content) {
if (item.type === "text" && item.text) parts.push(item.text);
else if (item.type === "tool_use" && item.name) {
const brief = item.input?.description || item.input?.command?.slice(0, 80) || "";
parts.push(`[Tool: ${item.name}]${brief ? " " + brief : ""}`);
}
}
return parts.length ? `[Assistant] ${parts.join("\n")}` : null;
}
} catch { return null; }
return null;
}
// Main: receive hook data from stdin and process
let input = "";
process.stdin.on("data", (d) => (input += d));
process.stdin.on("end", () => {
try {
const data = JSON.parse(input);
const transcript = data.transcript_path;
const cwd = data.cwd;
if (!transcript || !fs.existsSync(transcript)) process.exit(0);
const snapshot = path.join(cwd, ".claude", "CONTEXT-SNAPSHOT.md");
const raw = fs.readFileSync(transcript, "utf8");
const lines = raw.split("\n").filter((l) => l.trim());
const tail = lines.slice(-200); // Target the last 200 lines
const entries = [];
for (const line of tail) {
const entry = extractEntry(line);
if (entry) entries.push(entry);
}
// Cut at 20KB limit (cutting at conversation boundaries)
let output = entries.join("\n\n");
if (output.length > 20000) {
output = output.slice(-20000);
const idx = output.indexOf("\n\n[");
if (idx > 0) output = output.slice(idx + 2);
}
fs.mkdirSync(path.dirname(snapshot), { recursive: true });
fs.writeFileSync(snapshot, output);
} catch { process.exit(0); } // Exit silently on error (don't break the main process)
});
Restoration Side: restore-context.js (SessionStart hook)
Runs at session restart after compression. Injects the saved content into Claude's context and deletes the file.
restore-context.js (Click to expand)
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
let input = "";
process.stdin.on("data", (d) => (input += d));
process.stdin.on("end", () => {
try {
const data = JSON.parse(input);
if (data.source !== "compact") process.exit(0); // Run only on compact
const snapshot = path.join(data.cwd, ".claude", "CONTEXT-SNAPSHOT.md");
if (!fs.existsSync(snapshot)) process.exit(0);
const content = fs.readFileSync(snapshot, "utf8").slice(0, 20000);
fs.unlinkSync(snapshot); // Delete after reading (one-time use)
// Inject as additionalContext
const output = {
hookSpecificOutput: {
hookEventName: "SessionStart",
additionalContext: [
"## Context from before compaction\n",
"Below is the recent transcript before context was compacted.",
"Use this to maintain continuity of the current task:\n",
content,
].join("\n"),
},
};
console.log(JSON.stringify(output));
} catch { process.exit(0); }
});
settings.json
Add the following to .claude/settings.json:
settings.json (Click to expand)
{
"hooks": {
"PreCompact": [
{
"hooks": [{ "type": "command", "command": "node .claude/hooks/save-context.js" }]
}
],
"SessionStart": [
{
"matcher": "compact",
"hooks": [{ "type": "command", "command": "node .claude/hooks/restore-context.js" }]
}
]
}
}
Points of Concern
I encountered several pitfalls while creating this. I'll record them here for anyone else doing the same.
How Is It in Actual Use?
I use Claude Code daily with this mechanism in place. My honest impressions:
| Without hook | With hook | |
|---|---|---|
| After compression | Complete reset. Remembers nothing | Remembers the last few dozen turns |
| Re-explanation | Everything from scratch | Just supplementing small details is enough |
| Experience | Exhausting every time | Much better than not having it |
However, it's not perfect:
- Only what fits in the 20KB range is restored. Details of long discussions get lost.
- What's restored is "fragments of raw conversation," not a "summary." It's incomplete for grasping the big picture.
- In sessions lasting several hours (design discussion → implementation → article creation, etc.), early parts of the conversation will drop off.
It changes "sudden amnesia" into being "a bit forgetful." But trying to solve everything with just this hook is unrealistic. In practice, it's more realistic to think about context management in layers:
| Layer | Mechanism | What it keeps |
|---|---|---|
| CLAUDE.md | Manual | Project policies and rules |
| PreCompact hook | Automatic | Recent work context (this mechanism) |
| Auto Memory | Automatic | Cross-session learning |
No single one is perfect, but they become practical when combined.
Summary
- The issue of context loss during Claude Code compression can be automatically restored using PreCompact + SessionStart hooks.
- Saving the transcript as-is results in too much noise. The key is to parse it and extract only the conversation.
- Writing in Node.js allows it to run on Windows, Mac, and Linux.
- It's not perfect, but work continuity improves significantly compared to not having the hook.
- You can use it as-is by copy-pasting the three pieces of code above (save-context.js, restore-context.js, and settings.json).
Discussion