Claude Media
Claude Codeでruffとmypyの検証ループを組む — CLAUDE.mdで品質ゲートを固定

Claude Codeでruffとmypyの検証ループを組む — CLAUDE.mdで品質ゲートを固定

ruffとmypyの実行順と完了条件をCLAUDE.mdに書き、permissionsで確認なしに回せるようにする手順です。ruffの安全でない修正を止める設定と、指示が守られないときの調べ方も扱います。

PythonのコードをClaude Codeに書かせると、型の付け忘れや未使用importが残ったまま「完了」と報告されることがあります。これを防ぐには、ruffとmypyを実行する順番と、どこまで通れば完了かをCLAUDE.mdに具体的なコマンドで書いておくのが手堅い方法です。あわせてpermissionsで両コマンドの確認プロンプトを外し、危険な修正だけを止めます。

この記事の構成は次の3層です。

  • CLAUDE.md: 実行順と完了条件を書く(Claudeが読む指示)
  • pyproject.toml: ruffとmypyの挙動を設定に固定する(ツール側の基準)
  • .claude/settings.json: 実行を許可し、危険なオプションを拒否する

実行順は「ruff check --fix → ruff check → mypy」にする

ruffはruff checkが入口で、--fixを付けると直せる違反を書き換えます。ruff checkはファイルやディレクトリを渡さなければカレントディレクトリを対象にします。

ruff check --fix
ruff check
mypy src

この順番にする理由は2つあります。

  1. --fixは未使用importの削除や型注釈の書き換えなど、コードを実際に変える。型検査は書き換えの後に行わないと、直前の修正で変わった結果を見られない
  2. --fixのあとに--fixなしのruff checkをもう一度走らせると、自動で直せなかった違反だけが残った状態を確認できる

ruffの終了コードは、違反が無い場合と、すべて自動修正できた場合に0です。違反が残ると1、設定ミスなどの異常終了では2になります。--exit-non-zero-on-fixを付けると、自動修正で直った場合も1になります。この挙動は、後で書く完了条件に効いてきます。

mypyは、ruffと違い書き換えの機能を持ちません。指摘を出すだけなので、mypyの結果は必ずClaudeが読んでコードを直す流れになります。

CLAUDE.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はextend-selectより規則の集合を明示できます。

ruffは既定で安全な修正だけを適用します。安全な修正は、コードの意味と実行時の挙動を変えないものです。危険な修正は挙動が変わりうるもので、--unsafe-fixesを付けないと適用されません。

ruffの公式は、危険な修正の例を挙げています。list(...)[0]をnext(iter(...))に書き換える修正(RUF015)は大幅に速くなりますが、コレクションが空のときの例外がIndexErrorからStopIterationに変わります。上流のエラー処理が壊れうるため、危険な修正に分類されています。

自動修正の対象は設定でも絞れます。

[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 *)"
    ]
  }
}

同じ理由で、denyルールも「Claudeが普段書く形」しか止めません。--unsafe-fixesのdenyは強い境界ではなく、CLAUDE.mdの「使わない」という指示と重ねて使う二重の歯止めです。

従われないときの調べ方

CLAUDE.mdは、システムプロンプトの一部ではなく、その後のユーザーメッセージとして渡されます。Claudeは従おうとしますが、厳密な遵守の保証はありません。従われない場合は次の順に確認します。

  1. /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アプリを開発する手順にあります。

まとめ

ruffとmypyの検証ループは、実行順をruff check --fix → ruff check → mypyと決め、完了条件を「2と3が連続で通る」と書くところから始まります。基準はpyproject.toml、実行許可と--unsafe-fixesの拒否は.claude/settings.jsonに分けて置くと、CLAUDE.mdは短く保てます。それでも順番を必ず守らせたい段階で、初めてhookの出番になります。

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