はじめに / 対象と前提
Claude Code に Write / Edit させたあと、毎回手で npm run lint を叩いていないだろうか。Hooks を使えば「ファイルが書き換わった瞬間」に lint と型チェックを自動で走らせて、エラーがあればその場で Claude に再修正させられる。
- 対象: Claude Code を業務コードに使い始めて、自分のローカルで型エラー・lint エラーを毎回手動で潰している人
- 前提: Claude Code v2.x 系 / Node.js 22.x / macOS or Linux / TypeScript プロジェクト (
prettiereslinttscがインストール済み) - 動作確認: Claude Code v2.0.27, Node.js v22.11,
typescript5.6,eslint9.x,prettier3.x
TL;DR
-
~/.claude/settings.jsonにhooks.PostToolUseを 1 ブロック足すだけで、Write / Edit のたびにprettier --write→tsc --noEmitを実行できる - 終了コード
2を返すと Claude 側に stderr の内容がフィードバックされて自動リトライ される (これがキモ) -
matcherは正規表現ではなく ツール名の完全一致リスト。Edit|Writeのような OR 表記は効かない
手順 / 動かし方
1. settings.json に Hook を書く
~/.claude/settings.json (プロジェクト単位なら .claude/settings.json) を開いて以下を追加。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/format-and-check.sh" }
]
},
{
"matcher": "Edit",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/format-and-check.sh" }
]
}
]
}
}
matcher には Write と Edit を 別ブロックで 書く。後述するが、ここを "Write|Edit" にしてもマッチしない。
2. Hook スクリプトを書く
~/.claude/hooks/format-and-check.sh を作る。
#!/usr/bin/env bash
set -uo pipefail
# Claude が stdin で JSON を流してくる。file_path を取り出す。
INPUT=$(cat)
FILE=$(printf '%s' "$INPUT" | python3 -c 'import sys, json; print(json.load(sys.stdin).get("tool_input", {}).get("file_path", ""))')
# 対象拡張子だけ処理 (関係ないファイルで失敗させない)
case "$FILE" in
*.ts|*.tsx|*.js|*.jsx) ;;
*) exit 0 ;;
esac
# プロジェクトルートに移動 (tsconfig.json を遡って探す)
DIR=$(dirname "$FILE")
while [ "$DIR" != "/" ] && [ ! -f "$DIR/tsconfig.json" ]; do
DIR=$(dirname "$DIR")
done
cd "$DIR" || exit 0
# 1) prettier で整形 (失敗しても止めない)
npx --no-install prettier --write "$FILE" >/dev/null 2>&1 || true
# 2) tsc で型チェック (失敗したら exit 2 で Claude にフィードバック)
if ! OUTPUT=$(npx --no-install tsc --noEmit 2>&1); then
echo "type check failed:" >&2
echo "$OUTPUT" >&2
exit 2
fi
exit 0
実行権限を忘れずに。
chmod +x ~/.claude/hooks/format-and-check.sh
3. 動作確認
Claude Code を再起動して、適当な .ts ファイルを Edit させる。型エラーになるコードをわざと書かせると、stderr が Claude 側に戻って 自分から再修正に入る のが確認できる。
> // 型を意図的に間違える
> const n: number = "hello";
[hook] type check failed:
[hook] src/foo.ts:1:7 - error TS2322: Type 'string' is not assignable to type 'number'.
[Claude] Hook がエラーを返したので修正します...
ハマりどころ
matcher は正規表現ではない
公式ドキュメントには「matcher pattern」と書いてあって Write|Edit で動きそうに見えるが、内部実装は 完全一致。ブロックを分けて書く必要がある。
exit 1 では Claude に通知されない
通常のシェルスクリプトの感覚だと「異常終了 = 1」を返したくなるが、Claude Code の Hook 仕様では exit 2 だけが「Claude にフィードバックする」モード になっている。1 を返すと Claude 側からは「Hook がコケた」としか見えず、内容が無視される。
| 終了コード | 挙動 |
|---|---|
| 0 | 成功扱い、何も起きない |
| 1 | Hook 失敗扱い、stderr は Claude には渡らない |
| 2 | stderr が Claude に渡され、Claude が自動でリトライ |
巨大プロジェクトで毎回 tsc が走ると遅い
tsc --noEmit をルートで叩くと、ファイル 1 個の Edit でプロジェクト全体の型チェックが走って数秒〜数十秒待たされる。変更ファイル + その依存だけ を見る tsc --noEmit --incremental か、tsgo のような代替を検討した方が良い。実測で 8 秒 → 0.4 秒まで縮んだ。
Hook 内で npx が見つからない
launchd や IDE 経由で起動した Claude Code は PATH が痩せていて、npx がないことがある。スクリプト冒頭に export PATH="$HOME/.nvm/versions/node/v22.11.0/bin:$PATH" のような行を足すか、絶対パスで呼ぶ。
背景・補足
Hook の本質は「Claude のツール呼び出しに対する pre/post の差し込み」で、PreToolUse で実行を止めることもできる。今回の Post の使い方が一番リスクが低くて入りやすい。
exit 2 のフィードバックループは「Claude が自分で修正に向かう」ので、自分が席を立っていても勝手に lint が通った状態まで持って行ってくれる。これは想像以上に効く。
まとめ
-
~/.claude/settings.jsonのhooks.PostToolUseにmatcher: "Write"と"Edit"を 別々に 足す - スクリプトは終了コード
2で stderr を返すと Claude が自動リトライしてくれる - 重い型チェックは
--incrementalかtsgoで逃げる - PATH と実行権限のハマりどころに注意
「保存したら自動で整形・型チェックが通った状態にしてくれる」状態は、Claude Code の体験を一段引き上げる。慣れてきたら PreToolUse で危険なコマンドをブロックする方向にも広げられる。