Claude Media
Claude CodeでRuboCopの指摘を片付ける — Railsの-aと-A、todoの縮め方

Claude CodeでRuboCopの指摘を片付ける — Railsの-aと-A、todoの縮め方

rubocop -aと-Aの境目、.rubocop_todo.ymlをcop単位で縮める手順、編集直後に-aを走らせて残りをClaudeへ返すPostToolUse hookの置き方をまとめます。

RuboCopは、Rubyのコードスタイルと不具合の兆候を検査するリンターです。既存のRailsアプリに初めて入れると、数千件の指摘が出て、そのまま.rubocop_todo.ymlに押し込まれがちです。Claude Codeに任せれば片付くのかというと、条件があります。-aと-Aの違いを守らせ、1つのcopずつ進め、設定ファイルで指摘を消させないことです。

この記事は、その進め方を3段で組みます。まず自動修正の安全性の線引き、次にtodoファイルを縮める手順、最後に編集直後に-aを走らせて残りの指摘をClaudeへ返すPostToolUse hookです。Railsアプリの開発全体はClaude CodeでRailsアプリを開発する手順が扱うので、ここではRuboCopの指摘だけに絞ります。

-aと-Aの違いは「意味が変わるか」

RuboCopの自動修正には3つのモードがあります。-a(--autocorrect)は安全な修正だけ、-A(--autocorrect-all)は意味が変わりうる修正も含めたすべて、-x(--fix-layout)は空白や改行などレイアウトの修正だけです。

フラグ適用される修正Claudeに任せる場面
-a適用される修正安全な修正のみClaudeに任せる場面hookや日常の修正で常用
-A適用される修正安全でない修正も含む全部Claudeに任せる場面差分を見る前提で、人が指示したとき
-x適用される修正レイアウトのみClaudeに任せる場面整形だけ先に流したいとき

線引きの根拠は、copごとの2つの注釈です。Safeは、cop自体が設計上の誤検出を出しうるかを表します。SafeAutoCorrectは、自動修正がコードの意味を保つかを表します。cop自体が安全でなければ、その自動修正も安全でないものとして扱われます。-aは安全でないcopと修正を飛ばし、-Aはすべてを適用します。

注意したいのは、cop自体は正しい指摘なのに、自動修正だけが危ない型です。RuboCopのドキュメントの例は、マジックコメント# frozen_string_literal: trueの追加です。指摘は妥当でも、str = 'hello'のあとにstr << 'world'を書くコードは、文字列が凍結されて実行時に失敗します。直し方はstr = +'hello'のように、凍結しないリテラルを明示する形です。Railsアプリで古い文字列操作が多いほど、この型は引っかかります。

Claude Codeにとっての含意は単純です。-Aは「差分を読む人がいるとき」だけに限り、テストが通るまで確定させません。AIエージェント向けのページも、指示がない限りエージェントは-aにとどめるよう書いています。

先に差分だけ見る

--diffは、自動修正が何を変えるかをunified diffで表示し、ファイルには何も書きません。単独では安全な修正の適用を前提にし、-Aや-xと組み合わせればそのモードに従います。修正が必要な状態では終了コードが0以外になります。

bundle exec rubocop --diff
bundle exec rubocop --diff -A --format quiet

-Aの効果をまず目で見てから、Claudeに「この差分のうちテストに影響しそうなものを挙げて」と聞く使い方ができます。--diffは公式ドキュメントのmaster版に載っているオプションなので、古いRuboCopでは動かないことがあります。手元のバージョンはbundle exec rubocop --helpで確かめてください。

.rubocop_todo.ymlを1つずつ空にする

大量の指摘があるコードベースには、rubocop --auto-gen-configを使います。指摘が出ているcopを、.rubocop_todo.ymlの設定変更(対象ファイルの除外、またはcop自体の無効化)に落とし込み、.rubocop.ymlにinherit_from: .rubocop_todo.ymlを足します。除外ファイルが一定数(既定は15)を超えると、そのcopごと無効になります。--exclude-limit COUNTでこの数を変え、--no-exclude-limitなら無効化されません。

auto-gen-configのページの手順には、Claudeに任せる前に人間がやる仕事が含まれています。生成された.rubocop_todo.ymlから、組織のスタイルに合うものを切り出して.rubocop.ymlへ移す作業です。各エントリの上のコメントにはEnforcedStyleなどの設定値が出ているので、copを無効にする代わりにスタイルを合わせる選択肢が見えます。この判断は方針の話なので、Claudeに一任しません。

そのうえで、todoのエントリを1つずつ消していきます。1つのcopにつき、次の流れを回します。

手順

todoを1つのcopずつ縮める流れ

  1. 1

    件数の多い順に並べる

    --format offensesで、cop別の件数と「Safe Correctable」の有無を一覧にします。

  2. 2

    cop名を1つ決めて直す

    --onlyでそのcopに絞り、安全な修正は-a、残りはClaudeが手で直します。

  3. 3

    テストを通す

    自動修正で挙動が変わっていないかを、テストスイートで確かめます。

  4. 4

    todoのエントリを消してコミットする

    .rubocop_todo.ymlから該当copの節を削除し、1cop1コミットにします。

件数の一覧は、次のコマンドで出せます。出力に[Safe Correctable]や[Unsafe Correctable]が付くので、どのcopが-aで片付くかを先に見分けられます。

bundle exec rubocop --format offenses

autocorrectとformattersのページにある出力例は、次の形です(説明用の例で、件数は実際のコードベースで変わります)。

36   Layout/LineLength [Safe Correctable]
18   Style/StringLiterals [Safe Correctable]
13   Style/Documentation
1    Style/ClassAndModuleChildren [Unsafe Correctable]

Claudeには、たとえば次のように頼みます。

.rubocop_todo.ymlから Style/StringLiterals を片付けて。
1. bundle exec rubocop --only Style/StringLiterals -a で直す
2. bin/rails test を実行し、落ちたら原因を報告する
3. 通ったら .rubocop_todo.yml の該当節を削除する
-A は使わない。 .rubocop.yml は変更しない

--onlyは、特定のcopや部門だけを検査するオプションです。Claudeに「全体を検査し直して」と頼むと、毎回数千行の出力が文脈に入ります。cop単位に絞ると、出力もレビューの差分も小さく保てます。

一覧が長いときの出力の絞り方

最初の1回は、複数のcopが数千件ずつ出ることがあります。--max-offenses-per-cop=COUNTを付けると、各copの報告数がCOUNT件に制限され、何件を伏せたかがstderrに出ます。同種の指摘を延々と読ませずに、どんな種類の問題があるかだけを把握できます。cop名の意味が分からないときは--explain Department/CopNameで、説明、良い例と悪い例、プロジェクトでの設定、自動修正の有無を表示できます。

再生成は慎重に

rubocop --regenerate-todoは、同じオプションでtodoを作り直します。ただし、これは新しい指摘もtodoに取り込むコマンドです。Claudeに「todoを更新して」と頼むと、作業中に増やした違反まで黙認の対象にできてしまいます。公式の説明どおり、再生成は人間が意図して基準を引き直すときの手段です。

todoを縮め続けたいなら、--report-unused-todo-entriesが使えます。todoのExcludeに書かれたファイルのうち、もうそのcopが指摘しなくなったものを報告し、見つかれば失敗させます。新しい違反は通常どおりに失敗するだけで、基準は動きません。CIに入れると、直したエントリを消し忘れる状態を防げます。

bundle exec rubocop --report-unused-todo-entries

編集直後に-aを走らせるPostToolUse hook

ここからが、hookの出番です。PostToolUse hookは、ツールが成功した直後に動きます。hooksガイドのPrettierの例と同じ型で、matcherにEdit|Writeを指定し、Claudeが編集したRubyファイルにRuboCopを当てます。

違いは、結果の返し方です。整形だけなら実行して終わりですが、RuboCopは自動で直せない指摘が残ります。そこで-aで安全な修正を適用し、残った指摘を標準エラーに出して終了コード2で返します。PostToolUseの終了コード2は、すでに実行されたツールを取り消さず、標準エラーをClaudeに見せます。

.claude/settings.jsonには次のように書きます。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/rubocop.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

スクリプトは次のとおりです。

mkdir -p .claude/hooks
cat > .claude/hooks/rubocop.sh <<'SH'
#!/bin/bash
file_path=$(jq -r '.tool_input.file_path // empty')
[ -z "$file_path" ] && exit 0
 
case "$file_path" in
  *.rb|*.rake|*.gemspec|*/Gemfile) ;;
  *) exit 0 ;;
esac
[ -f "$file_path" ] || exit 0
 
cd "${CLAUDE_PROJECT_DIR:-.}" || exit 0
 
output=$(bundle exec rubocop -a --force-exclusion \
  --format emacs "$file_path" 2>&1)
status=$?
 
case "$status" in
  0) exit 0 ;;
  1) echo "$output" >&2; exit 2 ;;
  *) echo "$output" >&2; exit 1 ;;
esac
SH
chmod +x .claude/hooks/rubocop.sh

要点は4つあります。

  • -aだけを使い、-Aは入れない。hookは編集のたびに動くので、意味が変わる修正を毎回自動で適用してはいけません
  • --force-exclusionを付ける。.rubocop.ymlや.rubocop_todo.ymlで除外されたファイルは、パスを明示して渡しても検査されます。付けないと、todoで除外中のファイルにも指摘が出て、ループの原因になります
  • --format emacsで、1指摘1行の機械的な形式にする。ファイル、行、列、cop名が並ぶので、Claudeが該当箇所を探しやすくなります
  • 終了コードを分ける。RuboCopは、修正済みの指摘では失敗せず、残った指摘で1、設定ミスや内部エラーで2を返します。1のときはClaudeへ返し、2のときは終了コード1にして、ユーザー側のhookエラーとして見せます

終了コード0で終えたhookの標準エラーは、Claudeには届きません。「指摘を見せたい」と「通常は黙っていたい」を両立するために、指摘が残ったときだけ終了コード2にします。

-aはファイルを書き換えるので、Claudeが書いたあとのファイルは、hookの実行後に変わっています。この動きはPrettierの例と同じで、hooksガイドも、成功したhookは会話に何も表示しないと説明しています。直ったかどうかは、ファイルの差分で確かめます。

このhookが拾えないもの

matcherがEdit|Writeなので、Bashコマンドでの書き換えは対象外です。rails generateやsed -iがファイルを作った場合、PostToolUseは動きません。公式ドキュメントにも、Edit|Writeに一致するhookは、Bashコマンドが同じファイルを書き換えたときには動かない、という記述があります。

すべての変更を拾いたいときは、Stop hookでターンの終わりに変更ファイルを検査する方法があります。RuboCopのドキュメントmaster版には--changedオプションがあり、gitのリビジョンとの差分があるファイル(未追跡も含む)だけを検査します。bundle exec rubocop --changedをStop hookに置けば、ターン中に触ったRubyファイルをまとめて確かめられます。Stopは「Claudeが応答を終えるたび」に動くため、終了コード2で返すと会話が続きます。ループが止まらなくならないよう、入力のstop_hook_activeを確認してください。

hookの基本はPostToolUse hookでツール実行後の後処理を自動化するに、言語別のリンターはPostToolUse hookのLint自動修正にあります。同じ型で別のリンターを回す例が、hadolintでDockerfileを検査する手順とgolangci-lintです。

指摘を消す近道をふさぐ

「RuboCopを通して」と頼まれたClaudeには、最短の手があります。# rubocop:disableのコメントを足す、.rubocop.ymlで対象のcopをEnabled: falseにする、todoに除外を足す、の3つです。どれも指摘は消えますが、直したことにはなりません。

RuboCopのAIエージェント向けページは、これに備えた書き方を挙げています。CLAUDE.mdに入れるなら、次のような形です。

## RuboCop
 
- 編集後は `bundle exec rubocop --changed` で自分の変更を確かめる
- 指摘は `bundle exec rubocop -a ファイル名` で直す。頼まれない限り `-A` は使わない
- 意味が分からない指摘は `bundle exec rubocop --explain Department/CopName` で調べる
- `.rubocop.yml` と `.rubocop_todo.yml` を、指摘を消す目的で編集しない。todoの再生成もしない
- 指摘を残すときは、行に `# rubocop:disable Department/CopName -- 理由` を書く

todoを縮める作業の日は、.rubocop_todo.ymlの編集を許す必要があります。「通常は禁止、縮める作業のときだけ許可」という例外が要るので、規約は作業の指示側に書くほうが扱いやすくなります。

規約だけでは強制力がありません。Claude Codeの権限で、設定ファイルの編集を拒否しておくと確実です。パスを指定する規則はEdit(path)の形で書き、WriteやNotebookEditのパス規則は一致しないとされているため、Edit(...)で指定します。

{
  "permissions": {
    "deny": [
      "Edit(.rubocop.yml)"
    ]
  }
}

.rubocop_todo.ymlも同様に拒否したうえで、縮める日だけ一時的に外す運用にすれば、todoの再生成と編集を封じられます。

無視コメントに理由を求める

無視コメントを禁じるより、理由を必須にするほうが実用的です。RuboCopのStyle/DisableCopsWithinSourceCodeDirectiveは、AllowWithReasonを有効にすると、--で理由が付いていないdisable、todo、disable-nextをすべて指摘にします。このcopは自動修正されません。理由を足せるのはClaudeか人間だけだからです。

Style/DisableCopsWithinSourceCodeDirective:
  Enabled: true
  AllowWithReason: true
# 指摘になる
name = "fixture" # rubocop:disable Style/StringLiterals
 
# 通る
name = "fixture" # rubocop:disable Style/StringLiterals -- 記録済みの出力と合わせるため

ただし同じページも、これは設定ファイルの緩和までは防がない、と明記しています。.rubocop.ymlの緩和を防ぐ確実な手段は、上のdeny規則のように、エージェントが書けない状態にすることです。rubocop --display-suppressedを使うと、無視コメントで隠れている指摘も後から一覧にできます。

MCPサーバーで回す選択肢

RuboCop 1.85から、組み込みのMCPサーバーがあります。実験的な機能で、挙動やインターフェースが変わる可能性があると書かれています。mcpgemは別途入れる必要があり、Claude Codeへの登録は次のコマンドです。

claude mcp add rubocop -- bundle exec rubocop --mcp

提供されるツールは、検査のrubocop_inspection、自動修正のrubocop_autocorrection、cop説明のrubocop_explainの3つです。自動修正ツールは必須のsafety引数を取り、trueなら安全な修正のみ、falseなら安全でない修正も含みます。つまり、MCP経由でも-aと-Aの線引きは同じで、Claudeがsafety: falseを選ばないように、指示や権限で縛ることになります。dry_runをtrueにすると、書き込まずに差分だけが返ります。

起動の速さが利点ですが、本稿のhookのように「編集のたびに必ず動く」仕組みにはなりません。呼び出しはClaudeの判断で行われるため、強制したい検査はhookに、探索や仕分けにはMCPと、役割を分けて使います。

まとめ

自動修正は-aまでをClaudeの標準とし、-Aは差分を読む人がいる場面に限ります。todoの縮小は、--format offensesで順番を決め、1cop1コミットで進めます。hookは-aと--force-exclusionで動かし、残った指摘だけを終了コード2で返します。この3つを押さえたうえで、設定ファイルをdenyで守っておくと、「指摘が消えた」と「直った」を同じ意味で扱えます。

Railsのバージョンを上げる作業と並行する場合は、Claude CodeでRailsをアップグレードする手順が、テストのベースラインを先に作る流れを扱っています。マイグレーションの検査はstrong_migrationsの指摘を直す手順にあります。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/hooks-guide
  2. code.claude.com/docs/en/hooks
  3. github.com/rubocop/rubocop
  4. docs.rubocop.org/rubocop/usage/autocorrect.html
  5. docs.rubocop.org/rubocop/usage/auto_gen_config.html
  6. docs.rubocop.org/rubocop/usage/ai_agents.html
この記事を共有:XはてブLinkedIn