Claude Codeでruffとmypyを併用する検証ループ — CLAUDE.mdで品質ゲートを固定
ruffは規約違反、mypyは型の不整合を見る別の道具で、併用が前提です。実行順と完了条件をCLAUDE.mdに書き、permissionsで回す手順と、ruffの安全でない修正を止める設定を示します。
ruffとmypyは役割が別なので、Claude Codeでは両方を併用します。ruffは規約違反や未使用importを見るリンターで、型の整合を見るのはmypyです。
PythonのコードをClaude Codeに書かせると、型の付け忘れや未使用importが残ったまま「完了」と報告されることがあります。これを防ぐには、ruffとmypyを実行する順番と、どこまで通れば完了かをCLAUDE.mdに具体的なコマンドで書いておくのが手堅い方法です。あわせてpermissionsで両コマンドの確認プロンプトを外し、危険な修正だけを止めます。
役割を3つのファイルに分ける
CLAUDE.md
実行順と完了条件を書く。Claudeが読む指示です。
pyproject.toml
ruffとmypyの挙動を設定に固定する。ツール側の基準です。
.claude/settings.json
実行を許可し、危険なオプションを拒否する。
ruffとmypyは何が違うのか
ruffはリンターで、型チェッカーではありません。公式FAQも、型チェッカーが拾う誤りをruffは見逃し、逆にruffが拾う誤りを型チェッカーは通すと説明しています。
| 観点 | ruff | mypy |
|---|---|---|
| 見るもの | ruff未使用import、書き方の規約、バグになりやすい記法 | mypy型注釈とコードの整合 |
| 自動修正 | ruff--fixで書き換える | mypy指摘するだけ |
| 設定の置き場 | ruff[tool.ruff.lint] | mypy[tool.mypy] |
片方では穴が残るので、品質ゲートには両方を入れます。
実行順は「ruff check --fix → ruff check → mypy」にする
ruffはruff checkが入口で、--fixを付けると直せる違反を書き換えます。ruff checkはファイルやディレクトリを渡さなければカレントディレクトリを対象にします。
ruff check --fix
ruff check
mypy srcこの順番にする理由は2つあります。
--fixは未使用importの削除や型注釈の書き換えなど、コードを実際に変える。型検査は書き換えの後に行わないと、直前の修正で変わった結果を見られない--fixのあとに--fixなしのruff checkをもう一度走らせると、自動で直せなかった違反だけが残った状態を確認できる
ruffの終了コードは、違反が無い場合と、すべて自動修正できた場合に0です。違反が残ると1、設定ミスなどの異常終了では2になります。--exit-non-zero-on-fixを付けると、自動修正で直った場合も1になります。この挙動は、後で書く完了条件に効いてきます。
mypyは、ruffと違い書き換えの機能を持ちません。指摘を出すだけなので、mypyの結果は必ずClaudeが読んでコードを直す流れになります。
実際に回すと、直る違反と残る違反が分かれる
ruff 0.16.10とmypy 2.4.0を新しい仮想環境に入れ、次のファイルを検査した出力です。pyproject.tomlは、select = ["E", "F", "B"]にextend-select = ["RUF100", "RUF015"]を足した設定です。
import os
import sys
def first(xs: list[int]) -> int:
return list(xs)[0]
def add(a, b):
return a + b
def risky() -> int:
x: int = "a" # noqa: E501
return x最初にruff check --fixを実行します。
$ ruff check --fix
RUF015 Prefer `next(iter(xs))` over single element slice
--> src/app.py:4:12
|
3 | def first(xs: list[int]) -> int:
4 | return list(xs)[0]
| ^^^^^^^^^^^
help: Replace with `next(iter(xs))`
Found 4 errors (3 fixed, 1 remaining).
No fixes available (1 hidden fix can be enabled with the `--unsafe-fixes` option).4件の違反のうち、未使用のimport osとimport sys、効いていない# noqa: E501の3件は自動で消えました。残ったのはRUF015の1件で、修正は「隠れた修正」として用意されているだけで適用されていません。終了コードは1です。続けてruff checkを実行しても同じ1件が出るので、--fixなしの再実行は「自動で直せなかった違反の一覧」として読めます。
mypyは別の問題を拾います。
$ mypy src
src/app.py:12: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment]
Found 1 error in 1 file (checked 1 source file)型注釈の無いdef add(a, b)は、この実行では報告されません。mypyは既定で、注釈の無い関数の本体を検査しません。--disallow-untyped-defs(設定ならdisallow_untyped_defs = true)を付けると、次の指摘が加わりました。
$ mypy --disallow-untyped-defs src
src/app.py:7: error: Function is missing a type annotation [no-untyped-def]
src/app.py:12: error: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment]
Found 2 errors in 1 file (checked 1 source file)「型の付け忘れを残さない」ことを完了条件にするなら、mypyの既定のままでは足りません。addにintの注釈を付け、next(iter(xs))への書き換えとx: int = 1の修正を入れると、両方とも通りました。
$ ruff check
All checks passed!
$ mypy src
Success: no issues found in 1 source fileCLAUDE.mdに検証コマンドと完了条件を書く
CLAUDE.mdは、毎回の会話で言い直すことになる内容を書き留める場所です。公式は「ビルドコマンド、規約、プロジェクト構成、常にこうする、というルール」を入れる例に挙げています。「Claudeが同じ間違いを2回した」場面も、追記のきっかけです。
書き方では、検証できるほど具体的にするのが要点です。「コードを整形する」ではなく「npm testをコミット前に実行する」のように書くと従われやすい、というのが公式の説明です。Pythonの品質ゲートも同じで、曖昧な「Lintを通す」ではなくコマンドと合格条件を並べます。
次のような形が出発点になります。
## Python品質ゲート
コードを変更したら、完了と報告する前に次の順で実行する。
1. `ruff check --fix` を実行する
2. `ruff check` を実行し、終了コード0(`All checks passed!`)になるまで直す
3. `mypy src` を実行し、エラーが0件になるまで直す
### 完了条件
- 2と3が連続で通った状態を確認してから完了と報告する
- `# noqa` と `# type: ignore` は、理由をコメントに書いた場合だけ追加する
- `--unsafe-fixes` は使わない。使いたい場合はユーザーに確認する
- 3で直した後は、2からやり直す最後の行が、検証ループの核です。mypyの指摘を直すとコードが変わるので、ruffの違反が新しく生まれることがあります。「2と3が連続で通る」と書いておけば、片方だけ通った状態で終わることを防げます。
抑制コメントの扱いも決めておく
エラーを消すだけなら、# noqaや# type: ignoreを付ける方法があります。ルールに書かなければ、この選択は制限されません。ruffには、効いていない# noqaを検出するunused-noqa(RUF100)という規則があります。ただし規則を選ぶ設定に加えない限り検査されないので、次の節のextend-selectで有効にします。上の例のように「理由を書いたときだけ」と条件を付けたうえで、RUF100で不要になった抑制を洗い出せる形にしておきます。
200行を超えたら分ける
CLAUDE.mdは1ファイル200行以内が目安です。長くなると多くのコンテキストを消費し、従われる度合いも下がるためです。Pythonの品質ゲートだけで数十行になるなら、.claude/rules/に切り出し、pathsでPythonファイルに限定できます。
---
paths:
- "**/*.py"
---
# Python品質ゲート
コードを変更したら、完了前に `ruff check --fix` → `ruff check` → `mypy src` の順で実行する。paths付きのルールは、該当ファイルを開いたときに読み込まれます。Pythonを触らない会話では、この指示がコンテキストを占めません。
pyproject.tomlでツール側の基準を固定する
CLAUDE.mdに「厳しめにチェックする」と書くより、基準は設定ファイルに置くほうが確実です。ruffの規則はselect・extend-select・ignoreの3つで選びます。規則コードは英字1〜3文字の接頭辞と3桁の数字(F401など)で、接頭辞だけを書けばその系統をまとめて指定できます。
[tool.ruff.lint]
select = ["E", "F", "B"]
extend-select = ["RUF100"]
ignore = ["F401"]この例は、pycodestyleのE、PyflakesのF、flake8-bugbearのBを有効にし、F401(未使用import)だけを外します。extend-selectで足したRUF100は、効いていない# noqaの検出です。コマンドラインならruff check --extend-select RUF100でも同じ検査ができ、--fixを付けると不要な抑制コメントを削除します。ALLは全規則を有効にしますが、ruffをアップグレードするたびに新しい規則が入るので、小さな集合から始めて系統を足していく進め方が公式のガイドラインです。selectなら、設定ファイルを見るだけで有効な規則の集合が分かります。
ruffの自動修正は2種類ある
安全な修正
コードの意味と実行時の挙動を変えない。--fixだけで書き換わります。
危険な修正
挙動が変わりうる。付けない限り「隠れた修正」として残ります。
危険な修正の代表が、先ほど残ったRUF015です。list(...)[0]をnext(iter(...))に書き換えると、先頭の1要素だけを遅延評価でき、大きなコレクションでの無駄な生成を避けられます。ただしコレクションが空のときの例外がIndexErrorからStopIterationに変わり、上流のエラー処理が壊れうるため、危険な修正に分類されています。もう一つの理由は副作用です。元の書き方は反復の全要素を先に評価しますが、next(iter(...))は先頭しか評価しないので、反復中に起きる副作用が遅れます。ruff check --unsafe-fixes --diffなら、ファイルを書き換えずに差分だけ確認できます。
自動修正の対象は設定でも絞れます。
[tool.ruff.lint]
fixable = ["ALL"]
unfixable = ["F401"]この例は、F401(未使用import)以外のすべての規則で自動修正を有効にします。逆にfixable = ["F401"]だけを書くと、F401以外の自動修正が止まります。
修正の安全性は規則ごとに変更できます。extend-safe-fixesで危険な修正を安全側へ、extend-unsafe-fixesで安全な修正を危険側へ動かします。
[tool.ruff.lint]
extend-unsafe-fixes = ["UP034"]mypyは[tool.mypy]セクションに書きます。--strictは複数の厳しいフラグをまとめて有効にする指定で、公式は「有効になるフラグの一覧は将来変わりうる」と注記しています。
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
[[tool.mypy.overrides]]
module = ["somelibrary"]
ignore_missing_imports = true[[tool.mypy.overrides]]はモジュール単位の設定です。型情報を持たないライブラリがあるとき、その1モジュールだけignore_missing_importsにして、他は厳しいままにできます。Claudeに「型が無いライブラリのエラーを無視して」と毎回頼むより、この設定に寄せたほうが完了条件がぶれません。
permissionsで確認プロンプトを外し、危険なオプションを拒否する
検証ループは、Claudeが同じコマンドを何度も呼ぶ作業です。確認プロンプトが毎回出ると、ループが途中で止まります。.claude/settings.jsonで許可します。
{
"permissions": {
"allow": [
"Bash(ruff check *)",
"Bash(mypy *)"
],
"deny": [
"Bash(ruff * --unsafe-fixes*)"
]
}
}Bashルールの*は、空白を含む任意の文字列にマッチします。末尾のBash(ruff check *)は、引数なしのruff checkにもマッチします。スペースは規則の一部で、Bash(ruff*)と書くとruffleのような別のコマンド名にもマッチします。
拒否ルールは、許可ルールと同じ書式でオプション単位に書けます。denyはどのスコープの設定でも許可より先に評価されるため、ユーザー設定側で許可があっても、プロジェクト側のdenyが勝ちます。
複合コマンドと実行ラッパーの落とし穴
ruff check --fix && mypy srcのように1行で書くと、Claude Codeは&&で分割し、各コマンドが独立してルールにマッチする必要があります。両方のルールを許可していれば通ります。
uv run ruff checkやpoetry run mypyのように環境ランナー経由で呼ぶプロジェクトでは、注意が必要です。Claude Codeが自動で取り除くラッパーはtimeout・time・nice・nohup・stdbufなどの固定リストで、環境ランナーは含まれません。Bash(ruff check *)はuv run ruff checkにマッチしないので、ランナーごと書いたルールが必要です。
{
"permissions": {
"allow": [
"Bash(uv run ruff check *)",
"Bash(uv run mypy *)"
]
}
}Bash(uv run *)のように広く許可する方法もありますが、*はrunの後ろの任意のコマンドにマッチします。公式は環境ランナーについて、ランナーと内側のコマンドの両方を書く具体的なルールを案内しています。
同じ理由で、denyルールも「Claudeが普段書く形」しか止めません。--unsafe-fixesのdenyは強い境界ではなく、CLAUDE.mdの「使わない」という指示と重ねて使う二重の歯止めです。
従われないときの調べ方
CLAUDE.mdは、システムプロンプトの一部ではなく、その後のユーザーメッセージとして渡されます。Claudeは従おうとしますが、厳密な遵守の保証はありません。従われない場合は次の順に確認します。
品質ゲートが守られないときの調べ方
- 1
Claudeに見えているか
/contextを実行し、Memory filesの一覧に該当のCLAUDE.mdが出ているか見ます。出ていなければClaudeには見えていません。 - 2
指示が曖昧でないか
「型を確認する」より
mypy srcのようなコマンドが効きます。 - 3
矛盾する指示がないか
別のCLAUDE.mdや
.claude/rules/に食い違う指示があると、どちらかが恣意的に選ばれます。 - 4
一括で洗い出す
/doctor prompt-auditが、存在しないコマンドへの参照や矛盾を報告します(Claude Code v2.1.283以降)。
/compactの後も、プロジェクトルートのCLAUDE.mdはディスクから再読み込みされて残ります。会話の中だけで伝えた指示は残りません。検証手順を口頭で伝えたなら、CLAUDE.mdに移します。
指示ではなく強制したいなら、hookに移す
CLAUDE.mdはあくまで文脈であり、強制ではありません。「毎回のファイル編集後に必ず実行する」「完了前に必ず走らせる」といった保証が必要なら、公式もhookにするよう案内しています。hookはシェルコマンドとして動き、Claudeの判断に左右されません。
PostToolUse hookでruffを自動実行する書き方と言語別の早見表は、PostToolUse hookのLint自動修正 — 言語別コマンド12種の早見表にあります。Agent Teamsの完了時に品質ゲートを強制する仕組みなら、Agent Teams Hooksで強制する品質ゲートの仕組みが扱っています。
CLAUDE.mdとpermissionsの組み合わせは、hookより導入が軽い代わりに、順番を守る保証がありません。役割は分けられます。
| 目的 | 置き場所 | 強制力 |
|---|---|---|
| 実行順・完了条件を伝える | 置き場所CLAUDE.md / rules | 強制力指示(従わないことがある) |
| 承認なしで回す | 置き場所permissions allow | 強制力確認プロンプトの省略 |
| 危険なオプションを止める | 置き場所permissions deny | 強制力通常の書き方を拒否 |
| 編集のたびに必ず実行 | 置き場所hook | 強制力固定タイミングで実行 |
同じ「CLAUDE.mdで規約化して検証する」型の記事として、別言語の例はClaude CodeでGoアプリを開発する手順にあります。検証コマンドをmake checkに束ねる入口の設計はClaude CodeでMakefileの検証ループを作るで扱っています。mypyの代わりにpyrightのstrictを使う場合の段階導入はClaude Codeでpyright strictを段階導入する手順にあります。
まとめ
mypyは既定では注釈の無い関数を報告しないため、「型の付け忘れを残さない」を完了条件にするなら--disallow-untyped-defsを含めるかどうかが分かれ目です。既存コードの注釈がまだ少ないプロジェクトでは指摘が一気に増えるので、型付けの進み具合に合わせて、完了条件のmypy行にそのフラグを入れる時期を決めます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。