Claude Media
Claude Codeでwp search-replaceを回す — WordPress移行の手順

Claude Codeでwp search-replaceを回す — WordPress移行の手順

WordPressのDB移行でURLを置き換えるとき、wp search-replaceを--dry-runから回させ、バックアップ前の本番実行をpermissionsとhookで止める設定をまとめます。

WordPressを別ドメインや別サーバーへ移すと、データベースに残った旧URLの置換が必ず要ります。この作業をClaude Codeに任せるなら、先に決めておきたいのは「どこまで自動で走らせ、どこで人間が止めるか」です。wp search-replace は--dry-runで本番と同じ処理を保存なしで試せるので、この性質を許可設定に写せば、件数を見てからだけ本番に進む流れを組めます。

結論から書きます。--dry-run付きの実行とバックアップ取得は許可リストに入れ、--dry-runなしの実行は許可しません。さらに「直近のバックアップが無いと止める」条件だけは許可ルールで書けないので、PreToolUseフックで補います。

wp search-replaceのdry-runで分かること

wp search-replaceは、選んだテーブルの全行を走査し、最初の文字列を2番目の文字列に置き換えるコマンドです。WP-CLIのリファレンスでは、PHPのシリアライズデータを適切に扱い、主キーの値は変えないと説明されています。主キーを持たないテーブルは処理の対象外です。

--dry-runを付けると、置換の処理を最後まで実行してレポートを出し、データベースには保存しません。本番に流す前の確認に使えます。

wp search-replace 'https://old.example.com' 'https://new.example.com' \
  --skip-columns=guid --dry-run

リファレンスの例にも、--skip-columns=guidでguid列を外した置換が載っています。件数だけ欲しいときは--format=countが使えます。出力形式はtableかcountのどちらかです。

一方、dry-runが教えてくれないこともあります。

  • 置換後のサイトが表示崩れしないか
  • 置換対象の文字列が意図しない箇所に含まれていないか(レポートの表で、列とテーブルを目で見て判断します)
  • 本番DBの現在の状態から見て、バックアップが取れているか

最後の点が、許可設定の出番です。

シリアライズ値を守る仕組みは既定で働く

URL置換でよく壊れるのは、オプションやウィジェットに入ったPHPのシリアライズ値です。文字列の長さを値の中に持つ形式なので、SQLダンプをエディターで文字列置換すると長さがずれます。wp search-replaceはこの形式を認識して扱うので、Claude Codeに「sedで置換して」と書かせず、このコマンドを使わせる理由になります。

既定ではSQLクエリで高速に処理し、シリアライズデータを含む列だけPHPに切り替えます。複雑な構造が心配なら--preciseで全列をPHPで処理させられますが、遅くなります。--regexも同様に遅く、リファレンスには15〜20倍の時間がかかるとあります。移行の置換に正規表現が要る場面は少ないので、後述の設定では禁止側に置きます。

許可リストで強制できること、できないこと

Claude Codeの許可ルールはdeny、ask、allowの順に評価され、最初に一致した結果で決まります。具体的なルールほど優先される仕組みではありません。広いdenyが狭いallowを打ち消し、askとallowも同じ関係です。

このため、「wp search-replace *は確認を求め、--dry-run付きだけ許可する」という書き方はできません。askが広く一致して、狭いallowが効かなくなるからです。代わりに次の形にします。

設計

ルールの役割分担

  • allowに入れるもの

    --dry-runで終わる置換と、backups/配下へのwp db exportだけです。

  • denyに入れるもの

    全テーブルを対象にする--all-tables系のフラグと、遅くて結果が読みにくい--regexです。

  • どちらにも書かないもの

    --dry-runなしの本番置換です。ルールに一致しないので、既定の権限モードでは毎回確認が出ます。

もうひとつ、Bashルールはコマンド文字列に対するパターン照合であることに注意が要ります。ルールは*を任意の文字列に当て、複合コマンドは区切りごとに分解して、全部の要素が許可されて初めて通ります。wp db export backups/pre.sql && wp search-replace ...のようにつなぐと、後ろの置換が許可されていないので確認が出ます。

また、引数の位置が違うだけで一致しなくなります。

書き方Bash(wp search-replace * --dry-run)に
wp search-replace A B --dry-runBash(wp search-replace * --dry-run)に一致する
wp search-replace --dry-run A BBash(wp search-replace * --dry-run)に一致しない(末尾が--dry-runではない)
wp --path=/var/www search-replace A B --dry-runBash(wp search-replace * --dry-run)に一致しない(search-replaceの前にグローバル引数がある)

一致しなかった場合、allowは効かず確認に落ちるだけなので安全側です。問題はdenyのほうで、wp --path=/var/www search-replace A B --all-tablesのように書かれるとdenyも一致しません。コマンド名の直後に書かれる形が前提のルールだと理解したうえで、この種のルールは「Claudeが普通に書く形を止める」ものとして使います。引数の形式を揃えさせるには、CLAUDE.mdに--dry-runは末尾に付けると書いておくのが現実的です。ただしCLAUDE.mdはClaudeの振る舞いを方向づけるだけで、許可の境界にはなりません。

設定の全体像

プロジェクトの.claude/settings.jsonに次を置きます。パスは例です。

{
  "permissions": {
    "allow": [
      "Bash(wp db export backups/*)",
      "Bash(wp search-replace * --dry-run)"
    ],
    "deny": [
      "Bash(wp search-replace * --all-tables*)",
      "Bash(wp search-replace * --regex*)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(wp search-replace *)",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/require-db-backup.sh"
          }
        ]
      }
    ]
  }
}

wp db export backups/*は保存先をbackups/配下に固定します。リファレンスによれば、ファイル名を省くと{dbname}-{Y-m-d}-{ランダム文字列}.sqlというファイルが作られるので、引数なしのwp db exportはこのルールに一致せず、確認に落ちます。

--all-tables*は--all-tables-with-prefixも巻き込みます。プレフィックス付きの全テーブルを対象にする指定ですが、移行の置換では範囲を広げる理由が薄いので、あえて同じ網にかけています。必要なら人間が自分の手で実行します。

wp db exportの--tablesや--exclude_tablesを使えば部分バックアップも取れますが、移行前は全テーブルが無難です。

直近のバックアップが無いと止めるフック

許可ルールは「どのコマンドか」しか見ません。「バックアップが先に取れているか」という順序の条件は、フックで判定します。フックは標準入力でJSONを受け取り、Bashならtool_input.commandにコマンド文字列が入ります。exit 2で終われば、その呼び出しは止まり、標準エラーの文面がClaudeに返ります。

#!/bin/bash
# .claude/hooks/require-db-backup.sh
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command')
 
# dry-run と SQLファイルへの書き出しはDBを変えないので通す
case "$CMD" in
  *--dry-run*|*--export*) exit 0 ;;
esac
 
# 30分以内の.sqlが backups/ にあれば通す
if find backups -name '*.sql' -mmin -30 2>/dev/null | grep -q .; then
  exit 0
fi
 
echo "直近30分のバックアップがありません。wp db export backups/<名前>.sql を先に実行してください。" >&2
exit 2

--exportはリファレンス上、置換結果をデータベースに保存せずSQLファイルとして書き出すオプションです。DBを書き換えないので、バックアップの条件から外しています。

exit 0で終わったフックは「判断なし」の扱いで、通常の許可の流れに進みます。フックが通っても、--dry-runなしの置換は許可ルールに一致しないので、確認ダイアログが出ます。つまり本番実行には、バックアップの存在というフックの条件と、人間の承認という許可の条件の両方が要ります。

フックのifフィルターは「ベストエフォート」だと説明されており、caseのパターンも単純な部分一致です。wp search-replace A B --dry-run; wp search-replace C Dのようにつなげば、後半が素通りします。ここで確実に止めたい本番環境なら、そもそもDBの認証情報をClaudeのセッションに渡さない構成が必要で、このフックは事故防止の網と位置づけてください。

移行当日の流れ

手順

本番DBに触れるまでの順序

  1. 1

    バックアップを取らせる

    wp db export backups/pre-migration.sqlを実行させます。許可リストに入っているので確認なしで走ります。

  2. 2

    dry-runで件数を見せる

    wp search-replaceを--dry-run付きで実行させ、テーブルと列ごとの置換数を表で出させます。

  3. 3

    想定外の行を読む

    postmetaやoptionsなど、置換されてほしくない場所がないかを人間が読みます。外したい列は--skip-columns、テーブルは--skip-tablesで除きます。

  4. 4

    同じ引数で本番実行を承認する

    --dry-runだけを外した同じコマンドをClaudeに提案させ、確認ダイアログの文面がdry-runと同じ引数であることを見比べて承認します。

  5. 5

    再度dry-runを回して残りを確認する

    実行後にもう一度--dry-runを回し、置換件数が0になっていることを確かめます。

手順4で見比べる、という一手間が肝です。dry-runと本番で引数が1つでも違えば、レポートの数字は意味を失います。Claudeに「dry-runの結果をそのままコピーして--dry-runだけ外す」と指示しておくと、ずれが減ります。

Claude Codeへの指示の書き方

CLAUDE.mdには、手順の順序とコマンドの書き方を置いておきます。

## WordPress DB移行のルール
- 置換は `wp search-replace <旧> <新> <オプション> --dry-run` の順で書き、--dry-run は必ず末尾に置く
- 本番実行の前に `wp db export backups/<名前>.sql` を実行し、続けて同一セッション内で dry-run の結果を表で報告する
- `--regex` と `--all-tables` は使わない。必要なら理由を書いて人間に確認する
- 複数のコマンドを `&&` でつながない

最後の行は、複合コマンドが分解されて確認に落ちるのを避けるためのものです。

置換が思った通りに進まないとき

落とし穴

マルチサイトでは--networkと--urlの扱いが変わる

マルチサイトでは、--networkを付けないと現在のサイトのテーブルだけが対象です。リファレンスの例には、本番のマルチサイトDBをローカル用に変える--url=と--networkの組み合わせが載っています。マルチサイトの移行は、dry-runの対象テーブルが想定どおりか、レポートの先頭から確かめてから進めてください。

  • レポートが空になる: 置換する文字列がそのままの形でDBに入っているかを確かめます。スキーム(httpとhttps)の違い、末尾のスラッシュ、wwwの有無で一致しなくなります。
  • 主キーのないテーブルが残る: リファレンスでは主キーのないテーブルはスキップされます。wp db query 'DESCRIBE <テーブル名>'でKey列にPRIがあるか確認できます。
  • --oldと--newが使えない: リファレンスの注記によると、この2つはまだ安定版のリリースに入っていません。ナイトリービルドへの切り替えが必要で、位置引数で書く通常の方法で足ります。

DB移行をPostgreSQLなど別の製品へ載せ替える場合は、Claude CodeでMySQLからPostgreSQLへ移行するのほうが近い作業です。一括置換の「差分と件数を見てから適用する」という考え方は、ast-grepの一括置換でも同じ型を使っています。移行後のテーマやプラグイン側のコードを直すなら、WordPressプラグイン開発の始め方とnonce・権限・エスケープ漏れの洗い出しが続きの作業になります。

まとめ

このやり方の要点は、権限の機能にできないことをフックに回す分業です。--dry-run付きの置換とバックアップ取得だけを許可し、本番実行は承認ダイアログに残し、バックアップ前の本番実行はフックが止めます。許可ルールは引数の位置に弱いので、--dry-runを末尾に置く規約をCLAUDE.mdに書き添える運用まで含めて、ひとつの仕組みと考えるのが現実的です。

参照した一次ソース

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

  1. code.claude.com/docs/en/permissions
  2. code.claude.com/docs/en/hooks-guide
  3. developer.wordpress.org/cli/commands/search-replace
  4. developer.wordpress.org/cli/commands/db/export
この記事を共有:XはてブLinkedIn