Notebook
Note
Download Markdown

Hooks

5 min read

A hook is a small script that Claude Code runs automatically at a fixed moment — when a session starts, when a session ends, or just before it runs a particular kind of command. You configure them in settings.json and then forget about them.

Hooks matter more than they sound, and here’s why.

Why a hook beats a rule in your CLAUDE.md

Anything you write in CLAUDE.md is a should. The AI reads it, intends to follow it, and then a prompt comes along that looks simple, and it reaches for the familiar tool instead. This gets worse after the conversation compacts, because your instruction is now further back in the conversation than the thing it was meant to govern.

A hook runs outside the AI’s discretion entirely. It fires whether the AI remembered or not. So the rule of thumb is:

The three hooks I run

1. socket-check.sh — package safety

Intercepts every npm install, pnpm add, yarn add and pip install and runs a Socket.dev security check before anything is installed. It blocks the install and hands the findings back so the AI can report them to you in plain English. The reporting format it uses is in my Sample CLAUDE.md file.

Download: socket-check.sh

2. session-snapshot.sh — orient a new session for ~350 tokens

Runs at session start and on resume. It works out the current state rather than reading a file: branch, how many files are uncommitted, the last three commits, the titles of the last eight sessions, and any open blockers. About 1.4 KB of output.

This replaced a hook that ran cat context-log.md — which by then meant loading 447 KB, roughly 112,000 tokens, on every single session start and every resume. The full story is in Project memory - what your AI loads before you type.

Download: session-snapshot.sh

3. session-log-check.sh — did this session record what it did?

Runs when a session stops. It measures what was actually written and reports one of three things: nothing was added; more than 4 KB was added (about twice the “5–8 bullets” rule — this catches the transcript-length entries); or the log has passed 150 KB and it’s time to archive, naming the file to create.

The hook it replaced printed the same sentence every time. It couldn’t tell whether anything had been written, so it nagged after sessions that had already logged and stayed silent about sessions that wrote 72 KB. A rule nothing measures is a rule that gets ignored.

Download: session-log-check.sh

Three rules I learned the hard way

Warn, never block

The stop hook only prints. It never refuses to let the session end.

A blocking stop-hook can trap the AI in a loop — it tries to finish, gets refused, tries again. The diagnostic is the point, not the enforcement. Anything that genuinely must be blocked belongs at the point of action (like the package check above), not at the exit.

One file, not two copies

Claude Code merges hooks from your global settings and your project settings. If a project ships its own copy of a hook you also have installed globally, both fire and every session sees the output twice.

The obvious fix is to maintain two slightly different versions. Don’t — you’ll have drift and a “which version is this?” problem within a month. Instead, use one identical file in both places, and have the script compare its own location against the project’s copy and exit quietly if it isn’t the one that should run. Add a # VERSION: 3 line in the header so you can see at a glance whether a copy is stale.

The project copy also needs to be committed to GitHub, not just sitting in your global folder — cloud sessions clone the repo and never see your local machine.

Make it degrade quietly

These run in every project, including scratch folders with no context log and no git. A hook that throws an error in a throwaway folder is a hook you’ll delete within a week. Both of mine check whether there’s anything to report and say nothing if there isn’t.

If a hook doesn’t seem to be firing

Three silent failure modes, in the order worth checking:

  1. Matcher casing. In settings.json, the hook’s "matcher" must exactly match the tool name Claude Code uses — "Bash" with a capital B, not "bash". A lowercase matcher never matches, the hook simply never runs, and no error is shown.
  2. The executable bit. The script needs chmod +x. If you (or the AI) ever rewrite the file’s contents, check this again — a fresh write can silently drop it.
  3. Command wrapping. Claude Code’s Bash tool runs commands as cd "<folder>" && <your command>, not the bare command. If your detection pattern is anchored to the start of the text, it will never match. Split the command on &&, ||, ; and | first, then check each piece.

Test it directly rather than assuming. For the package hook, run pnpm add is-odd — a harmless, well-known test package — in a scratch folder and confirm it gets blocked with findings.

That advice generalises: whenever you add a guard, deliberately break the thing it’s supposed to catch and confirm it complains. Two of mine didn’t work the first time, and I only found out because I checked.

Related