Make Claude Code run a typecheck before it says "done"
"Done" from an assistant is a claim, not a result. If the code does not typecheck, the claim is wrong, and you find out later. A Stop hook turns the claim into a check.
How a Stop hook works
Claude Code fires Stop when Claude is about to end its turn. Your script runs tsc --noEmit. If it fails, the script exits with code 2 and prints the errors. Claude Code then sends that text back to Claude, which fixes the problems before finishing.
Three details keep it safe and quiet:
- Limit the output. Send only the first 25 lines of errors. A wall of text wastes context and rarely helps.
- Run at most once per turn. Claude Code sets
stop_hook_activewhen a Stop hook has already forced a continuation. Check it and exit 0, otherwise the hook can loop forever. - Fail soft. Skip if
tsccannot run, or if it takes longer than about two minutes. A slow or broken check should never trap the session.
// .claude/hooks/typecheck-on-stop.mjs (sketch)
import { readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
const input = JSON.parse(readFileSync(0, "utf8"));
if (input.stop_hook_active) process.exit(0);
const r = spawnSync("node_modules/.bin/tsc", ["--noEmit"], { encoding: "utf8", timeout: 120000 });
if (r.status === 0 || r.error) process.exit(0);
console.error(r.stdout.split("\n").slice(0, 25).join("\n"));
process.exit(2);
Use the project's local TypeScript from node_modules/.bin. That way the hook matches your own version rather than a global one.
When it feels like Claude will not stop
If Claude keeps working after it seemed finished, that is usually this hook sending type errors back. It only happens once per turn. If the check is too slow on a large project, you have two options: remove the Stop block from .claude/settings.json, or gate the script on an environment variable you set before starting claude, for example a SKIP_TYPECHECK=1 switch.
Pair it with a format hook
A second hook can run after each edit. It runs your project's own Prettier on the file Claude just changed, and does nothing at all when Prettier is not installed locally. This prevents formatting-only review noise and style drift.
One gotcha: if a file was never formatted, the first edit reformats the whole file. Format the codebase once in a separate commit to avoid noisy diffs.
Where this fits
A typecheck on stop catches type errors, not logic errors, and it does not replace tests, lint, a build, or review. For a fuller gate, a /preflight command that runs typecheck, lint, tests and build and prints one pass/fail table is a useful companion.
Remember the general rule: hooks are read at session start, so restart Claude Code after changing settings and confirm with /hooks.
Versions
A Stop hook like this depends only on the documented hook events and exit code 2. TypeScript 5.x and Prettier 3.x are the expected versions, and both are used only if present locally. Next.js itself does not matter to this hook, since it never imports it.
Going further
The Next.js Claude Code workflow pack includes a tested typecheck-on-stop hook, the Prettier hook and a /preflight command.
This article was written with AI assistance.