Claude Media
Claude Codeでlefthookを使う — pre-commitの失敗を直させる設定

Claude Codeでlefthookを使う — pre-commitの失敗を直させる設定

lefthook.ymlのpre-commitでコミットが落ちたとき、Claude Codeに出力を読ませて直させる設定を、glob・stage_fixed・fail_textの実測つきでまとめます。迂回を止める方法も扱います。

lefthookのpre-commitが落ちたら、Claudeに出力を読ませて直させる

Claude Codeがgit commitを実行すると、lefthookを入れたリポジトリではpre-commitのジョブが走ります。Lintや型チェックが落ちればコミットは止まり、失敗の理由はgit commitの出力に出ます。この出力をClaudeに読ませて直させるのが、この記事の運用です。

必要なのは3つです。

  • lefthook.ymlに、落ちたときに原因が読める形でジョブを書く
  • 自動で直せるものはlefthookに直させ、残りをClaudeに任せる
  • --no-verifyやLEFTHOOK=0による迂回を、仕組みで止める

huskyとpre-commitで同じことをする手順は、huskyとpre-commitを併用して失敗したコミットを直させる記事にあります。ここではlefthook固有の設定に絞ります。

lefthookは何をしているのか

lefthookはGitフックの管理ツールです。lefthook.ymlを書いてlefthook installを実行すると、.git/hooks/にフックが作られます。そのフックは、実行されるとlefthook run {hook-name}を呼ぶだけの単純なスクリプトです。

導入の流れは短くまとまります。

手順

lefthookを入れてフックを有効にする

  1. 1

    パッケージを入れる

    Node.jsのプロジェクトなら、npmパッケージが最も手軽です。

  2. 2

    lefthook.ymlを書く

    プロジェクトの直下に置きます。.lefthook.ymlや.config/lefthook.ymlも使えます。

  3. 3

    フックを設置する

    lefthook installを実行します。npmパッケージならpostinstallで自動実行されます。

npm install --save-dev lefthook
npx lefthook install

Homebrew、winget、apt、scoopなどでの導入にも対応しています。npmパッケージにはlefthook、@evilmartians/lefthook、@evilmartians/lefthook-installerの3種類がありますが、後ろ2つはレガシー扱いで、将来は終了予定と案内されています。新規の導入はlefthookで足ります。

一度installしたあとは、lefthook.ymlを書き換えても再インストールは要りません。設定ファイルはGitフックが走るたびに読まれます。Claudeにlefthook.ymlを編集させる運用では、この性質が効きます。

Claudeが読める失敗を出すlefthook.yml

最小構成は次の形です。MarkdownにあるTODOの取り残しを検出して落とします。

# lefthook.yml
pre-commit:
  jobs:
    - name: check-todo
      glob: "*.md"
      run: "! grep -n TODO {staged_files}"
      fail_text: "TODO を消してから再コミットしてください"

jobsはlefthook 1.10.0で加わった書き方で、コマンドとスクリプトを同じ形で並べられます。従来のcommandsでも同じことが書けます。

runに渡せるファイルのテンプレート

runには、対象ファイルに置き換わるテンプレートを書けます。Claudeの変更に対して検査を絞るときの基本部品です。

テンプレート置き換わる中身
{staged_files}置き換わる中身これからコミットするステージ済みファイル
{push_files}置き換わる中身コミット済みで未pushのファイル
{all_files}置き換わる中身Gitが追跡している全ファイル
{files}置き換わる中身filesオプションのコマンド結果
{1}、{2}置き換わる中身Gitフックに渡された引数

ファイル数が多くてコマンド行の長さ制限を超えるときは、lefthookがファイルを分割して、複数回に分けて順に実行します。

globで検査対象を絞る

globで対象を拡張子やパスに絞ります。1.10.10からは、globをリストで書けます。

pre-commit:
  jobs:
    - name: lint
      run: yarn eslint {staged_files}
      glob:
        - "*.ts"
        - "*.tsx"

runにファイルのテンプレートを書かない場合、pre-commitでは{staged_files}、pre-pushでは{push_files}が自動で絞り込まれます。該当ファイルが残らなければ、ジョブは丸ごとスキップされます。npm run lintのようなコマンドでも、glob: "*.js"を付けると、JSがステージされていないコミットでは走りません。

落とし穴は**の意味です。lefthookの**は「1階層以上」のディレクトリに一致し、0階層を含みません。

glob: "src/**/*.js"  # src/file.js には一致しない
glob: "src/*.js"     # src/file.js だけに一致する

src直下のファイルと、サブディレクトリ配下の両方を拾いたいときは、パターンを2つ書くか、glob_matcher: doublestarで標準的な挙動にします。Claudeがsrc/**/*.tsと書いて、直下のファイルが検査されない設定を作ることがあるので、この違いはレビューで見る価値があります。

fail_textで次の行動を書く

fail_textは、ジョブが失敗したときに出す文言です。Claudeへの指示を書いておくと、失敗の出力から次の行動が読み取れます。

失敗したとき、コミットの出力はこうなる

一時リポジトリでlefthook v2.2.2を入れ、上のlefthook.ymlを置いて試しました。TODO fixと書いたa.mdをステージしてgit commitすると、コミットは終了コード1で止まります。出力は枠線を省くと次のとおりです。

🥊 lefthook  v2.2.2   hook:  pre-commit
┃  check-todo ❯
1:TODO fix
 
exit status 1
summary: (done in 0.01 seconds)
🥊 check-todo: TODO を消してから再コミットしてください (0.01 seconds)

ジョブ名、ジョブ自体の出力(grepが見つけた行)、fail_textの順に並びます。Claudeが読みたいのは「どのジョブが、どのファイルの何行目で落ちたか」です。ジョブ名をcheck-todoのように意味の分かる名前にし、検査コマンドが行番号付きで出力するようにしておくと、直す場所がそのまま分かります。

TODOを消して再度ステージすれば、✔️ check-todoと出てコミットが通ります。

落ちる理由は、大きく3つに分かれます。

失敗の型出力の手がかりClaudeの動き
指摘による失敗出力の手がかりジョブ名と、該当ファイル・行Claudeの動き指摘箇所を直して、ステージし直す
検査が書き換えて失敗出力の手がかり自動修正の後に差分が残るClaudeの動きgit diffで確認して、ステージし直す
環境の失敗出力の手がかりcommand not foundなどClaudeの動き直さず、原因を人に報告する

環境の失敗は、コードを直しても進みません。fail_textに「nodeをPATHに通してください」のような案内を書いておくと、Claudeが無駄な修正を重ねる前に止まれます。

stage_fixedで往復を減らす

フォーマッタやeslint --fixのように、検査がファイルを書き換えるジョブは、stage_fixed: trueが使えます。ジョブの実行後に、lefthookがgit addまで自動で行います。

pre-commit:
  parallel: true
  jobs:
    - name: prettier
      glob: "*.{js,ts,css,md}"
      run: yarn prettier --write {staged_files}
      stage_fixed: true

stage_fixedはpre-commitフックでだけ働きます。git addの呼び出しが失敗したときは、フックも失敗します。そうしないと、直っていない内容でコミットが通ってしまうためです。globやexcludeのフィルタは、git addの対象にも適用されます。

行末の空白を取るジョブで挙動を確かめました。perl -pi -e 's/[ \t]+$//' {staged_files}にstage_fixed: trueを付け、末尾に空白のあるc.mdをコミットします。コミットは1回で通り、保存された内容は空白が取れたものでした。ステージ後にglobへ一致しないd.txtだけをコミットしたときは、trim (skip) no files for inspectionと出てジョブが飛ばされました。

parallel: trueを付けると、ジョブが同時に走ります。既定は順次実行です。なおpiped: trueとparallel: trueを同時に指定するとエラーになります。

これは、直せるものをlefthookに、直せないものをClaudeに分担させる設計です。

くらべる

修正の分担

stage_fixed

lefthookに任せる

整形や空白の除去など、機械的に直せるもの。ステージまで自動で済み、コミットも1回で通ります。

失敗の出力

Claudeに任せる

型エラー、Lintの指摘、落ちたテストなど、意味を読んで直す必要があるもの。

fail_on_changesを使うと、ファイルが書き換わったときの扱いを選べます。never(既定)、always、ci、non-ciの4値があり、ciはCI環境変数があるときだけ、非ゼロで終了します。ローカルではstage_fixedでなめらかに通し、CIでは書き換えを失敗として検出する、という使い分けができます。

CLAUDE.mdに書く指示

失敗時の動きは、CLAUDE.mdに短く書いておきます。長文にせず、判断基準だけを置きます。

## コミットとlefthook
 
- コミットがlefthookで失敗したら、出力を最後まで読み、原因のコードを直して再コミットする
- 失敗したジョブだけを確かめたいときは `npx lefthook run pre-commit --job <ジョブ名>` を使う
- 自動修正でファイルが書き換わった場合は `git diff` で確認し、`git add` してから再コミットする
- `--no-verify`、`-n`、`LEFTHOOK=0` でフックを飛ばさない。通らないときは原因を報告する
- 同じ原因で2回続けて失敗したら、それ以上試さずに状況を説明する

CLAUDE.mdはClaudeにとってコンテキストであり、強制される設定ではありません。memoryの解説にも、作業を確実にブロックしたいならPreToolUse hookを使うよう明記されています。次の節の迂回対策は、その前提で組みます。

Claude自身にフックを回させる

コミットを試す前に、Claudeがフックを単独で実行できると、失敗の特定が早くなります。lefthook runは、Gitを介さずにフックを直接実行します。

# pre-commitを実行する(ステージ済みのファイルが対象)
npx lefthook run pre-commit
 
# 全ファイルを対象にする
npx lefthook run pre-commit --all-files
 
# 指定したファイルだけを対象にする
npx lefthook run pre-commit --file src/a.ts --file src/b.ts
 
# 特定のジョブだけを実行する
npx lefthook run pre-commit --job lint

--all-filesを付けると、{staged_files}のようなテンプレートが全ファイルに置き換わります。新しいジョブを足した直後に、既存のコード全体へ走らせて確かめるときに使えます。--all-filesと--fileを両方指定した場合は--all-filesが無視されます。

設定そのものの確認には、次の2つがあります。

  • lefthook validate: lefthook.ymlの構文を検証する
  • lefthook dump: extendsやremotesをマージしたあとの最終的な設定を出力する

Claudeにlefthook.ymlを編集させたあとは、lefthook validateを通す、とCLAUDE.mdに添えておくと、タイプミスの混入を防げます。

出力量を絞りたいときは、outputか環境変数LEFTHOOK_OUTPUTを使います。たとえばLEFTHOOK_OUTPUT="meta,success,summary"にすると、実行ログを省いて要約だけを出せます。ただし、Claudeに失敗の原因を読ませたいときは、failureを残してください。

迂回を止める

lefthookには、人間のための逃げ道が公式に用意されています。

  • LEFTHOOK=0 git commit: lefthookのフックをまるごと無効にする
  • git commit --no-verify(-n): Git自体がフックを実行しない

一時リポジトリでLEFTHOOK=0 git commitを試すと、TODOを含むファイルのコミットがそのまま通りました。Claudeにとっては、テストを通す近道としても使えてしまいます。

仕組みで止めるなら、PreToolUse hookです。PreToolUse hookはどのパーミッションモードよりも先に評価され、permissionDecision: "deny"を返すと、bypassPermissionsモードや--dangerously-skip-permissionsでもツールの実行をブロックします。exit code 2で止める場合は、stderrに書いた理由がClaudeへのフィードバックになります。

.claude/hooks/block-hook-bypass.shの検査パターンは、huskyの記事のスクリプトから次の1箇所を差し替えて流用できます。HUSKY=0|SKIP=の部分を、lefthookの迂回手段に置き換えます。

# huskyの記事のスクリプトの、迂回を検出する正規表現の行
if echo "$BARE" | grep -Eq '(--no-verify|[[:space:]]-[a-zA-Z]*n[a-zA-Z]*([[:space:]]|$)|LEFTHOOK=0|LEFTHOOK=false|core\.hooksPath)'; then

スクリプト全体と、git -c core.hooksPath=/dev/null commitのようにオプションを挟んだ形への対策は、PreToolUse hookの基本と、huskyの記事に載っています。引用符の中を取り除いてから判定する、といった落とし穴もそちらにあります。

権限側のdenyルールを足す方法もあります。

{
  "permissions": {
    "deny": [
      "Bash(git commit *--no-verify*)",
      "Bash(git push *--no-verify*)"
    ]
  }
}

この形で拾えるのは明白な--no-verifyだけです。-nの束ね書きやLEFTHOOK=0の前置は止まらないため、hookのスクリプトの保険として使います。

つまずきやすい点

  • pnpmでフックが入らない: npmパッケージのpostinstallが実行されません。pnpm-workspace.yamlのonlyBuiltDependenciesと、ルートのpackage.jsonのpnpm.onlyBuiltDependenciesにlefthookを足します
  • core.hooksPathが設定済み: postinstallは素のlefthook installを実行するため、メッセージを出して止まります。一度だけ意図してlefthook install --forceか--reset-hooks-pathを実行します
  • CIでinstallが走らない: CIがCI=trueを自動で設定する場合、LEFTHOOK=1 npm installのように指定するとpostinstallでフックを設置できます
  • lefthookが未導入のマシンでフックが黙って抜ける: assert_lefthook_installed: trueを書くと、実行ファイルが見つからないときにフックが終了コード1で失敗します
  • マージとリベースで重い検査が走る: ジョブにskip: [merge, rebase]を付けると、その間は飛ばせます。ブランチ単位ではskip: - ref: mainのように書けます
  • モノレポで対象がずれる: rootを指定しても、globはGitリポジトリの実際のルートから計算されます。rootは無視されます

Lintをコミット前でなくClaudeの編集直後に回す設計は、PostToolUse hookのLint自動修正にまとめています。Gitフックはそれでも残った指摘を拾う最後の関門として、役割を分けると見通しが立ちます。コミットメッセージの書式を縛るcommitlintの設定も、lefthookのcommit-msgフックから呼べます。フック全体の仕様はClaude Code Hooks完全ガイドで確認できます。

まとめ

lefthookは、設定を書き換えるたびに再インストールが要らず、globとテンプレートで対象を絞れるため、Claudeに触らせやすいGitフック管理です。整形のような機械的な修正はstage_fixedに寄せ、意味を読む修正だけをClaudeに回すと、コミットの往復が減ります。

一方で、LEFTHOOK=0と--no-verifyは公式の機能なので、CLAUDE.mdの指示だけでは止まりません。迂回を許さない運用にしたいなら、PreToolUse hookまで入れて初めて強制になります。

この記事を共有:XはてブLinkedIn