Claude Media
Claude Codeで一括リネームのスクリプトを書く — dry-runの対応表を見てからmvを許可

Claude Codeで一括リネームのスクリプトを書く — dry-runの対応表を見てからmvを許可

Claude Codeに一括リネームのスクリプトを書かせ、dry-runで変更前後のCSVを出させて確認してから実行するまでの手順と、--applyだけを確認に残すpermissions設定を説明します。

Claude Codeで一括リネームのスクリプトを書く — dry-runの対応表を見てからmvを許可

一括リネームは、間違えたときの被害が見えにくい作業です。mv は上書きしても警告を出さず、数百ファイルの名前が一度に変わるため、あとから元の名前を復元する手がかりが残りません。Claude Codeに頼むときは、mv を直接打たせるのではなく、変更前後の対応表をCSVで出すだけのdry-runと、そのCSVのとおりに mv する実行を分けたスクリプトを書かせます。

この記事では、そのスクリプトの形と、dry-runだけを確認なしで流し、--apply だけを毎回確認に残すpermissions設定を作ります。あわせて、この設定が効かなくなる書き方も扱います。

一括リネームは「計画・確認・実行」の3段に割る

Claude Codeに「ファイル名を全部kebab-caseにして」と頼むと、for ループで mv を並べたコマンドが一度に走りがちです。確認の機会は、ループ全体への1回の承認しかありません。承認する側は、どのファイルがどの名前になるのかを、コマンド文字列から読み取ることになります。

そこで、作業を3段に分けます。

手順

一括リネームを3段に分ける

  1. 1

    計画: dry-runで対応表を作る

    スクリプトを引数なしで実行すると、rename-plan.csv に「旧名・新名・状態」を書き出すだけで、ファイルは動かしません。

  2. 2

    確認: 人が対応表を読む

    CSVを開き、新名が衝突している行(conflict)や、意図しない名前になっている行がないかを見ます。直したい点があれば、Claudeにスクリプトの変換規則を直させて、もう一度dry-runを流します。

  3. 3

    実行: --applyだけを確認に残す

    --apply を付けたときだけ、CSVの ok の行を mv -n で実行します。この呼び出しだけを、permissionsで毎回の確認に残します。

この分け方のよい点は、確認の対象が「コマンドの文字列」から「CSVの中身」に変わることです。承認ダイアログで読めるのは bash scripts/rename.sh --apply までですが、CSVなら全件の対応が表で並びます。

Claudeにスクリプトを書かせる指示

指示には、仕様の核になる3点を入れます。dry-runが既定であること、衝突を検出すること、実行は --apply を明示したときだけであること、の3つです。

docs/ 直下の *.md を、小文字・スペースをハイフンに置き換えた名前に変えたい。
scripts/rename.sh を作って。仕様は次のとおり。
- 引数なしはdry-run。ファイルは動かさず、rename-plan.csv に old,new,status を出す
- 新名が既に存在する行は status を conflict にする
- --apply のときだけ、rename-plan.csv の status が ok の行を mv -n で実行する
- --apply は rename-plan.csv が無ければ失敗する
作ったら、実行はせず、スクリプトの中身を見せて。

最後の1行が大事です。スクリプトを書いたその場で動かさせないと、確認の段に入る前に実行されてしまいます。

出来上がるスクリプトは、たとえば次のような形になります(サンプルとして、手元の空のディレクトリで動かした形です)。

#!/usr/bin/env bash
# 使い方: bash scripts/rename.sh          対応表 rename-plan.csv を作るだけ(dry-run)
#         bash scripts/rename.sh --apply  rename-plan.csv のとおりに mv する
set -euo pipefail
PLAN=rename-plan.csv
 
if [ "${1:-}" = "--apply" ]; then
  [ -f "$PLAN" ] || { echo "先に dry-run で $PLAN を作ってください" >&2; exit 1; }
  tail -n +2 "$PLAN" | while IFS=, read -r old new status; do
    [ "$status" = "ok" ] || continue
    mv -n -- "$old" "$new"
  done
  exit 0
fi
 
echo "old,new,status" > "$PLAN"
find docs -maxdepth 1 -name '*.md' | sort | while read -r old; do
  base=$(basename "$old" .md)
  new="docs/$(echo "$base" | tr 'A-Z ' 'a-z-').md"
  [ "$old" = "$new" ] && continue
  if [ -e "$new" ]; then status=conflict; else status=ok; fi
  echo "$old,$new,$status" >> "$PLAN"
done
cat "$PLAN"

mv -n は、移動先に同名のファイルがあれば上書きしないオプションです。dry-runで衝突を検出していても、確認から実行までの間にファイルが増えることがあるため、二重の安全策として残します。

ただし、mv -n は上書きを避けるだけで、飛ばしたことを知らせません。macOS(bash 3.2.57)で試すと、移動先が既にあっても終了コード0で戻り、直後の echo "renamed" は「成功」と表示しました。スクリプトにログを足すときは、mv の後ろに成功の表示を続けず、[ -e "$new" ] で移動先を先に確かめます。

dry-runの出力で見るところ

docs/ に Setup Guide.md、API Notes.md、api-notes.md、Release Plan.md、readme.md を置いて、引数なしで実行すると、次の対応表が出ます。

bash scripts/rename.sh
old,new,status
docs/API Notes.md,docs/api-notes.md,conflict
docs/Release Plan.md,docs/release-plan.md,ok
docs/Setup Guide.md,docs/setup-guide.md,ok

readme.md は変換後も同じ名前なので、対応表に出ません。API Notes.md は変換先の api-notes.md が既にあるため conflict になり、--apply では飛ばされます。この行が見つかるのが、dry-runを挟む価値です。直接 mv を並べていれば、mv が api-notes.md を上書きしていたかもしれません。

CSVを読むときの観点は3つあります。

  • conflict の行が出ていないか。出ていれば、どちらを残すかを人が決める
  • 件数が想定と合っているか。100件のつもりが3件なら、find の条件が狭すぎる
  • 新名が意図した形になっているか。先頭の数行と末尾の数行を、目で見る

このスクリプトは、ファイル名にカンマが含まれるとCSVの列がずれます。名前にカンマがありうる対象では、区切り文字を変えるよう、Claudeにスクリプトの修正を頼みます。また、macOSの標準設定のように大文字・小文字を区別しないファイルシステムでは、README.md を readme.md にするような大文字小文字だけの変更は、-e の存在判定が「既にある」と答えて conflict になることがあります。この場合は、いったん別名を経由する変換が必要になるため、Claudeに変換規則の追加を頼みます。

permissionsで --applyだけを確認に残す

次は、権限の設定です。考え方は「dry-runの呼び出しは丸ごと許可し、--apply は毎回聞く」です。

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

{
  "permissions": {
    "allow": [
      "Bash(bash scripts/rename.sh)"
    ],
    "ask": [
      "Bash(bash scripts/rename.sh --apply)",
      "Bash(mv *)"
    ]
  }
}

allowの Bash(bash scripts/rename.sh) には * がありません。* のないBashルールは、完全に同じコマンド1つにだけ一致します。そのため、bash scripts/rename.sh --apply のように引数が付いたコマンドは、このallowには当たりません。

ここで Bash(bash scripts/rename.sh *) と書いてしまうと、--apply もallowに含まれます。ルールはdeny、ask、allowの順に評価され、最初に一致したものが結果を決めます。askとallowの両方に一致するコマンドはaskが勝つので、--apply にaskを足しておけば、広いallowを書いた場合の保険にもなります。

acceptEditsモードとmvの関係

acceptEdits モードは、ファイル編集に加えて、mkdir、touch、mv、cp のような一般的なファイルシステムコマンドも、作業ディレクトリ内のパスに限って自動承認します。このモードでClaudeが mv を直接打つと、確認なしで通ります。

これを止めるのが、askの Bash(mv *) です。permissions.ask に書いたものは、acceptEdits や bypassPermissions のようにほかの理由で承認されるモードでも、確認が出ます。リネームの作業中だけ acceptEdits で回したいときは、mv の直接実行をaskで押さえておくと、スクリプトを経由しない抜け道を塞げます。

この設定が効かない場面

Bashの権限ルールは、Claudeが書いたコマンドの文字列を照合します。スクリプトの中で何が起きるかは見ていません。ここを取り違えると、設定が安全装置として働かなくなります。

状況結果
bash ./scripts/rename.sh --apply のようにパスの書き方が違う結果askにもallowにも当たらず、モードの既定の判断になる
timeout 30 bash scripts/rename.sh --apply結果timeout などの固定のラッパーは取り除いて照合されるため、askに当たる
/bin/mv -n a b結果Bash(mv *) のaskは、別の呼び方には効かない
bash -c 'mv a b'結果同じく、askに当たらない
スクリプトの中の mv結果コマンド文字列に出ないので、ルールは何も見ない

この中で見落としやすいのは、最後の行と、スクリプトそのものを書き換えられる点です。dry-runを許可しているということは、Claudeが scripts/rename.sh を編集してから、同じ「引数なし」の呼び出しをすれば、許可のまま中身だけ別物が走る、ということです。

対策は2段あります。

  1. スクリプトの中身を人が読んで承認したら、Edit(scripts/rename.sh) をdenyに追加して、以降の書き換えを止める。Edit のルールは、ファイルを編集する組み込みツール全般に適用される
  2. OSの水準でパスを限定したいときは、サンドボックスを有効にする。ReadとEditのdenyルールは、Pythonやシェルスクリプトのように、自分でファイルを開く別プロセスには効かないため
{
  "permissions": {
    "deny": [
      "Edit(scripts/rename.sh)"
    ]
  }
}

別の対策として、PreToolUseフックで、Bashのコマンド全文を自前のロジックで検査する方法もあります。フックでは、--apply のときに rename-plan.csv が新しいかどうかを調べて、古ければ止める、という検査が書けます。

dontAskモードでは--applyが拒否される

確認を出さないモードに dontAsk があります。このモードは、本来なら確認が出る呼び出しをすべて自動で拒否します。askに入れた --apply も、確認ではなく拒否になります。

CIや claude -p の非対話実行でdry-runだけを回し、CSVを成果物として残したい場合には、この性質が向いています。--apply は人が手元で実行するか、確認が出るモードで流します。dry-runのallowルールは dontAsk でも有効なので、計画の段までは自動化できます。

実行後に戻せるようにする

mv -n は上書きを防ぎますが、名前を変えた事実そのものは元に戻りません。対応表があれば、列を入れ替えた逆向きのCSVで戻せます。--apply の直前に、CSVを日付つきで退避しておく運用を、Claudeにスクリプトへ入れさせます。

cp rename-plan.csv "rename-plan.$(date +%y%m%d-%H%M).csv"

戻すときは、退避したCSVの old と new を入れ替えて、status が ok だった行を逆向きに mv -n します。Gitで管理しているディレクトリなら、実行前にコミットしておくのが最も簡単です。履歴を残したい場合は、スクリプトの mv を git mv に置き換えるようClaudeに頼みます。その場合は、askのルールに Bash(git mv *) も足します。

古いCSVを実行しない

この手順には、もう1つ落とし穴があります。dry-runのあとにファイルが増減したり、スクリプトを直したりすると、CSVが現状と合わなくなります。直したのにdry-runを流し直さないまま --apply すると、古い対応表のとおりに動きます。

運用の決まりとして、スクリプトを直したら必ずdry-runからやり直す、と決めます。Claudeに頼むときも、「直したらdry-runを流して、CSVの conflict の件数を報告して」と書いておくと、確認の材料が毎回そろいます。

--apply の確認ダイアログを出す前に、CSVの先頭を開いて、作成日時と件数がスクリプトを直した直後のものかを確かめます。

まとめ

一括リネームで守りたいのは、実行前に全件の対応を人が読むことです。dry-runが既定のスクリプトと、完全一致のallow、--apply と mv に付けたask、の3つを組み合わせると、確認の対象はコマンド名ではなくCSVの中身になります。スクリプトを承認したあとはEditをdenyにして、書き換えで抜けられないようにします。

同じ「読むだけは許可し、書き込みだけ確認に残す」考え方は、DVCの dvc push だけを確認に残す設定や、本番へのスキーマ変更を権限で止める手順でも使えます。&& でつないだコマンドがどう判定されるかは、Bashの複合コマンドと権限判定にあります。

参照した一次ソース

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

  1. code.claude.com/docs/en/permissions
  2. code.claude.com/docs/en/settings-reference
  3. code.claude.com/docs/en/headless
この記事を共有:XはてブLinkedIn