Claude Media
Claude Codeでstrong_migrationsの指摘を直す — Railsの危険なマイグレーション対策

Claude Codeでstrong_migrationsの指摘を直す — Railsの危険なマイグレーション対策

Railsのマイグレーションをstrong_migrationsで検査し、指摘文をClaude Codeに渡して安全な形へ書き直させる流れ。Stop hookで検査を完了条件にし、safety_assuredの自己付与も止めます。

Claude Codeでstrong_migrationsの指摘を直す — Railsの危険なマイグレーション対策

RailsのマイグレーションをClaude Codeに書かせると、rename_columnやadd_indexのように、テーブルが大きい本番では止まりかねない操作もそのまま出てきます。strong_migrationsは、そうした操作を開発環境のdb:migrateで止めるgemです。止まったときに出る指摘文には、安全な書き方まで添えられています。

この指摘文こそ、Claude Codeが直すときの最良の入力になります。人が読んで直す代わりに、メッセージを渡して書き直させ、通るまで繰り返させる。その流れと、任せきりにしないための設定を順に示します。

strong_migrationsは何を止めるか

strong_migrationsが危険と判定する基準は2つです。ロックを取得した後に数秒を超えて読み書きを塞ぐ操作と、アプリケーションエラーを起こす可能性が高い操作。READMEの検査一覧は次のとおりです。

区分検査される操作
共通検査される操作カラム削除 / カラム型変更 / カラム名変更 / テーブル名変更 / force付きのテーブル作成 / 自動採番カラムの追加 / 保存型生成カラムの追加 / 外部キー追加 / チェック制約追加 / SQL直接実行 / データのバックフィル
Postgres検査される操作インデックスの非同時作成 / 参照の追加 / ユニーク制約追加 / 排他制約追加 / json型カラム追加 / volatileな既定値つきカラム追加 / 既存カラムへのNOT NULL設定 / enum値の名前変更 / スキーマ名変更
MySQL・MariaDB検査される操作COPYアルゴリズム / shared・exclusiveロック / 式の既定値つきカラム追加

加えて、ベストプラクティスとして「非ユニークインデックスは3カラムまで」も検査します。ただし表の「データのバックフィル」には但し書きがあります。READMEには「Strong Migrationsは危険なバックフィルを検出しない」とあります。この点は後の節で扱います。

導入はGemfileにgem "strong_migrations"を足し、次の2つを実行するだけです。

bundle install
rails generate strong_migrations:install

インストール後、strong_migrationsはマイグレーション用に長いstatement timeoutを設定します。そのため、アプリ側には短いstatement timeoutを置けます。

指摘文を渡して直させる基本の流れ

危険な操作を含むマイグレーションを実行すると、strong_migrationsは=== Dangerous operation detected #strong_migrations ===で始まるエラーを出します。カラム削除の場合、READMEが示す内容は次の通りです。

=== Dangerous operation detected #strong_migrations ===
 
Active Record caches attributes, which causes problems
when removing columns. Be sure to ignore the column:
 
class User < ApplicationRecord
  self.ignored_columns += ["name"]
end
 
Deploy the code, then wrap this step in a safety_assured { ... } block.

エラー本文に「次に何をするか」が書いてあるので、Claude Codeに読ませる価値があります。依頼の書き方は、db:migrateの出力を全文貼り、直す範囲と禁止事項を添えるだけです。

db/migrate/ の最新のマイグレーションが bin/rails db:migrate で
strong_migrations に止められた。出力は以下。
 
(エラー全文を貼る)
 
指摘文が示す安全な手順に沿って、マイグレーションを書き直して。
- safety_assured は付けない
- 1回のデプロイで終わらない手順なら、今回やる分だけ実装し、
  残りの手順を一覧にして返す
- 直したら bin/rails db:migrate を再実行して結果を見せる

ここで大事なのは2つ目の条件です。カラム削除でも名前変更でも、安全な手順は「コードをデプロイしてから、次のマイグレーションを流す」という複数回のデプロイにまたがります。1つのPRに全部を詰めさせると、結局どこかで危険な操作が混ざります。

公式のベストプラクティスにも、Claudeに「合否が返ってくる確認手段」を持たせると、変更・実行・出力の読み取り・修正を自分で繰り返せるとあります。strong_migrationsのdb:migrateはまさにその確認手段です。

検査を完了条件にするStop hook

プロンプトに「再実行して」と書くだけでは、実行を省くことがあります。ベストプラクティスは、Stopフックを使うと、スクリプトが終了コード2で終わる限りClaude Code側がターンの終了を許さないと説明しています。同じ仕組みをdb:migrateに当てます。

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "bin/rails db:migrate 1>&2 || exit 2" }
        ]
      }
    ]
  }
}

書き込み先は.claude/settings.jsonです。strong_migrationsが止めればdb:migrateは失敗するので終了コード2を返し、標準エラーに流れた指摘文がそのままClaudeに戻ります。

このフックはドキュメントのtscの例をdb:migrateに置き換えた設計で、副作用があります。通ったマイグレーションは開発DBに適用され、db/schema.rbも更新されます。気になる場合は、開発用とは別のDBを向く環境で動かすか、フックの代わりにCLAUDE.mdで実行を指示する運用にします。8回連続でブロックしても進まなければ、Claude Code側が次のブロックを無効にしてターンを終えます。上限は環境変数CLAUDE_CODE_STOP_HOOK_BLOCK_CAPで変えられます。

safety_assuredをClaudeに書かせない

指摘文の末尾にはsafety_assured { ... }で包めば通る旨が書かれています。Claudeにとって最短の「通し方」はこれです。ブロックを外して、検査自体を黙らせてしまうからです。

ただしこのブロックは、正当に使う場面が実在します。カラム削除の手順はこうです。

  1. Active Recordにカラムを無視させる(self.ignored_columns += ["some_column"])
  2. コードをデプロイする
  3. safety_assuredで包んだ削除マイグレーションを書く
  4. デプロイして実行する
  5. 手順1で足した行を消す

3番目の段階ではsafety_assuredは必須です。だから禁止するのではなく、「付けるかどうかを人が決める」状態にします。PostToolUseフックで、マイグレーションファイルにsafety_assuredが入ったら差し戻します。

#!/bin/bash
# .claude/hooks/no-safety-assured.sh
FILE_PATH=$(jq -r '.tool_input.file_path // empty')
 
case "$FILE_PATH" in
  */db/migrate/*.rb) ;;
  *) exit 0 ;;
esac
 
if grep -q 'safety_assured' "$FILE_PATH"; then
  echo "safety_assured を検出: 付けてよいかは人が判断する。" >&2
  echo "指摘文の手順のうち、今回の段階で必要か報告して止まること。" >&2
  exit 2
fi

設定はPostToolUseにEdit|Writeのマッチャーで登録します。PostToolUseが終了コード2を返すと、ツールの実行は済んでいても、標準エラーがClaudeに表示されます。

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": ".claude/hooks/no-safety-assured.sh" }
        ]
      }
    ]
  }
}

こうすると、削除の第3段階では、Claudeがいったん止まって人に確認を求めます。人が承認したら、そのファイルだけ手でsafety_assuredを足すか、指示して許可します。ベストプラクティスの原則どおり、「例外のないルール」はCLAUDE.mdに書くよりフックに置いたほうが確実です。

CLAUDE.mdに書いておくこと

フックは止める役、CLAUDE.mdは方針を伝える役です。短くて足ります。

# マイグレーション
- 変更は db/migrate/ に新しいファイルとして足す。作成済みのファイルは編集しない
- 実行は bin/rails db:migrate。strong_migrations の指摘が出たら全文を読み、
  その手順に従って分割する
- safety_assured は自分で付けない。必要なら理由を説明して止まる
- Postgres のインデックス追加は algorithm: :concurrently と
  disable_ddl_transaction! の組で書く
- 大きなテーブルへのデータ更新は、マイグレーションに入れず別ファイルにする

最後の2行は、strong_migrationsが指摘する内容ではなく、指摘を待たずに先回りさせる方針です。CLAUDE.mdは短く保つほうがよい、というのがベストプラクティスの立場なので、項目は増やしすぎないでください。

指摘別・直し方の実例

READMEの「Good」にあたる書き方を、Claudeへの指示としてどう伝えるかを指摘別に示します。

インデックスを同時作成にする(Postgres)

Postgresでadd_indexを素のまま実行すると書き込みを塞ぎます。READMEの直し方は、algorithm: :concurrentlyを付け、disable_ddl_transaction!を宣言することです。

class AddSomeIndexToUsers < ActiveRecord::Migration[8.1]
  disable_ddl_transaction!
 
  def change
    add_index :users, :some_column, algorithm: :concurrently
  end
end

disable_ddl_transaction!を忘れるとマイグレーションが失敗する、とREADMEにあります。失敗した出力をそのまま返せば、Claudeはこの行を足して直します。同じ理由でadd_referenceはindex: {algorithm: :concurrently}を渡します。

外部キーとNOT NULLは2つのマイグレーションに割る

Postgresの外部キーは、まずvalidate: falseで追加し、検証は別のマイグレーションで行います。

class AddForeignKeyOnUsers < ActiveRecord::Migration[8.1]
  def change
    add_foreign_key :users, :orders, validate: false
  end
end
 
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[8.1]
  def change
    validate_foreign_key :users, :orders
  end
end

既存カラムのNOT NULL化も同型で、まずvalidate: falseのチェック制約を足し、別のマイグレーションで検証してからchange_column_nullを行い、チェック制約を外します。Claudeには「2ファイルに分けて、検証側は別デプロイ」と指示します。

カラム型変更・名前変更は段階を並べさせる

型変更と名前変更は、1つのマイグレーションでは直せません。READMEの手順は次の6段階です。

手順

カラムの型変更・名前変更の安全な手順

  1. 1

    新しいカラムを作る

    新しい型または新しい名前のカラムを追加します。

  2. 2

    両方に書き込む

    アプリのコードが旧カラムと新カラムの両方に書くようにします。

  3. 3

    既存データを移す

    旧カラムの値を新カラムへバックフィルします。

  4. 4

    読み取りを切り替える

    アプリが新カラムから読むようにします。

  5. 5

    旧カラムへの書き込みを止める

    コードから旧カラムへの書き込みを外します。

  6. 6

    旧カラムを削除する

    旧カラムを落とします。最初のカラム削除の手順(ignored_columns)に従います。

6段階を1つのPRに詰めると、結局どこかで危険な操作が混ざります。「今日のPRは第1段階だけ」と範囲を切って依頼し、残りはTODO.mdに段階表として書かせておくと、次の依頼で続きから進められます。なお、Postgresでは一部の型変更はテーブル書き換えを伴わず安全で、READMEに一覧(stringの:limit拡大やtextへの変更など)があります。指摘が出たとき、その一覧に当たるかどうかもClaudeに確認させる価値があります。

検査をすり抜ける2つの穴

strong_migrationsが保証するのは、検査一覧にある操作だけです。次の2つは別に手当てが必要です。

バックフィルは検出されません。READMEは、同じトランザクション内でテーブルを変更しつつデータを更新すると、バックフィルが終わるまでテーブルがロックされ続けると警告しています。安全に書く条件は、バッチ分割・間引き(sleep)・トランザクション外での実行の3つです。

class BackfillSomeColumn < ActiveRecord::Migration[8.1]
  disable_ddl_transaction!
 
  def up
    User.unscoped.in_batches(of: 10000) do |relation|
      relation.where(some_column: nil).update_all some_column: "default_value"
      sleep(0.01) # throttle
    end
  end
end

update_all以外の方法で更新するときは、先にUser.reset_column_informationを呼ぶ必要があるとも書かれています。検査が通っても、この3条件を満たしている保証にはなりません。CLAUDE.mdに入れ、Claudeにも守らせます。

SQL直接実行も保証の外です。executeで生SQLを書くと、strong_migrationsはその安全性を確かめられず、safety_assured { execute "..." }で人が責任を持つ形になります。この箇所のレビューは、フックの差し戻しで人の目に入るので、結果として検査の穴を人が埋める運用になります。

つまずきやすい点

  • MySQLとPostgresで直し方が違う。外部キーはPostgresならvalidate: false、MySQLならforeign_key_checksを切る手順で、前者は検証を後回しにできるが後者は全行が正しいと確信できる場合に限る、とREADMEにあります。チェック制約のMySQL側には「安全な方法がある場合は教えてほしい」とあるだけで、公式の安全策がありません。DBの種類は依頼文に必ず書いてください。
  • 指摘文を要約させない。要約を挟むと、ignored_columnsの行やdisable_ddl_transaction!のような肝が落ちかねません。出力は全文を渡します。
  • すでにあるマイグレーションを直させない。本番で適用済みのファイルを書き換えると、環境ごとに履歴がずれます。直すのは今回新しく作ったファイルだけにし、適用済みのものは新しいマイグレーションで上書きする運用が基本です。RDBの別ツールでも同じ考え方で、FlywayをClaude Codeに触らせる記事やAlembicの往復検証が、適用済みファイルを触らせない規約を扱っています。

まとめ

strong_migrationsの強みは、危険な操作を止めたうえで「次に何をするか」をエラー文に含めている点です。Claude Codeに渡す入力としてそのまま使えるため、人が仲介する場面が減ります。

仕組みとして固めるなら、役割分担は3つです。Stop hookでdb:migrateを完了条件にして、safety_assuredはPostToolUse hookで人の判断に回し、検査が見ないバックフィルはCLAUDE.mdで縛ります。Railsアプリの雛形づくりから始める場合は、Railsアプリ開発の手順でdb:migrateまでの基本を押さえておくと、この記事の設定が自然につながります。

参照した一次ソース

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

  1. github.com/ankane/strong_migrations
  2. code.claude.com/docs/en/best-practices
  3. code.claude.com/docs/en/hooks-guide
  4. code.claude.com/docs/en/permissions
この記事を共有:XはてブLinkedIn