- Git
Git hooks・husky v9の設定方法|実行結果とlefthook比較
Git hooksとは、git commit や git push などの操作の前後にGitが自動で実行するスクリプトのことで、huskyはそのスクリプトをリポジトリ内の .husky/ フォルダで管理し、npm install した全員に同じフックを配る仕組みです。フックを使うと、「lintを通していないコードをコミットできない」「規約に合わないコミットメッセージを受け付けない」「テストが落ちる状態ではpushできない」といったルールを、チーム全員の手元で自動的に守れます。
この記事では、まずGit hooks 8種の早見表(タイミング・引数・止められるか)を示し、素の .git/hooks と core.hooksPath で最小例を動かしてから、husky 9.1.7 の現行手順(npx husky init)を、lint-staged 17.6.0・commitlint 21.2.3・pre-push でのテストと組み合わせて実際に実行します。コミットが止まる出力と通る出力を両方そのまま掲載し、--no-verify や HUSKY=0・CI・モノレポでの扱い、lefthook 2.1.15 と pre-commit 4.6.2(pip)との比較表、フックが動かない時の対処表まで、検索上位の記事にはない実測ベースでまとめました。バージョンはすべてnpm・PyPIで2026年10月1日に確認したものです。
Git hooksの早見表
まずは「どのフックが・いつ・何を受け取って・止められるか」の一覧です。コミットを止めたいなら pre-commit か commit-msg、pushを止めたいなら pre-push と覚えておくと、ほとんどの用途は足ります。
| フック | 呼ばれるタイミング | 受け取る引数 | 非ゼロ終了で止められるか | --no-verify で飛ばせるか |
主な用途 |
|---|---|---|---|---|---|
pre-commit |
git commit 直後、メッセージ入力の前 |
なし | 止められる(コミット中止) | 飛ばせる | lint・フォーマット・秘密情報チェック |
prepare-commit-msg |
既定メッセージ作成後、エディタ起動前 | $1 メッセージファイル、$2 由来(message / template / merge / squash / commit)、$3 コミットID(amend時) |
止められる | 飛ばせない | ブランチ名からチケット番号を自動挿入 |
commit-msg |
メッセージ確定後、コミット作成の直前 | $1 メッセージファイル(.git/COMMIT_EDITMSG) |
止められる | 飛ばせる | メッセージ規約(commitlint) |
post-commit |
コミット作成後 | なし | 止められない(通知用) | 対象外 | 通知・ログ |
pre-merge-commit |
git merge の自動マージ成功後、コミット前 |
なし | 止められる | 飛ばせる | マージコミット前の検査 |
pre-rebase |
git rebase 開始前 |
$1 上流ブランチ、$2 対象ブランチ(現在のブランチ以外を指定した時のみ) |
止められる | git rebase --no-verify で飛ばせる |
特定ブランチのrebase防止 |
post-checkout |
checkout / switch / clone / worktree add の後 |
$1 前のHEAD、$2 新しいHEAD、$3 フラグ(1=ブランチ切替、0=ファイル取り出し) |
止められない(終了コードに反映されるだけ) | 対象外 | 依存関係の再インストール案内 |
post-merge |
git merge / git pull の成功後 |
$1 squashマージなら1 |
止められない | 対象外 | lockファイル変更時の npm install 案内 |
pre-push |
git push でリモートに送る前 |
$1 リモート名、$2 URL。標準入力に <local ref> <local sha> <remote ref> <remote sha> が1行ずつ |
止められる(push中止) | git push --no-verify で飛ばせる |
テスト実行・mainへの直接push防止 |
post-rewrite |
commit --amend / rebase の後 |
$1 amend か rebase。標準入力に書き換え前後のSHA |
止められない | 対象外 | メモ・タグの追従 |
pre-receive / update / post-receive |
サーバー側で git push を受け取った時 |
標準入力または引数で更新される参照と前後のSHA | pre-receive と update は拒否できる |
飛ばせない | サーバー側のポリシー強制(GitHub等ではブランチ保護で代替) |
補足として、Git公式ドキュメント(githooks)に書かれている前提を3つ押さえておきます。
- フックは
$GIT_DIR/hooks/(通常は.git/hooks/)にある実行可能ファイルが対象で、git config core.hooksPathで場所を変えられます(Git 2.9以降) .git/hooks/はgit cloneでコピーされません。チームで共有するには、リポジトリ内のフォルダをcore.hooksPathに向けるか、husky・lefthook・pre-commit のようなツールで各自の環境にインストールする必要があります- クライアント側のフックはワーキングツリーのルートをカレントディレクトリとして実行されます
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Git hooks・husky v9の設定方法|実行結果とlefthook比較」
素のGit hooksで最小例を動かす
ツールを入れる前に、Gitが素で持っている仕組みを2通り(.git/hooks/ 直置きと core.hooksPath)で動かしておくと、huskyが内部で何をしているかがよくわかります。検証環境は git 2.43.0(Linux)です。
.git/hooks/pre-commit にスクリプトを置く
git init 直後の .git/hooks/ には pre-commit.sample など14個のサンプルがあり、.sample を外した名前の実行可能ファイルを置くだけでフックは有効になります。ステージ済みの .js に console.log が残っていたらコミットを止める最小例です。
#!/bin/sh
# .git/hooks/pre-commit
# ステージ済みの .js ファイルに console.log が残っていたらコミットを止める
files=$(git diff --cached --name-only --diff-filter=ACMR -- '*.js')
[ -z "$files" ] && exit 0
if grep -Hn 'console\.log' $files; then
echo "pre-commit: console.log が残っています。削除してからコミットしてください。"
exit 1
fi
chmod +x .git/hooks/pre-commit
git add util.js
git commit -m "add label()"
実行結果(止まる場合)です。フックが非ゼロで終了すると、Gitは何も言わずにコミットを中止します。メッセージはフック自身が出したものです。
util.js:2: console.log("label", n);
pre-commit: console.log が残っています。削除してからコミットしてください。
exit code: 1
console.log を消して再度コミットすると、そのまま通ります。
[main 9e8d2fe] add label()
1 file changed, 3 insertions(+)
create mode 100644 util.js
exit code: 0
実行権限が付いていないと、フックは無視されます。この場合Gitがヒントを出してくれるので、「フックが動かない」と感じたらまずこの行を探します(chmod -x で再現)。
hint: The '.git/hooks/pre-commit' hook was ignored because it's not set as executable.
hint: You can disable this warning with `git config advice.ignoredHook false`.
[main fba1c37] add label()
core.hooksPath でリポジトリ内のフォルダを使う
.git/hooks/ はcloneで配られないため、フックをリポジトリ内の .githooks/ に置いてコミットし、各自が git config core.hooksPath .githooks を1回実行するのが、ツールなしで共有する定番です。commit-msg と prepare-commit-msg を置いて、引数の中身も確認します。
#!/bin/sh
# .githooks/commit-msg ($1 = コミットメッセージが入った一時ファイル)
msg=$(head -n1 "$1")
if ! echo "$msg" | grep -Eq '^(feat|fix|docs|refactor|test|chore)(\(.+\))?: .+'; then
echo "commit-msg: 1行目は 'feat: 内容' の形式にしてください。受け取った1行目: $msg"
exit 1
fi
#!/bin/sh
# .githooks/prepare-commit-msg ($1=メッセージファイル, $2=由来, $3=コミットID)
echo "prepare-commit-msg: \$1=$1 \$2=$2 \$3=$3"
chmod +x .githooks/*
git config core.hooksPath .githooks
git add version.js .githooks
git commit -m "version file"
prepare-commit-msg: $1=.git/COMMIT_EDITMSG $2=message $3=
commit-msg: 1行目は 'feat: 内容' の形式にしてください。受け取った1行目: version file
exit code: 1
-m で渡したので $2 は message、amendではないので $3 は空です。feat: を付けると通ります。
prepare-commit-msg: $1=.git/COMMIT_EDITMSG $2=message $3=
[main 3b37873] feat: add version file
3 files changed, 11 insertions(+)
create mode 100755 .githooks/commit-msg
create mode 100755 .githooks/prepare-commit-msg
create mode 100644 version.js
exit code: 0
core.hooksPath を設定すると .git/hooks/ は見られなくなる点と、設定はリポジトリごと(.git/config)なのでcloneした人が各自実行する必要がある点が、この方式の運用上のポイントです。husky v9は、まさにこの core.hooksPath を npm install 時に自動設定するツールです。
husky v9の設定方法(npx husky init)
インストールと初期化
前提はNode.js 18以上(検証は Node 22.22.2 / npm 10.9.7 / husky 9.1.7)。v9では npx husky init の1コマンドで初期設定が完了します。
npm i -D husky
npx husky init
husky init が行うのは次の3つです。
| 作られる・変わるもの | 内容 |
|---|---|
package.json の scripts.prepare |
"prepare": "husky" を追加(npm install のたびに husky が走り、フックを再インストール) |
.husky/pre-commit |
npm test の1行(pnpmなら pnpm test、bunなら bun test など、実行したパッケージマネージャに合わせて生成) |
.husky/_/(gitignore済み) |
実体のフック群。git config core.hooksPath .husky/_ も同時に設定される |
実行後の状態です。
{
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prepare": "husky"
},
"devDependencies": {
"husky": "^9.1.7"
}
}
$ cat .husky/pre-commit
npm test
$ git config core.hooksPath
.husky/_
$ ls .husky/_
.gitignore applypatch-msg commit-msg h husky.sh post-applypatch post-checkout
post-commit post-merge post-rewrite pre-applypatch pre-auto-gc pre-commit
pre-merge-commit pre-push pre-rebase prepare-commit-msg
.husky/_/ の各フックはすべて同じ2行で、h というスクリプトが「同名のファイル .husky/<フック名> があれば sh -e で実行する」だけの仕組みです。.husky/_/ は .gitignore(中身は *)で無視され、コミットされるのは .husky/pre-commit などの中身だけです。
#!/usr/bin/env sh
. "$(dirname "$0")/h"
h の中身(9.1.7)を読むと、この記事で後述する挙動がすべて説明できます(右側のコメントは筆者が追加したものです)。
#!/usr/bin/env sh
[ "$HUSKY" = "2" ] && set -x # HUSKY=2 でデバッグ出力
n=$(basename "$0")
s=$(dirname "$(dirname "$0")")/$n # .husky/<フック名>
[ ! -f "$s" ] && exit 0 # ファイルがなければ何もしない
if [ -f "$HOME/.huskyrc" ]; then
echo "husky - '~/.huskyrc' is DEPRECATED, please move your code to ~/.config/husky/init.sh"
fi
i="${XDG_CONFIG_HOME:-$HOME/.config}/husky/init.sh"
[ -f "$i" ] && . "$i" # 個人用の初期化ファイルを読む
[ "${HUSKY-}" = "0" ] && exit 0 # HUSKY=0 でスキップ
export PATH="node_modules/.bin:$PATH" # npx なしでもコマンドが見つかる
sh -e "$s" "$@" # -e: 途中で失敗したら即終了
c=$?
[ $c != 0 ] && echo "husky - $n script failed (code $c)"
[ $c = 127 ] && echo "husky - command not found in PATH=$PATH"
exit $c
最初のコミットで動作確認
npm test が「テスト未設定」で exit 1 するので、初期状態ではコミットが止まります。これが動作確認になります。
$ git add -A && git commit -m "chore: add husky"
> husky-demo@1.0.0 test
> echo "Error: no test specified" && exit 1
Error: no test specified
husky - pre-commit script failed (code 1)
exit code: 1
HUSKY=0 を付けるとフックを飛ばしてコミットできます(初回だけこの方法で入れると、履歴がきれいになります)。
$ HUSKY=0 git commit -m "chore: add husky"
[main 11c3157] chore: add husky
3 files changed, 39 insertions(+), 2 deletions(-)
create mode 100644 .husky/pre-commit
create mode 100644 package-lock.json
v8以前の手順との違い
検索上位にはv6〜v8時代の手順が多く残っています。v9で削除・非推奨になったコマンドを打つと、9.1.7では次のメッセージが出ます。
| v8以前の書き方 | v9(9.1.7)での結果 | v9の書き方 |
|---|---|---|
npx husky install |
husky - install command is DEPRECATED(動作はする) |
npx husky(prepare スクリプトが呼ぶ) |
npx husky add .husky/pre-commit "npm test" |
husky - add command is DEPRECATED で終了コード1 |
echo "npm test" > .husky/pre-commit |
npx husky set ... / npx husky uninstall |
同じく DEPRECATED で終了コード1 | ファイルを直接編集 / git config --unset core.hooksPath |
"prepare": "husky install" |
動くが非推奨 | "prepare": "husky" |
フック先頭の #!/usr/bin/env sh と . "$(dirname -- "$0")/_/husky.sh" |
実行のたびに husky - DEPRECATED ... They WILL FAIL in v10.0.0 と表示 |
2行を削除してコマンドだけ書く |
pre-commitでlint-stagedを動かす
lint-stagedは、ステージされたファイルだけを対象にlintやフォーマッタを走らせるツールです。プロジェクト全体にESLintをかけると数十秒かかる場合でも、変更した数ファイルなら1〜2秒で終わります。lint-staged 17.6.0 は Node 22.22.1 以上が必要です。
npm i -D lint-staged eslint @eslint/js prettier # lint-staged 17.6.0 / eslint 10.11.0 / prettier 3.9.9
echo "npx lint-staged" > .husky/pre-commit
.lintstagedrc.json(キーがglob、値が実行コマンド。配列なら順に実行)と、no-unused-vars と no-console をエラーにした eslint.config.js です。
{
"*.js": ["eslint --fix", "prettier --write"],
"*.{json,md,yml}": "prettier --write"
}
// eslint.config.js(package.json に "type": "module" がある前提)
import js from "@eslint/js";
export default [
js.configs.recommended,
{
files: ["**/*.js"],
languageOptions: {
ecmaVersion: 2024,
sourceType: "module",
globals: { console: "readonly" },
},
rules: {
"no-unused-vars": "error",
"no-console": "error",
},
},
];
コミットが止まる出力
未使用変数と console.log を含む src.js をコミットしてみます。
export function total(items) {
const unused = 1;
console.log(items);
return items.reduce((sum, i) => sum + i.price * i.qty, 0);
}
$ git add src.js && git commit -m "feat: add total()"
⋯ Backing up original state…
✔ Done backing up original state (aaf8e48)!
⋯ Running tasks for staged files…
*.js — 1 file
⋯ eslint --fix
⋯ prettier --write
✖ eslint --fix
↓ prettier --write
✖ Failed to run tasks for staged files!
↓ Skipped staging changes from tasks…
⋯ Reverting to original state because of errors…
✔ Done reverting to original state!
⋯ Cleaning up temporary files…
✔ Done cleaning up temporary files!
✖ eslint --fix:
/path/to/husky-demo/src.js
2:9 error 'unused' is assigned a value but never used no-unused-vars
3:3 error Unexpected console statement no-console
✖ 2 problems (2 errors, 0 warnings)
husky - pre-commit script failed (code 1)
exit code: 1
出力の読み方は次のとおりです(パスは短縮しています)。
- Backing up original state:lint-stagedは実行前に作業ツリーの状態を退避し、失敗したら Reverting to original state で元に戻します。
--fixが途中まで書き換えた状態が残る心配はありません ✖ eslint --fixで止まった後の↓ prettier --writeは「実行されなかった」印です- 最後の
husky - pre-commit script failed (code 1)はhuskyのhが付けた行で、どのフックが何番で失敗したかがわかります
コミットが通る出力(自動整形がステージされる)
エラーを直し、わざとインデントを4つ・セミコロンなしにした状態でコミットすると、prettierが整形した結果が自動でステージに反映されてコミットされます。
$ git add src.js && git commit -m "feat: add total()"
⋯ Backing up original state…
✔ Done backing up original state (f21931a)!
⋯ Running tasks for staged files…
*.js — 1 file
⋯ eslint --fix
⋯ prettier --write
✔ eslint --fix
✔ prettier --write
✔ Done running tasks for staged files!
⋯ Staging changes from tasks…
✔ Done staging changes from tasks!
⋯ Cleaning up temporary files…
✔ Done cleaning up temporary files!
[main ab12ad3] feat: add total()
1 file changed, 3 insertions(+)
create mode 100644 src.js
exit code: 0
$ git show HEAD:src.js
export function total(items) {
return items.reduce((sum, i) => sum + i.price * i.qty, 0);
}
lint-stagedの設定でつまずきやすい点
| やりたいこと | 書き方・注意 |
|---|---|
| 設定ファイルの置き場所 | package.json の "lint-staged"、.lintstagedrc(JSON/YAML)、.lintstagedrc.json、lint-staged.config.js(ESM/CJS)、.lintstagedrc.mjs など |
| 複数コマンドを順に実行 | 値を配列にする:["eslint --fix", "prettier --write"] |
| TypeScriptの型チェック | "*.ts": "tsc --noEmit" と書くとファイル名が渡されて tsconfig.json が無視される。JS設定で関数にしてファイル名を捨てる:"*.ts": () => "tsc -p tsconfig.json --noEmit" |
| ステージ以外も含めて全ファイル | npx lint-staged --all(--no-stash と --allow-empty を含意) |
| 直列で実行したい | npx lint-staged --concurrent false |
| 失敗しても全タスク走らせたい | --continue-on-error |
| デバッグ | npx lint-staged --debug。フック全体は HUSKY=2 git commit で set -x 相当の出力 |
.husky/pre-commit に npx lint-staged と書きましたが、h が node_modules/.bin を PATH に足すので lint-staged だけでも動きます。npx を付けておくと、フック以外から手で実行した時も同じ書き方で済みます。
commit-msgでcommitlintを動かす
commitlintは、コミットメッセージが「type(scope): subject」形式(Conventional Commits)に沿っているかを検査するツールです。commit-msg フックは $1 にメッセージファイルのパスを受け取るので、それを --edit に渡します。
npm i -D @commitlint/cli @commitlint/config-conventional # 21.2.3
echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg
// commitlint.config.js("type": "module" の場合。CJSなら module.exports = {...})
export default { extends: ["@commitlint/config-conventional"] };
npx --no は「未インストールならレジストリから取りに行かずに失敗する」指定で、フックの中でネットワークアクセスが発生しないようにするためのものです。
止まるメッセージと通るメッセージ
$ git commit -m "update tax"
⧗ --- input ---
update tax
✖ subject may not be empty [subject-empty]
✖ type may not be empty [type-empty]
✖ found 2 problems, 0 warnings
ⓘ Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint
husky - commit-msg script failed (code 1)
exit code: 1
$ git commit -m "Feat: Add tax rate."
⧗ --- input ---
Feat: Add tax rate.
✖ subject must not be sentence-case [subject-case]
✖ subject may not end with full stop [subject-full-stop]
✖ type must be lower-case [type-case]
✖ type must be one of [build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test] [type-enum]
✖ found 4 problems, 0 warnings
ⓘ Get help: https://github.com/conventional-changelog/commitlint/#what-is-commitlint
husky - commit-msg script failed (code 1)
exit code: 1
$ git commit -m "feat(order): add tax rate"
[main b26b527] feat(order): add tax rate
1 file changed, 1 insertion(+)
create mode 100644 tax.js
exit code: 0
[type-enum] のようにルール名が角かっこで出るので、緩めたい時は commitlint.config.js の rules にそのルール名で上書きします(例:"subject-case": [0] で無効化)。メッセージの書き方自体の規約は、コミットメッセージの記事に譲ります。
pre-pushでテストを動かす
pushの直前にテストを走らせると、落ちるコードがリモートに上がることを手元で止められます。pre-push はコミットのたびには走らないので、数十秒かかるテストでも置きやすいフックです。例では外部ライブラリ不要の node --test を使います。
npm pkg set scripts.test="node --test --test-reporter=spec"
echo "npm test" > .husky/pre-push
// src.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { total } from "./src.js";
test("total() sums price * qty", () => {
assert.equal(total([{ price: 100, qty: 2 }, { price: 50, qty: 1 }]), 240);
});
期待値をわざと間違えた(正しくは250)状態でpushすると止まります(スタックトレースは一部省略)。
$ git push -u origin main
> husky-demo@1.0.0 test
> node --test --test-reporter=spec
✖ total() sums price * qty (1.17386ms)
ℹ tests 1
ℹ pass 0
ℹ fail 1
✖ failing tests:
test at src.test.js:5:1
✖ total() sums price * qty (1.17386ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
250 !== 240
at TestContext.<anonymous> (/path/to/husky-demo/src.test.js:6:10)
husky - pre-push script failed (code 1)
error: failed to push some refs to '../remote.git'
exit code: 1
error: failed to push some refs to は、フックが非ゼロで終わった時にGitが出す定型文です。テストを直すと通ります(ℹ の集計行は一部省略)。
$ git push
> husky-demo@1.0.0 test
> node --test --test-reporter=spec
✔ total() sums price * qty (0.793033ms)
ℹ tests 1
ℹ pass 1
ℹ fail 0
To ../remote.git
edd4bd0..b6622ad main -> main
exit code: 0
ここまでの .husky/ の完成形は次の3ファイルです。
.husky/pre-commit → npx lint-staged
.husky/commit-msg → npx --no -- commitlint --edit "$1"
.husky/pre-push → npm test
フックをスキップする方法とCI・モノレポでの設定
スキップの方法の早見表
| 方法 | 効く範囲 | 使いどころ |
|---|---|---|
git commit --no-verify(-n) |
pre-commit と commit-msg(prepare-commit-msg は飛ばせない) |
WIPコミットを急いで残したい時 |
git push --no-verify |
pre-push |
同上 |
HUSKY=0 git commit ... |
huskyが管理する全フック(h の先頭で exit 0) |
--no-verify を持たないコマンドにも効く |
export HUSKY=0 … unset HUSKY |
シェルセッションの間 | まとめて複数操作する時 |
~/.config/husky/init.sh に export HUSKY=0 |
その端末で常に | GUIクライアントから止めたい時 |
git -c core.hooksPath=/dev/null commit ... |
Gitのフック機構そのもの | ツールに関係なく1回だけ完全に無効化 |
実行例です。--no-verify を付けると、lintに通らないファイルも規約外のメッセージもそのまま入ります。
$ git add bad.js && git commit -m "wip" --no-verify
[main 1dbabeb] wip
1 file changed, 2 insertions(+)
create mode 100644 bad.js
exit code: 0
CI(GitHub Actions)ではフックを入れない
CIではコミットしないのでフックは不要です。husky公式が案内するのは環境変数 HUSKY: 0 の設定で、prepare が走っても husky は HUSKY=0 skip install と表示して何もしません。
# .github/workflows/ci.yml
name: ci
on: [push, pull_request]
env:
HUSKY: 0
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm test
npm ci の付け方によって prepare の挙動が変わるので、cloneしたての状態で試した結果を表にします。--omit=dev は「devDependenciesのhuskyがないのに prepare は走る」ため失敗するのが最も多いつまずきです。
| コマンド | 結果(実測) | core.hooksPath |
|---|---|---|
npm ci |
> husky が走り、フックがインストールされる |
.husky/_ |
HUSKY=0 npm ci |
HUSKY=0 skip install と表示して終了コード0 |
未設定 |
npm ci --ignore-scripts |
prepare 自体が走らない |
未設定 |
npm ci --omit=dev(本番用) |
sh: 1: husky: not found → npm error code 127 でインストール失敗 |
未設定 |
"prepare": "husky || true" にして npm ci --omit=dev |
husky: not found は出るが終了コード0で続行 |
未設定 |
本番イメージのビルドなど --omit=dev を使う場面では、HUSKY=0 を渡すか、"prepare": "husky || true" にしておくと安全です。もう少し丁寧にするなら、公式の例のように .husky/install.mjs で CI や NODE_ENV=production の時に process.exit(0) する方法もあります。
モノレポ(package.jsonがサブディレクトリにある場合)
huskyはカレントディレクトリに .git があることを前提にしているので、frontend/ の中で npx husky を実行すると次のように出ます。
$ cd frontend && npx husky
.git can't be found
公式の対処は、prepare でリポジトリのルートに移動してから .husky の場所を引数で渡す方法です。フック側では逆に frontend/ に降りてから実行します。
{
"scripts": {
"prepare": "cd .. && husky frontend/.husky"
}
}
# frontend/.husky/pre-commit
cd frontend
npx lint-staged
$ npm run prepare
> frontend@1.0.0 prepare
> cd .. && husky frontend/.husky
$ git config core.hooksPath
frontend/.husky/_
$ git commit -m "chore: husky in subdir"
> frontend@1.0.0 test
> echo frontend-test-ok
frontend-test-ok
[main (root-commit) 3604463] chore: husky in subdir
lint-staged側は、各パッケージに .lintstagedrc.json を置くと、ステージされたファイルに最も近い設定ファイルが使われ、その設定のディレクトリでコマンドが実行されるので、パッケージごとに違うlint設定でも1つの pre-commit から扱えます。
lefthook・pre-commit(pip)との比較
huskyはNode.jsプロジェクトの定番ですが、Go製の lefthook(並列実行・言語非依存)と Python製の pre-commit(Pythonプロジェクトの定番)も広く使われています。同じ「ESLint + Prettier + commitlint」を lefthook で、「ruff」を pre-commit で動かして比べます。
lefthook 2.1.15 の最小設定
npmからは npm i -D lefthook で入り、postinstallで lefthook install が自動実行されます(バイナリはプラットフォーム別パッケージ lefthook-linux-x64 などから取得)。設定は lefthook.yml 1ファイルで、フック名の下に jobs を並べ、parallel: true で並列実行できます。
# lefthook.yml(lefthook 2.1.15)
pre-commit:
parallel: true
jobs:
- name: eslint
glob: "*.js"
run: npx eslint --fix {staged_files}
stage_fixed: true
- name: prettier
glob: "*.{js,json,md,yml}"
run: npx prettier --write {staged_files}
stage_fixed: true
commit-msg:
jobs:
- name: commitlint
run: npx --no -- commitlint --edit {1}
pre-push:
jobs:
- name: test
run: npm test
{staged_files} がステージ済みファイル、{1} がフックの第1引数、stage_fixed: true が「修正結果を自動でステージ」に相当します。lefthook.yml を書き換えたら npx lefthook install で同期します。
$ npx lefthook install
sync hooks: ✔️(pre-commit, commit-msg, pre-push)
huskyと違ってcore.hooksPath は変えず、.git/hooks/pre-commit などに「lefthookを探して lefthook run pre-commit を呼ぶ」ラッパーを書き込みます。同じ src.js をコミットした出力です(色コードは除去)。
$ git commit -m "add total"
╭───────────────────────────────────────╮
│ lefthook v2.1.15 hook: pre-commit │
╰───────────────────────────────────────╯
┃ prettier ❯
src.js 70ms (unchanged)
┃ eslint ❯
/path/to/lefthook-demo/src.js
2:9 error 'unused' is assigned a value but never used no-unused-vars
3:3 error Unexpected console statement no-console
✖ 2 problems (2 errors, 0 warnings)
exit status 1
────────────────────────────────────
summary: (done in 2.00 seconds)
✓ prettier (1.70 seconds)
✗ eslint (1.99 seconds)
exit code: 1
コードを直してメッセージが規約外のままだと、pre-commit は通り commit-msg で止まります。
$ git commit -m "add total"
(pre-commit の枠は省略:✓ prettier ✓ eslint)
╭───────────────────────────────────────╮
│ lefthook v2.1.15 hook: commit-msg │
╰───────────────────────────────────────╯
┃ commitlint ❯
⧗ --- input ---
add total
✖ subject may not be empty [subject-empty]
✖ type may not be empty [type-empty]
✖ found 2 problems, 0 warnings
exit status 1
summary: (done in 0.81 seconds)
✗ commitlint (0.81 seconds)
exit code: 1
$ git commit -m "feat: add total()"
(pre-commit・commit-msg とも ✓)
[main cb92b7f] feat: add total()
1 file changed, 3 insertions(+)
create mode 100644 src.js
exit code: 0
スキップは LEFTHOOK=0 git commit ...、手動実行は npx lefthook run pre-commit --all-files、有効な設定の確認は npx lefthook dump です。
pre-commit 4.6.2(pip)の最小設定
pre-commit(ツール名がフック名と同じなので紛らわしいのですが)は、.pre-commit-config.yaml に「どのリポジトリの・どのバージョンの・どのフックを使うか」を書き、ツールが各フックの実行環境を自動で用意する仕組みです。Pythonプロジェクトの定番で、pre-commit sample-config がひな形を出してくれます。
pip install pre-commit # 4.6.2
pre-commit sample-config > .pre-commit-config.yaml
pre-commit install # .git/hooks/pre-commit に書き込む
pre-commit run --all-files # 手動で全ファイルに実行
pre-commit autoupdate # rev を最新タグに更新
一般的な設定は次のように公開リポジトリを指定します(この環境からはGitHubに接続できないため、この形は未実行です)。
# .pre-commit-config.yaml(一般的な形。rev は pre-commit autoupdate で更新)
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.16.8
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
同じ内容を、手元にインストール済みのコマンドを呼ぶ repo: local で書いて実行しました。
# .pre-commit-config.yaml(repo: local 版。pre-commit 4.6.2 / ruff 0.16.9 で実行)
repos:
- repo: local
hooks:
- id: ruff-check
name: ruff check
entry: ruff check --fix
language: system
types: [python]
- id: ruff-format
name: ruff format
entry: ruff format
language: system
types: [python]
$ pre-commit install
pre-commit installed at .git/hooks/pre-commit
$ git add -A && git commit -m "add total()"
ruff check...............................................................Failed
- hook id: ruff-check
- exit code: 1
- files were modified by this hook
F841 Local variable `unused` is assigned to but never used
--> app.py:3:5
|
2 | def total(items):
3 | unused = 1
| ^^^^^^
4 | return sum(i["price"] * i["qty"] for i in items)
|
help: Remove assignment to unused variable `unused`
Found 2 errors (1 fixed, 1 remaining).
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).
ruff format..............................................................Failed
- hook id: ruff-format
- files were modified by this hook
1 file reformatted
exit code: 1
pre-commitは「フックがファイルを書き換えたら、それだけで Failed 扱い」です(files were modified by this hook)。未使用importの削除と整形は自動で済んでいるので、残った unused = 1 を消して git add し直せば通ります。
$ git add -A && git commit -m "add total()"
ruff check...............................................................Passed
ruff format..............................................................Passed
[main (root-commit) 75fbda2] add total()
3 files changed, 17 insertions(+)
特定フックだけ飛ばすには SKIP=ruff-check git commit ...、全部飛ばすなら git commit --no-verify です。
4方式の比較表
| 観点 | 素の core.hooksPath |
husky 9.1.7 | lefthook 2.1.15 | pre-commit 4.6.2 |
|---|---|---|---|---|
| インストール | 不要(各自 git config core.hooksPath .githooks) |
npm i -D husky(prepare で自動) |
npm i -D lefthook / brew / go install / pip / gem など |
pip install pre-commit(brew も可) |
| 設定ファイル | シェルスクリプト(1フック1ファイル) | .husky/<フック名>(1フック1ファイル、シェル) |
lefthook.yml(1ファイルに全フック。TOML/JSONも可) |
.pre-commit-config.yaml(1ファイル) |
| フックの実体 | .githooks/ |
.husky/_/(core.hooksPath を変更) |
.git/hooks/ にラッパーを書き込む |
.git/hooks/ にラッパーを書き込む |
| 並列実行 | 自分で書く | なし(lint-staged側の --concurrent はタスク単位) |
parallel: true / piped: true |
なし(フック内の並列はツール次第) |
| ステージ済みファイルだけ対象 | git diff --cached を自分で書く |
lint-staged と組み合わせる | {staged_files} が組み込み |
既定でステージ済みだけ渡す |
| 自動修正をステージ | 自分で git add |
lint-staged が自動 | stage_fixed: true |
書き換えは Failed 扱い(再 git add が必要) |
| 言語・ランタイム依存 | シェルのみ | Node.js 18以上 | 単一バイナリ(Go製)。Node不要 | Python 3.10以上。フック側の言語環境は自動構築 |
| 向いているプロジェクト | 小規模・ツールを増やしたくない | Node.js中心 | 複数言語のモノレポ、速度重視 | Python中心、公開フックを組み合わせたい |
| スキップ | --no-verify |
--no-verify / HUSKY=0 |
--no-verify / LEFTHOOK=0 |
--no-verify / SKIP=<id> |
どれを選ぶかの判断表
| 状況 | おすすめ |
|---|---|
| Node.jsのアプリで、lintとフォーマットをコミット前に走らせたい | husky + lint-staged(情報量が多く、既に package.json 中心で運用しているなら追加コストが最小) |
| フロント(Node)とバックエンド(Go/Ruby/Python)が同じリポジトリ | lefthook(1つの lefthook.yml に root: 付きで並べ、並列実行できる) |
| Pythonプロジェクトで、ruff・mypy・YAMLチェックなど公開フックを組み合わせたい | pre-commit(rev でバージョンを固定でき、CI では pre-commit run --all-files を同じ設定で流せる) |
| ツールを1つも増やしたくない、フックが1〜2個 | core.hooksPath + .githooks/(prepare や Makefile で git config を自動化) |
よくあるトラブルと対処表
以下はすべてこの記事の検証中に実際に再現した出力です。
| 症状・出力 | 原因 | 対処 |
|---|---|---|
hint: The '.git/hooks/pre-commit' hook was ignored because it's not set as executable. |
実行権限がない | chmod +x .git/hooks/pre-commit(huskyは .husky/_/ を755で生成するので通常は不要) |
| フックが何も言わずに動かない | ファイル名の誤り(precommit、pre-commit.sh)/core.hooksPath が別の場所を指している/Git 2.9未満 |
git config core.hooksPath で .husky/_ を確認。ls .husky/_ で実体を確認 |
cloneしたばかりで git config core.hooksPath が空 |
npm install(prepare)がまだ走っていない |
npm install または npm run prepare |
.git can't be found(npm install 時は > husky の直後に表示) |
.git がカレントディレクトリにない(サブディレクトリの package.json、またはGit管理外) |
"prepare": "cd .. && husky frontend/.husky" の形にする。Git管理外なら git init |
husky - add command is DEPRECATED(終了コード1) |
v8以前の手順 | echo "コマンド" > .husky/pre-commit |
husky - DEPRECATED ... They WILL FAIL in v10.0.0 |
フック先頭に v8 の2行(shebang と _/husky.sh の読み込み)が残っている |
その2行を削除 |
/usr/bin/env: 'sh\r': No such file or directory |
フックファイルがCRLF(Windowsで編集) | LF に変換。.gitattributes に .husky/** text eol=lf と .githooks/** text eol=lf を追加 |
npm error notarget No matching version found for undefined@lint-staged. |
.husky/pre-commit がCRLFで、npx lint-staged\r と解釈された |
同上(LFに変換) |
.husky/pre-commit: 1: npx: not found + husky - command not found in PATH=... |
GUIクライアント(エディタ内蔵Git・SourceTree等)や nvm 環境で node が PATH にない |
~/.config/husky/init.sh に export PATH="/opt/node22/bin:$PATH" や nvm の初期化2行を書く(実測で通ることを確認済み) |
sh: 1: husky: not found → npm error code 127(npm ci --omit=dev) |
devDependencies を入れないのに prepare が走る |
HUSKY=0 を渡す、"prepare": "husky || true"、または --ignore-scripts |
error: failed to push some refs to ...(直前にテスト失敗の出力) |
pre-push が非ゼロで終了 |
テストを直す。緊急時は git push --no-verify |
| huskyをやめた後もフックが動かない | core.hooksPath が .husky/_ のまま |
git config --unset core.hooksPath |
~/.config/husky/init.sh は、GUIクライアント対策の要です。nvmなら次の2行を置きます。
# ~/.config/husky/init.sh
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
よくある質問
Q. .husky/_ はコミットすべきですか?
A. しません。husky が生成する .husky/_/.gitignore(中身は *)で自動的に無視されます。コミットするのは .husky/pre-commit など自分で書いたファイルだけで、cloneした人は npm install の prepare で .husky/_ が再生成されます。
Q. npx husky init で作られた pre-commit の npm test は消していいですか?
A. 構いません。npm test は「フックが動くことを確かめる」ためのサンプルで、実運用では npx lint-staged などに書き換えます。テストは pre-push に置くと、コミットのたびに待たされずに済みます。
Q. yarn や pnpm でも手順は同じですか?
A. pnpm は pnpm add -D husky && pnpm exec husky init、bun は bun add --dev husky && bunx husky init で同じです。Yarn 2以降は prepare を自動実行しないため、公式ドキュメントでは postinstall に husky を書き、公開パッケージなら pinst で npm publish 時の実行を抑える方法が案内されています。
Q. フックを一時的に全部止めたいときは?
A. 1回なら HUSKY=0 git commit ...、しばらく止めるなら export HUSKY=0(戻すときは unset HUSKY)です。git commit --no-verify は pre-commit と commit-msg だけを飛ばし、prepare-commit-msg は飛ばせない点が違います。
まとめ
- Git hooksは
.git/hooks/の実行可能ファイル。pre-commit・commit-msg・pre-pushは非ゼロ終了で操作を止められ、post-*は止められない(早見表参照) .git/hooks/はcloneで配られないので、共有にはcore.hooksPathかツールが必要。husky v9はnpm installのprepareでcore.hooksPath=.husky/_を自動設定する- husky v9の手順は
npm i -D husky && npx husky initの2行。husky install/husky addは非推奨で、フックは.husky/<フック名>にコマンドを直接書く - lint-stagedは失敗時に元の状態へ自動で戻し、成功時は整形結果を自動ステージする。
tscは関数形式でファイル名を捨てる commit-msgはnpx --no -- commitlint --edit "$1"、pre-pushはnpm test。止まる時はhusky - <フック名> script failed (code N)が出る- CIでは
HUSKY: 0。npm ci --omit=devはhusky: not foundで失敗するのでHUSKY=0か"prepare": "husky || true"にする - 複数言語のモノレポはlefthook、Pythonはpre-commitが向く。フックが動かない時は実行権限・
core.hooksPath・CRLF・PATHの4点を順に確認する
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Git hooks・husky v9の設定方法|実行結果とlefthook比較」
株式会社RJCでは、Git hooksのようにチームの品質を自動で支える仕組みを、プロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。