Claude Code hooks
The PreCompact hook in Claude Code
PreCompact fires before context compaction and can block it. Matches on manual or auto so you can treat the two differently.
The two triggers say different things about intent. Manual means someone asked for it and probably knows what they are giving up; auto means the window simply filled up with nobody deciding anything, which is the trigger worth treating with more care and the one most save-before-compact examples match on.
- When it fires
- Before the context window is compacted.
- Can it block?
- Yes. Exit 2 blocks the compaction.
- Matcher
- Yes, on
trigger - Matcher examples
manualauto
What it receives
The event arrives as JSON on standard input. These are the fields worth reading.
| Field | Type | What it is |
|---|---|---|
trigger | string | manual when you ran the command, auto when the window filled up. Also what the matcher filters on. |
session_id | string | Stable for the session. |
transcript_path | string | Path to the full transcript, before it is compacted. |
cwd | string | Working directory. |
Exit codes
The exit code is the decision. Anything the script writes to standard error on a blocking exit is what Claude gets told.
| Exit | What happens |
|---|---|
0 | Let compaction proceed. |
2 | Block compaction. |
other | Non-blocking error. |
What it can return
Blocking compaction is rarely what you want, since the alternative to compacting a full context window is not having one. The useful move is to exit 0 and use the event to save something first.
{
"decision": "block",
"reason": "string"
}
Saving the full transcript before it is summarized
Compaction replaces the detail with a summary. If a session has been running for hours, the detail is worth keeping on disk even though the model no longer carries it.
In settings.json
{
"hooks": {
"PreCompact": [
{
"matcher": "auto",
"hooks": [
{ "type": "command", "command": "node ~/.claude/hooks/archive-transcript.mjs" }
]
}
]
}
}
The script
const fs = require('fs');
const path = require('path');
const input = JSON.parse(fs.readFileSync(0, 'utf8')) || {};
if (!input.transcript_path) process.exit(0);
const dir = path.join(require('os').tmpdir(), 'transcripts');
try {
fs.mkdirSync(dir, { recursive: true }); // nothing else creates this
fs.copyFileSync(input.transcript_path, path.join(dir, `${input.session_id}-${Date.now()}.jsonl`));
} catch (e) {
console.error('Could not archive the transcript: ' + e.message);
}
process.exit(0);
Match auto to archive only the compactions you did not ask for.
The catch
There is a matching PostCompact event with the same trigger matcher, which is where you re-inject anything the summary dropped that the rest of the session still needs.