jared hebb ~ %

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
clear resume logout prompt_input_exit bypass_permissions_disabled other

What it receives

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

FieldTypeWhat it is
reasonstringWhy the session ended. Also what the matcher filters on.
session_idstringStable for the session.
transcript_pathstringPath to the transcript, which still exists at this point.
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
0Success.
2No blocking behavior on this event.
otherNon-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.