Husky + lint-staged完全設定ガイド〜pre-commit hook・型チェック・テスト・コミットメッセージ検証【2026年版】〜

「コミット直前にESLintとPrettierを必ず通したい」「壊れたコードがmainブランチに混入するのを防ぎたい」「コミットメッセージの体裁を統一したい」――そんな現場の悩みを解決するのが、Husky + lint-stagedの組み合わせです。本記事では、Husky 9・lint-staged 15 を基準に、pre-commit hook の構築、型チェック・テスト連動、commitlint による Conventional Commits の強制、monorepo 対応、CI との二重チェック設計まで、コピペで動く設定を 40 を超えるコードブロックで網羅的に解説します。

本記事の前提パッケージバージョンは以下の通りです。Node.js は 20 LTS 以上、パッケージマネージャは pnpm を主軸に扱いますが、npm / yarn / bun でも置換可能です。

パッケージ バージョン 役割
husky ^9.1.7 Git hook 管理
lint-staged ^15.2.10 ステージ済みファイルのみに lint 適用
@commitlint/cli ^19.6.0 コミットメッセージ検証
@commitlint/config-conventional ^19.6.0 Conventional Commits ルール
eslint ^9.17.0 静的解析
prettier ^3.4.2 整形
  1. Husky とは何か:Git hook を「コミットされた設定」として運用する
    1. Husky 9 のインストール手順
    2. 生成される .husky/pre-commit の中身
  2. lint-staged を組み合わせて「ステージ済みファイルだけ」を高速チェック
    1. lint-staged のインストールと package.json 設定
    2. .husky/pre-commit から lint-staged を呼び出す
    3. 設定ファイルを切り出す:lintstagedrc.js
  3. TypeScript 型チェックを pre-commit で走らせる
    1. NGパターン:ファイル単位で tsc を呼ぶ
    2. OKパターン:プロジェクト全体で型チェック
  4. Vitest テストを pre-commit で連動させる
    1. テスト失敗時の挙動を制御する
  5. commitlint で Conventional Commits を強制する
    1. インストール
    2. 設定ファイル commitlint.config.js
    3. .husky/commit-msg を作成
    4. 正しいコミットメッセージの例
    5. NG例(commitlint で弾かれる)
  6. pre-push hook でテストとビルドを最終チェック
  7. semantic-release と連携してバージョン自動化
  8. monorepo(pnpm workspace)対応
    1. ルート package.json
    2. pnpm-workspace.yaml
    3. パッケージごとに違うルールを当てる
  9. Turborepo + Husky で並列実行
  10. GitHub Actions と Husky の二重チェック設計
  11. pre-commit hook の高速化テクニック
    1. テクニック 1:lint-staged の並列実行を制御
    2. テクニック 2:–no-stash で I/O を減らす
    3. テクニック 3:ESLint キャッシュ
  12. Husky をスキップしたい時の安全策
    1. 方法 1:HUSKY 環境変数で一時停止
    2. 方法 2:特定の hook だけ条件分岐でスキップ
  13. shellcheck で hook 自体の品質を担保
  14. Windows / WSL / Git Bash 環境で動かす
    1. 改行コードを LF に固定
    2. Windows のパス区切り対応
  15. CI 環境では hook を skip する
  16. 自動コミットメッセージ生成 hook
  17. PR テンプレートとの連携
  18. アンチパターン集
    1. アンチパターン 1:pre-commit に重いビルドを置く
    2. アンチパターン 2:lint-staged で全ファイルを対象にする
    3. アンチパターン 3:Husky 4 時代のヘッダ行を残す
    4. アンチパターン 4:eslint-config-prettier を入れずに両方走らせる
  19. トラブルシュート
    1. 症状 1:Husky install しても hook が動かない
    2. 症状 2:lint-staged が「No staged files found」で終わる
    3. 症状 3:commitlint が hook で動かない
    4. 症状 4:VSCode の Source Control タブからコミットすると hook が動かない
    5. 症状 5:Husky 8 から 9 にアップグレード後、警告が出る
  20. 実運用テンプレート(Next.js + TypeScript + pnpm)
  21. まとめ:Husky + lint-staged は「品質の最後の砦」

Husky とは何か:Git hook を「コミットされた設定」として運用する

Husky は .git/hooks/ ディレクトリを直接編集する代わりに、リポジトリ管理下の .husky/ ディレクトリに hook スクリプトを置き、git config core.hooksPath で読み替える仕組みです。Husky 9 以降は仕組みが大幅に簡略化され、シェバンや husky.sh の読み込み行が不要になりました。

Husky 9 のインストール手順

まずは新規プロジェクトに Husky を導入します。husky init は package.json への prepare スクリプト追記と .husky/pre-commit の生成を同時に行います。

# pnpm の場合
pnpm add -D husky
pnpm exec husky init

# npm の場合
npm install -D husky
npx husky init

# yarn の場合
yarn add -D husky
yarn dlx husky init

# bun の場合
bun add -D husky
bunx husky init

実行後、package.json に以下のスクリプトが自動で追加されます。preparepnpm install 時に自動実行されるため、チームメンバが clone 後に install するだけで hook が有効化されます。

{
  "name": "my-app",
  "version": "0.1.0",
  "scripts": {
    "prepare": "husky",
    "dev": "next dev",
    "build": "next build",
    "lint": "eslint .",
    "format": "prettier --write ."
  },
  "devDependencies": {
    "husky": "^9.1.7"
  }
}

生成される .husky/pre-commit の中身

Husky 9 が生成するファイルは極めて簡潔で、Husky 4 時代に必要だったシェバン行や . "$(dirname -- "$0")/_/husky.sh"もう書きません。書くと逆に警告が出ます。

# .husky/pre-commit
npm test

このシンプルさが Husky 9 の特徴です。1 行目から実行コマンドを書くだけで動作します。Husky 8 以前から移行する場合は、不要なヘッダ行を削除してください。

lint-staged を組み合わせて「ステージ済みファイルだけ」を高速チェック

プロジェクト全体に eslint . をかけると数万ファイルでは数十秒かかります。lint-staged は git diff --cached で抽出したファイルだけに lint を実行するため、コミットあたりの待ち時間を 1〜2 秒に短縮できます。

lint-staged のインストールと package.json 設定

pnpm add -D lint-staged

package.json に lint-staged キーを追加し、ファイル拡張子ごとに実行コマンドを書きます。

{
  "scripts": {
    "prepare": "husky"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,yml,yaml,css,scss}": [
      "prettier --write"
    ]
  },
  "devDependencies": {
    "husky": "^9.1.7",
    "lint-staged": "^15.2.10"
  }
}

.husky/pre-commit から lint-staged を呼び出す

# .husky/pre-commit
pnpm exec lint-staged

これだけで「ステージ済みファイルに ESLint と Prettier を自動適用 → 失敗したらコミット中止」という動作が実現します。npm 環境なら npx lint-staged に置き換えてください。

設定ファイルを切り出す:lintstagedrc.js

条件分岐やコマンド組み立てが複雑になってきたら、JS ファイルとして外出しできます。CommonJS でも ESM でも書けます(プロジェクトの "type" に合わせる)。

// lint-staged.config.js (CommonJS)
module.exports = {
  '*.{ts,tsx}': (files) => [
    `eslint --fix --max-warnings=0 ${files.join(' ')}`,
    `prettier --write ${files.join(' ')}`,
  ],
  '*.{js,jsx}': ['eslint --fix', 'prettier --write'],
  '*.{json,md,yml}': 'prettier --write',
};
// lint-staged.config.mjs (ESM)
export default {
  '*.{ts,tsx}': ['eslint --fix --max-warnings=0', 'prettier --write'],
  '*.{js,jsx}': ['eslint --fix', 'prettier --write'],
  '*.{json,md,yml}': 'prettier --write',
};

TypeScript 型チェックを pre-commit で走らせる

ESLint は型情報なしでも動く設定が一般的なため、tsc --noEmit による厳密な型チェックは別途必要です。ただし型チェックは「変更されたファイルだけ」を対象にできないため、運用に工夫が要ります。

NGパターン:ファイル単位で tsc を呼ぶ

// ❌ これは動かない(tsconfig.json が無視されパスエイリアスが解決できない)
module.exports = {
  '*.ts': (files) => `tsc --noEmit ${files.join(' ')}`,
};

ファイル指定で tsc を呼ぶと tsconfig.json が読まれず、pathsbaseUrl が無視されます。これは公式ドキュメントでも明記された制約です。

OKパターン:プロジェクト全体で型チェック

// lint-staged.config.js
module.exports = {
  '*.{ts,tsx}': [
    'eslint --fix',
    'prettier --write',
    // ファイル名を渡さず常にプロジェクト全体を検査する
    () => 'tsc --noEmit -p tsconfig.json',
  ],
};

関数を返すことで「lint-staged が拾ったファイル名を捨て」、tsconfig ベースで全体チェックを行えます。大規模プロジェクトで遅い場合は --incremental を有効化しましょう。

// tsconfig.json
{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./node_modules/.cache/tsbuildinfo",
    "strict": true,
    "noEmit": true,
    "skipLibCheck": true
  }
}

Vitest テストを pre-commit で連動させる

「変更ファイルに関連するテストだけ」を回したい場合、Vitest の --related オプションが有効です。

// lint-staged.config.js
module.exports = {
  '*.{ts,tsx}': [
    'eslint --fix',
    'prettier --write',
    (files) => `vitest related --run ${files.join(' ')}`,
  ],
};

vitest related は依存グラフを解析し、変更されたファイルを import しているテストのみを実行します。pre-commit に置く場合は必ず --run(watch しない)を付けてください。

テスト失敗時の挙動を制御する

# .husky/pre-commit
pnpm exec lint-staged
# テストだけは別扱いにし、CI 環境でないときだけ走らせる
if [ "$CI" != "true" ]; then
  pnpm exec vitest --run --changed HEAD
fi

commitlint で Conventional Commits を強制する

「fix: ボタンが押せない」のような統一フォーマットを全員に守らせるには commit-msg hook で commitlint を走らせます。

インストール

pnpm add -D @commitlint/cli @commitlint/config-conventional

設定ファイル commitlint.config.js

// commitlint.config.js
export default {
  extends: ['@commitlint/config-conventional'],
  rules: {
    'type-enum': [
      2,
      'always',
      [
        'feat',     // 新機能
        'fix',      // バグ修正
        'docs',     // ドキュメント
        'style',    // コードスタイル(動作に影響なし)
        'refactor', // リファクタ
        'perf',     // パフォーマンス改善
        'test',     // テスト
        'build',    // ビルドシステム
        'ci',       // CI 設定
        'chore',    // その他
        'revert',   // revert コミット
      ],
    ],
    'subject-case': [0],
    'subject-max-length': [2, 'always', 100],
  },
};

.husky/commit-msg を作成

# .husky/commit-msg
pnpm exec commitlint --edit "$1"

$1 には Git が用意したコミットメッセージファイル .git/COMMIT_EDITMSG のパスが渡されます。--edit オプションでファイルから読み込んで検証します。

正しいコミットメッセージの例

feat(auth): add OAuth2 login flow

Implement Google and GitHub providers using next-auth v5.
Includes session refresh and error fallback page.

BREAKING CHANGE: removed legacy /login endpoint

NG例(commitlint で弾かれる)

update auth         # type が無い → 失敗
Feat: add login     # 大文字 type → config によっては失敗
fix:add login       # コロン後にスペース無し → 失敗

pre-push hook でテストとビルドを最終チェック

pre-commit は軽量に保ち、重い処理は pre-push に集約するのが定石です。

# .husky/pre-push
echo "🚀 pre-push: running full test suite..."
pnpm exec vitest --run --coverage
echo "🏗️  pre-push: type-check across all packages..."
pnpm exec tsc --noEmit
echo "✅ pre-push: ok"

これにより pre-commit は 2〜3 秒で完了し、push 時に 30 秒程度のフルテストを行うバランスが取れます。

semantic-release と連携してバージョン自動化

Conventional Commits を守れば、semantic-release が CHANGELOG 生成・npm publish・GitHub Release 作成までを完全自動化できます。

pnpm add -D semantic-release 
  @semantic-release/changelog 
  @semantic-release/git 
  @semantic-release/github
// .releaserc.json
{
  "branches": ["main"],
  "plugins": [
    "@semantic-release/commit-analyzer",
    "@semantic-release/release-notes-generator",
    ["@semantic-release/changelog", { "changelogFile": "CHANGELOG.md" }],
    "@semantic-release/npm",
    [
      "@semantic-release/git",
      {
        "assets": ["package.json", "CHANGELOG.md"],
        "message": "chore(release): ${nextRelease.version} [skip ci]"
      }
    ],
    "@semantic-release/github"
  ]
}

feat: は minor、fix: は patch、BREAKING CHANGE: は major と自動判定されます。手動で npm version を叩く運用から解放されるのが大きな利点です。

monorepo(pnpm workspace)対応

monorepo では各パッケージ配下のファイルしか変更されないコミットでも、ルートで lint-staged を実行する設計が安定します。

ルート package.json

{
  "name": "my-monorepo",
  "private": true,
  "scripts": {
    "prepare": "husky"
  },
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": ["eslint --fix", "prettier --write"],
    "*.{json,md,yml}": "prettier --write"
  },
  "devDependencies": {
    "husky": "^9.1.7",
    "lint-staged": "^15.2.10",
    "eslint": "^9.17.0",
    "prettier": "^3.4.2"
  }
}

pnpm-workspace.yaml

packages:
  - 'apps/*'
  - 'packages/*'

パッケージごとに違うルールを当てる

// lint-staged.config.js
module.exports = {
  'apps/web/**/*.{ts,tsx}': ['eslint --fix', 'prettier --write'],
  'packages/ui/**/*.{ts,tsx}': [
    'eslint --fix --config packages/ui/eslint.config.mjs',
    'prettier --write',
  ],
  'packages/api/**/*.ts': [
    'eslint --fix',
    () => 'pnpm --filter @myorg/api typecheck',
  ],
};

Turborepo + Husky で並列実行

Turborepo を入れていれば、lint-staged 内でも turbo run を呼べます。キャッシュが効くため、変更がないパッケージは即座にスキップされます。

// lint-staged.config.js
module.exports = {
  '*.{ts,tsx}': [
    () => 'turbo run lint --filter=...[HEAD^1]',
    () => 'turbo run typecheck --filter=...[HEAD^1]',
  ],
};
// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "lint": {
      "outputs": []
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"]
    }
  }
}

GitHub Actions と Husky の二重チェック設計

ローカルで --no-verify を使われたり、Husky を install していないメンバが居ても CI で同じチェックを必ず通すのが正解です。

# .github/workflows/ci.yml
name: CI
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]

jobs:
  lint-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: pnpm/action-setup@v4
        with:
          version: 9
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'
      - run: pnpm install --frozen-lockfile
      - name: Lint
        run: pnpm exec eslint . --max-warnings=0
      - name: Format check
        run: pnpm exec prettier --check .
      - name: Type check
        run: pnpm exec tsc --noEmit
      - name: Test
        run: pnpm exec vitest --run --coverage
      - name: Commitlint(PR タイトル)
        if: github.event_name == 'pull_request'
        run: echo "${{ github.event.pull_request.title }}" | pnpm exec commitlint

pre-commit hook の高速化テクニック

テクニック 1:lint-staged の並列実行を制御

lint-staged は標準で並列実行されますが、メモリを食う ESLint と TypeScript を同時に走らせるとマシンが固まることがあります。--concurrent で並列数を制限できます。

# .husky/pre-commit
pnpm exec lint-staged --concurrent 2

テクニック 2:–no-stash で I/O を減らす

lint-staged はデフォルトで未ステージ変更を stash しますが、ファイルが多いと数秒のオーバーヘッドになります。CI 環境やクリーンな状態でしか動かさない場合は --no-stash で省略できます。

# .husky/pre-commit
pnpm exec lint-staged --no-stash --concurrent 4

ただし未ステージ変更を巻き戻せなくなるため、ローカル開発者には推奨しません。CI でのみ有効にする運用が無難です。

テクニック 3:ESLint キャッシュ

// lint-staged.config.js
module.exports = {
  '*.{ts,tsx,js,jsx}': [
    'eslint --fix --cache --cache-location node_modules/.cache/eslint',
    'prettier --write --cache --cache-location node_modules/.cache/prettier',
  ],
};

ESLint と Prettier は両者ともキャッシュをサポートしています。これだけで 2 回目以降の lint-staged が 3〜5 倍速くなります。

Husky をスキップしたい時の安全策

「WIP コミットなので一旦 hook を通さず保存したい」場面は確かにあります。--no-verify は危険なので、安全に運用する仕組みを用意しましょう。

方法 1:HUSKY 環境変数で一時停止

# 当該シェルだけ Husky 全停止
HUSKY=0 git commit -m "wip: tmp"
# 永続化したい場合
export HUSKY=0

Husky 9 では HUSKY=0 を渡すと .husky/* がすべてスキップされます。--no-verify と違い意図的にスキップした履歴が環境変数に残るため、レビュー時に検知しやすくなります。

方法 2:特定の hook だけ条件分岐でスキップ

# .husky/pre-commit
if [ "$SKIP_LINT" = "1" ]; then
  echo "⚠️  SKIP_LINT=1 detected, skipping lint-staged"
  exit 0
fi
pnpm exec lint-staged

shellcheck で hook 自体の品質を担保

Husky の hook はシェルスクリプトなので、shellcheck で静的解析できます。CI に組み込むと「全員の hook が壊れていないか」を継続検査できます。

# .github/workflows/shellcheck.yml
name: shellcheck
on: [pull_request]
jobs:
  shellcheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ludeeus/action-shellcheck@master
        with:
          scandir: './.husky'

Windows / WSL / Git Bash 環境で動かす

Husky 9 は Windows ネイティブでも動きますが、いくつか落とし穴があります。

改行コードを LF に固定

# .gitattributes
.husky/* text eol=lf
*.sh text eol=lf

CRLF だとシェルが「コマンドが見つからない」エラーを返します。.gitattributes で必ず LF を強制してください。

Windows のパス区切り対応

// lint-staged.config.js
module.exports = {
  '*.{ts,tsx}': (files) => {
    // Windows でもスペース入りパスが壊れないようにシングルクォートで囲む
    const list = files.map((f) => `'${f}'`).join(' ');
    return [`eslint --fix ${list}`, `prettier --write ${list}`];
  },
};

CI 環境では hook を skip する

CI で pnpm install すると prepare スクリプトで Husky が走ろうとし、Git ディレクトリが無い環境でエラーになることがあります。これを回避する標準パターンが以下です。

// package.json
{
  "scripts": {
    "prepare": "node -e "if (process.env.CI !== 'true') require('child_process').execSync('husky', { stdio: 'inherit' })""
  }
}

または公式推奨の Husky 9 用ワンライナー:

{
  "scripts": {
    "prepare": "husky || true"
  }
}

|| true で失敗を吸収します。CI 上では Husky のセットアップは不要なので、エラーを無視して install を続行させるのが目的です。

自動コミットメッセージ生成 hook

prepare-commit-msg hook を使うと、ブランチ名から自動でプレフィックスを付けるなどの自動化が可能です。

# .husky/prepare-commit-msg
COMMIT_MSG_FILE=$1
COMMIT_SOURCE=$2
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)

# feature/123-add-login というブランチから feat(#123): をプレフィックスに
if [ -z "$COMMIT_SOURCE" ] && [[ $BRANCH =~ ^feature/([0-9]+) ]]; then
  ISSUE="${BASH_REMATCH[1]}"
  CURRENT_MSG=$(cat "$COMMIT_MSG_FILE")
  if [[ ! $CURRENT_MSG =~ ^feat ]]; then
    echo "feat(#$ISSUE): $CURRENT_MSG" > "$COMMIT_MSG_FILE"
  fi
fi

PR テンプレートとの連携

commitlint で守られた Conventional Commits を、PR タイトル検査でも適用します。

# .github/workflows/pr-title.yml
name: PR title
on:
  pull_request:
    types: [opened, edited, synchronize, reopened]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: amannn/action-semantic-pull-request@v5
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          types: |
            feat
            fix
            docs
            refactor
            perf
            test
            build
            ci
            chore
            revert
# .github/pull_request_template.md
## 変更の種類
- [ ] feat: 新機能
- [ ] fix: バグ修正
- [ ] refactor: リファクタ
- [ ] docs: ドキュメント
- [ ] chore: その他

## 概要


## 動作確認
- [ ] `pnpm test` がローカルで成功
- [ ] `pnpm exec tsc --noEmit` が成功
- [ ] スクリーンショット添付(UI 変更がある場合)

アンチパターン集

アンチパターン 1:pre-commit に重いビルドを置く

# ❌ NG: コミットのたびに 30 秒待たされ、開発者が --no-verify を多用するようになる
# .husky/pre-commit
pnpm run build
pnpm exec vitest --run --coverage

重いタスクは pre-push か CI に逃がしましょう。pre-commit は2 秒以内を目標に設計します。

アンチパターン 2:lint-staged で全ファイルを対象にする

// ❌ NG: ステージ済みファイルだけを対象にする lint-staged の意味がなくなる
{
  "lint-staged": {
    "*": "eslint ."
  }
}

アンチパターン 3:Husky 4 時代のヘッダ行を残す

# ❌ Husky 9 では不要(警告が出る)
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm exec lint-staged

Husky 9 では純粋なコマンドだけ書けば OK です。

アンチパターン 4:eslint-config-prettier を入れずに両方走らせる

ESLint のフォーマット系ルールと Prettier がぶつかり、保存するたびに整形が往復します。必ず eslint-config-prettier を ESLint 設定の最後で extends してください。

// eslint.config.mjs
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import prettier from 'eslint-config-prettier';

export default [
  js.configs.recommended,
  ...tseslint.configs.recommended,
  prettier, // ← 必ず最後
];

トラブルシュート

症状 1:Husky install しても hook が動かない

# 原因確認:core.hooksPath が .husky を指しているか
git config core.hooksPath
# 期待値: .husky/_
# 違う値が返るなら、再 init で直す
pnpm exec husky init

症状 2:lint-staged が「No staged files found」で終わる

# git add し忘れ、または .gitignore で対象が除外されている可能性
git status
git diff --cached --name-only
# どのファイルがステージ済みか必ず確認

症状 3:commitlint が hook で動かない

# .husky/commit-msg を直接実行して再現する
sh .husky/commit-msg .git/COMMIT_EDITMSG
# pnpm exec が見つからない場合はパスを通す
export PATH="$PATH:/usr/local/bin:$HOME/.local/share/pnpm"

症状 4:VSCode の Source Control タブからコミットすると hook が動かない

VSCode は親プロセスの PATH を引き継ぐため、nvm や Volta で入れた Node が見えないことがあります。VSCode 設定で "terminal.integrated.inheritEnv": true にするか、Node のフルパスを .husky/* に書きます。

# .husky/pre-commit(Node のパスを明示)
export PATH="$HOME/.volta/bin:$HOME/.nvm/versions/node/v20.18.0/bin:$PATH"
pnpm exec lint-staged

症状 5:Husky 8 から 9 にアップグレード後、警告が出る

husky - DEPRECATED
Please remove the following two lines from .husky/pre-commit:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

表示の通り 2 行を削除すれば解決します。1 ファイルずつ確認し、コマンド行だけ残します。

実運用テンプレート(Next.js + TypeScript + pnpm)

最後に、Next.js プロジェクトでそのまま使えるテンプレートを示します。Husky 9・lint-staged 15・commitlint 19 を組み合わせた、現場標準の最小構成です。

// package.json
{
  "name": "next-app",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint .",
    "format": "prettier --write .",
    "typecheck": "tsc --noEmit",
    "test": "vitest --run",
    "prepare": "husky || true"
  },
  "lint-staged": {
    "*.{ts,tsx}": [
      "eslint --fix --cache --max-warnings=0",
      "prettier --write --cache"
    ],
    "*.{js,jsx,mjs,cjs}": [
      "eslint --fix --cache",
      "prettier --write --cache"
    ],
    "*.{json,md,yml,yaml,css}": "prettier --write --cache"
  },
  "devDependencies": {
    "@commitlint/cli": "^19.6.0",
    "@commitlint/config-conventional": "^19.6.0",
    "eslint": "^9.17.0",
    "eslint-config-prettier": "^9.1.0",
    "husky": "^9.1.7",
    "lint-staged": "^15.2.10",
    "prettier": "^3.4.2",
    "typescript": "^5.7.2",
    "vitest": "^2.1.8"
  }
}
# .husky/pre-commit
pnpm exec lint-staged --concurrent 2
# .husky/commit-msg
pnpm exec commitlint --edit "$1"
# .husky/pre-push
echo "🔍 pre-push: typecheck"
pnpm exec tsc --noEmit
echo "🧪 pre-push: tests"
pnpm exec vitest --run
// commitlint.config.js
export default {
  extends: ['@commitlint/config-conventional'],
};

まとめ:Husky + lint-staged は「品質の最後の砦」

本記事では Husky 9・lint-staged 15 を中心に、pre-commit hook の構築から TypeScript 型チェック・Vitest 連動・commitlint による Conventional Commits 強制・monorepo 対応・GitHub Actions との二重チェック・パフォーマンス最適化・トラブルシュートまでを通しで解説しました。要点をもう一度整理します。

  • Husky 9 は husky init 一発で導入完了。シェバン行は書かない。
  • lint-staged はステージ済みファイルだけを対象にし、ESLint と Prettier は --cache を必ず付ける。
  • 型チェックはファイル単位ではなくプロジェクト単位で実行する。--incremental で高速化。
  • commit-msg hook と commitlint で Conventional Commits を強制し、semantic-release で版管理まで自動化。
  • 重い処理は pre-push か CI に逃がし、pre-commit は 2 秒以内を死守する。
  • CI 上では prepare: "husky || true" で失敗を吸収。GitHub Actions で必ず同じチェックを再実行し二重防御。
  • monorepo は Turborepo の --filter=...[HEAD^1] で差分パッケージだけを検査。
  • Windows 環境は .gitattributes で LF 強制。VSCode から動かないときは PATH を明示。

Git hook は地味な仕組みですが、チーム開発において「壊れたコード・統一感のないコミット」を未然に防ぐ最後の砦です。本記事のテンプレートをそのまま自プロジェクトに貼り付け、コミット品質の底上げを今日から始めてください。

関連記事として、ESLint 9 と Prettier 3 の Flat Config 詳細設定は ESLint 9 + Prettier 3完全設定ガイド、Monorepo の並列ビルド最適化は Turborepo + pnpm workspaceでMonorepo構築完全ガイド も併せてご覧ください。Web フロントエンドエンジニアとしてのキャリアを伸ばしたい方は テックアカデミー侍エンジニア のようなマンツーマンメンタリング型スクールも実務 Tips の即時習得に有効です。

コメント

タイトルとURLをコピーしました