Claude Code hooks
The SessionEnd hook in Claude Code
SessionEnd fires when a session terminates. It cannot block, which makes it the right place for logging and cleanup.
Unlike Stop, this one cannot send Claude back to work, and it fires for reasons that have nothing to do with a reply finishing, clear and logout included. Match on reason rather than assuming every firing means the same kind of ending happened.
- When it fires
- When a session terminates, for any reason.
- Can it block?
- No. The session is going away regardless.
- Matcher
- Yes, on
reason - Matcher examples
clearresumelogoutprompt_input_exitbypass_permissions_disabledother
What it receives
The event arrives as JSON on standard input. These are the fields worth reading.
| Field | Type | What it is |
|---|---|---|
reason | string | Why the session ended. Also what the matcher filters on. |
session_id | string | Stable for the session. |
transcript_path | string | Path to the transcript, which still exists at this point. |
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 | Success. |
2 | No blocking behavior on this event. |
other | Non-blocking. |
What it can return
Nothing this hook returns changes what happens. Use it to write a log line, archive the transcript, release a lock, or clean up temporary files keyed on session_id.
No decision fields. This event exists for side effects.
Cleaning up state another hook left behind
Hooks that keep state across a session, like the edit tracker feeding the uncommitted check, write files keyed on session_id into a temporary directory. SessionEnd is where those get removed.
In settings.json
{
"hooks": {
"SessionEnd": [
{
"hooks": [
{ "type": "command", "command": "node ~/.claude/hooks/cleanup.mjs" }
]
}
]
}
}
The script
const input = JSON.parse(require('fs').readFileSync(0, 'utf8')) || {};
const id = String(input.session_id || '').replace(/[^\w-]/g, '');
if (id) require('fs').rmSync(`/tmp/claude-state/${id}.txt`, { force: true });
process.exit(0);
Sanitize session_id before putting it in a path. It arrives as data from outside your script.
The catch
Do not put anything here you actually need to happen. A session that ends because the process died does not get to run its SessionEnd hook, so treat cleanup here as tidying rather than as a guarantee, and expire old state on a timer as well.