jared hebb ~ %

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
manual auto

What it receives

The event arrives as JSON on standard input. These are the fields worth reading.

FieldTypeWhat it is
triggerstringmanual when you ran the command, auto when the window filled up. Also what the matcher filters on.
session_idstringStable for the session.
transcript_pathstringPath to the full transcript, before it is compacted.
cwdstringWorking 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.

ExitWhat happens
0Let compaction proceed.
2Block compaction.
otherNon-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.