Claude CodeでESLintのflat config移行を進める — migrate-configの後始末
ESLint 9のflat configへの移行は、@eslint/migrate-configで終わりません。残る書き換えをlint実行の反復でClaude Codeに詰めさせる手順と、症状別の直し方をまとめます。
ESLint 9のflat configへの移行は、@eslint/migrate-configを1回走らせても終わりません。ESLint側が「生成物はそのままでは動くと保証できない出発点」と書いているためです。残る作業は、pluginsやextendsの書き換え、envの置き換え、除外パターンの移し替えなど、どれも「lintを実行してエラーを読み、1種類ずつ直す」反復に向いています。
この反復をClaude Codeに任せるときの要点は3つです。移行前の結果を保存しておくこと、検証コマンドをCLAUDE.mdに固定すること、エラーの種類ごとに直し方の辞書を持たせること。本記事ではこの順に、コマンドと設定の断片つきで進めます。
着手前に固定しておく前提
ESLint 9.0.0は、Node.jsのv18.18.0以上、v20.9.0以上、v21以上だけを対応対象にしています。v19と18.18未満は対象外です。エディタが内部で使うNode.jsのバージョンも見ておく必要があります。上げられない場合の逃げ道として、ESLint v8.56.0にとどまる案が示されています。
もう一つ、見落としやすい点があります。ESLint 9.0.0からeslint.config.jsが既定の設定形式になり、旧形式のeslintrcは自動探索されません。環境変数ESLINT_USE_FLAT_CONFIGをfalseにすれば旧形式を使い続けられます。この環境変数は、移行前後を見比べる場面で役に立ちます。
逆方向の手もあります。ESLint v8でも、プロジェクト直下にeslint.config.jsを置くかESLINT_USE_FLAT_CONFIG=trueを設定すれば、flat configを使えます。
移行の進め方(2段階にする場合)
- 1
v8のまま設定だけ移す
eslint.config.jsを置いて、lintの結果が移行前と揃うところまで詰めます。 - 2
v9へ上げる
Node.jsの要件を満たしたうえで
eslint本体を上げます。設定の問題と本体更新の問題を切り分けられます。
移行前の結果を保存する
反復の「正解」になるのは、移行前のlint結果です。最初に保存します。v8のまま作業する場合は、そのまま実行して構いません。
npx eslint . -f json -o before.json
jq -r '.[].messages[].ruleId' before.json \
| sort | uniq -c | sort -rn > before-rules.txt-f jsonは出力形式、-oは出力先ファイルの指定です。before-rules.txtはルールごとの指摘件数で、移行後に同じ集計を取って差分を見ます。jqが無い環境では、件数の集計だけClaudeにスクリプトで書かせれば足ります。
すでにv9へ上げてしまった場合は、ESLINT_USE_FLAT_CONFIG=falseを前置して同じコマンドを実行します。
なお、設定ファイルが.eslintrc.jsのときは別の注意が要ります。移行ツールは.eslintrc.jsをあまり得意としておらず、結果は「評価後の設定値をそのまま書き出したもの」になります。関数や条件分岐は残らないため、環境ごとに設定を切り替えていた場合は、その分岐を人かClaudeが書き戻す必要があります。
CLAUDE.mdに検証コマンドと禁止事項を書く
ClaudeにはこのリポジトリのlintコマンドとNGな近道を先に伝えます。Claude Codeのベストプラクティスでも、リンターは「Claudeが会話の中で読める信号」の例に挙がっています。検証手段を渡すかどうかで、目を離せるかどうかが変わります。
## ESLint flat config 移行(作業中のみ)
- 設定は eslint.config.js に集約する。.eslintrc.* と .eslintignore は、
移行完了を伝えるまで削除しない
- 検証は `npx eslint . --max-warnings 0`。出力の先頭20行を読んでから直す
- 1回の修正で扱う原因は1種類。直したら必ず再実行し、件数の増減を報告する
- ルールを off にして通さない。off にしたいときは理由を添えて確認する
- 移行前の結果は before.json と before-rules.txt に保存済み。上書きしない「ルールをoffにして通さない」の1行が肝です。lintが赤いままだと、最短で緑にする手はルールを無効化することだからです。この一文がないと、通ったように見えて検査が空洞化した設定ができあがります。
実行権限も先に開けておくと、反復が止まりません。permissions.allowに検証コマンドを足します。
{
"permissions": {
"allow": [
"Bash(npx eslint *)",
"Bash(npx @eslint/migrate-config *)",
"Bash(jq *)"
]
}
}Bash(git *)のようなパターンで許可を絞る書き方は、hooksのリファレンスにある権限ルール構文と同じです。npm run lintを許可リストに入れる例も、ベストプラクティスに載っています。インストールを伴うコマンド(npm installなど)は、この段階では許可に入れず、都度確認を挟むほうが安全です。
migrate-configを実行して出力を読む
移行ツールは、旧設定ファイルを渡して実行します。
npx @eslint/migrate-config .eslintrc.jsonyarnならyarn dlx、pnpmならpnpm dlx、bunならbunxで同じものを実行できます。生成直後に、いきなりlintを回さないでください。まずClaudeに、生成されたeslint.config.jsと旧設定を並べて読ませます。
.eslintrc.json と生成された eslint.config.js を読んで、
旧設定の項目ごとに、新設定のどこへ移ったかの対応表を作ってください。
対応先が見つからない項目は「未移行」として挙げてください。まだ編集はしないでください。編集を禁じて対応表だけ作らせるのが狙いです。「未移行」に挙がった項目は、後で原因不明のエラーに化けやすいものです。先に洗い出しておけば、反復の回数が減ります。
残作業をlint実行の反復で詰める
ここからは、次の形のループです。
1周の流れ
- 1
実行する
npx eslint . --max-warnings 0を実行し、終了コードと先頭のエラーを読みます。 - 2
原因を1種類に絞る
件数の多いエラーより、同じ原因から出ているエラーをまとめて扱います。
- 3
直して再実行する
件数の増減を報告させ、増えていたら直前の修正を疑います。
最初に見るのは終了コードです。ESLintは、指摘がなければ0、エラーがあるか--max-warningsの上限を超えれば1、設定の問題や内部エラーなら2で終わります。2が返ったら設定ファイルそのものが壊れているので、ルールの指摘を直す前に設定を直します。1が返ったら、設定は読めていて、指摘の中身を見る段階です。
症状別の直し方
エラーの文面ごとに、原因と直し方を引ける表にしておきます。Claudeには「該当する行があればこの直し方を第一候補にする」と伝えます。
| 症状 | 原因 | 直し方 |
|---|---|---|
context.getScope is not a function | 原因プラグインがESLint v9のルールAPIに未対応 | 直し方プラグインを更新する。更新が無ければ互換ユーティリティで手当てする |
/* eslint-env */コメントがエラーになる | 原因flat configではこのコメントが認識されず、エラーとして報告される | 直し方コメントを消し、設定側にglobalsを書く。/* global describe, it */への置き換えも可 |
no-undefが大量に出る | 原因envが消え、実行環境ごとのグローバル変数が未定義になる | 直し方globalsパッケージを入れ、languageOptions.globalsに展開する |
| 除外したはずのファイルがlintされる | 原因.eslintignoreは読み込まれない。temp.jsは設定ファイルと同じ階層の1ファイルしか指さない | 直し方ignoresだけを持つオブジェクトに移し、**/temp.jsの形に直す |
.dotfile.jsなどが新たに対象になる | 原因flat configではドットファイルが既定で除外されない | 直し方除外したければ**/.*をignoresに足す |
eslint:recommendedが解決できない | 原因文字列でのextendsが無くなった | 直し方@eslint/jsを入れ、js.configs.recommendedを配列に並べる |
| shareable configが読み込めない | 原因そのconfigがflat config未対応 | 直し方@eslint/eslintrcのFlatCompatでcompat.extends(...)に通す |
TypeScriptなど.tsが対象にならない | 原因filesを持たない設定は**/*.{js,mjs,cjs}にしか効かない | 直し方該当設定のfilesに拡張子を足す |
globalsパッケージは、ESLintが自動で入れてくれません。no-undefの対処では、npm install --save-dev globalsが先に必要になります。この手の追加インストールは、許可を開けていないので、Claudeが確認を求めてきます。そこで止まるのは正常です。
迷いやすい書き換えの実体
extendsの置き換えは、ここだけ例を出しておきます。ESLint公式のガイドにある形に沿った書き方は、次のとおりです。
// eslint.config.js
import { defineConfig } from "eslint/config";
import js from "@eslint/js";
import globals from "globals";
export default defineConfig([
js.configs.recommended,
{
files: ["**/*.js"],
languageOptions: {
globals: { ...globals.browser, ...globals.node },
},
rules: { semi: ["warn", "always"] },
},
{ ignores: ["**/temp.js", "config/*"] },
]);ignoresは、ほかのプロパティを持たない独立したオブジェクトに置きます。ガイドのコード例にも「このオブジェクトに他のプロパティを置かない」という注記があります。キーを同居させてしまうのが、除外が効かないときの第一の疑いどころです。
pluginsも同じで、文字列の配列からオブジェクトのマップに変わります。plugins: ["jsdoc"]は、import jsdoc from "eslint-plugin-jsdoc"で読み込んだうえでplugins: { jsdoc }と書きます。ルール名のjsdoc/require-descriptionは、そのまま使えます。
移行前と後を同じ尺度で見比べる
lintが緑になっても、「検査の中身が移行前と同じか」は別問題です。ここで、最初に保存した結果を使います。
npx eslint . -f json -o after.json
jq -r '.[].messages[].ruleId' after.json \
| sort | uniq -c | sort -rn > after-rules.txt
diff before-rules.txt after-rules.txt差分がなければ、少なくとも指摘の出方は揃っています。差分が出たら、増えたルールと減ったルールをClaudeに挙げさせます。減ったルールは「検査が落ちた」疑いです。増えたルールは、ドットファイルや拡張子の扱いが変わって、対象ファイルが増えた可能性を最初に疑います。
指摘がもともと0件のリポジトリでは、この比較が空振りします。その場合は代表的なファイルに絞って、適用される設定そのものを比べます。ESLintの--print-configは、指定したファイルに適用される設定を出力するオプションで、実行中はlintを行いません。
ESLINT_USE_FLAT_CONFIG=false npx eslint --print-config src/index.js > before-config.json
npx eslint --print-config src/index.js > after-config.json2つのJSONは書式や項目の並びが同じとは限らないので、diffの行数ではなく、rulesの中身をClaudeに読み比べさせます。見るのは、ルールが落ちていないか、重大度が変わっていないかの2点です。環境変数を前置した1行目は、ESLint 9の環境で旧形式を読ませるための指定です。v8のままなら外します。
最後の後片付けと、つまずきやすい点
緑になって比較も済んだら、最後に次を消します。
.eslintrc.*と.eslintignorepackage.jsonのeslintConfigキー(flat configではここに設定を置けません)- スクリプトに残った
--env、--ignore-path、--no-eslintrc、--resolve-plugins-relative-to、--rulesdir
最後の項目が見落とされがちです。これらのフラグはflat configでは使えなくなりました。--no-eslintrcは--no-config-lookupに置き換わりました。--ignore-path .gitignoreのように.gitignoreの内容を除外に使っていた場合は、設定側でincludeIgnoreFile()を使います。ガイドには、@eslint/compatの同名関数が非推奨になり、@eslint/config-helpers(eslint/configから再エクスポート)版に移ったという更新注記があります。どちらを使うかは、インストール済みのバージョンで選びます。
rootオプションは廃止されました。flat configはroot: trueが設定されているものとして動きます。filesに単一の文字列を書くこともできなくなり、配列にします。
エディタ側も確認します。VS CodeのESLint拡張は、v3.0.10以降でESLint 9に対応しています。それより古い拡張では、.vscode/settings.jsonに"eslint.experimental.useFlatConfig": trueを足します。CLIが緑でエディタだけ赤い場合は、先にこの拡張のバージョンを見てください。
最後に、移行後のLintをhookで自動修正する構成にしたいときは、PostToolUse hookのLint自動修正が扱っています。テストランナー側の規約は、Claude CodeでVitestを回すとpytestのCLAUDE.md規約に同じ型で書かれています。モノレポで複数パッケージを抱える場合は、Turborepoのモノレポ設定も併せて読むと、タスクの切り方を決めやすくなります。
まとめ
migrate-configは、設定を新形式に書き写す出発点にとどまります。残りは、移行前の結果を保存し、検証コマンドと「ルールをoffにしない」をCLAUDE.mdに固定し、症状別の辞書で1種類ずつ直す反復です。終了コードの2は設定の破損、1は指摘の中身という切り分けを先にClaudeへ渡しておくと、反復の無駄が減ります。
旧設定が.eslintrc.jsで条件分岐を含むときは、生成物を信用しすぎないほうが無難です。評価済みの値に潰れているため、書き戻した分岐ごとに、前後の結果を比べる確認が欠かせません。