この記事をシェア
先日、やらかした。
笑い話で済んだから書けるんだけど、エンジニアなら「うわ、やばい」と思うはずのやつだ。しかも原因がAIが提案したコマンドをそのままコピペしたという、2026年っぽいトラブルだったので記録しておく。
何が起きたか
開発用のUbuntuサーバーが「ディスクがいっぱい」と悲鳴を上げていた。GitHub Actionsのself-hosted runnerが10台動いていて、それぞれの_workディレクトリにビルドキャッシュが溜まりに溜まって、合計約194GBになっていた。
面倒だなと思いながら、OpenClaw上で動いているAI秘書の「みさき」に相談した。僕が「runner全部のworkディレクトリをクリアしたい」と言ったら、すぐにコマンドを提案してくれた。
for d in /home/github-runner/runners/runner-{01..10}/_work; do
rm -rf "$d"/*
echo "Cleaned $d"
done
パッと見て、正しそうだった。runner-{01..10}でブレース展開、$d/*で中身だけ削除、echoで確認。うん、いい感じじゃないか。
コードブロックをコピーして、ターミナルに貼り付けた。
問題のコマンド、そして沈黙
コマンドを貼り付けた瞬間、ターミナルには何も起きなかった。いや、正確にはエラーが出て止まった。
^[[200~for d in /home/github-runner/runners/runner-{01..10}/_work; do
bash: syntax error near unexpected token 'do'
「あ、なんか構文エラーか。まあいいや」と思って無視した。
……これが間違いだった。
数秒後、別のコマンドを打とうとしたら、なんか挙動がおかしい。ls /etcを叩いたら No such file or directory。
え?
なぜ壊れたか — bracketed paste modeという罠
原因は bracketed paste mode だった。知らない人のために説明すると、これは現代のターミナルエミュレータが持っている機能で、複数行のテキストをペーストするとき、前後に特殊なエスケープシーケンスを自動でくっつけてくれる。
- ペースト開始:
\e[200~(つまり^[[200~) - ペースト終了:
\e[201~
本来はこの機能、セキュリティのためのものだ。コピペ時に意図しないコマンドが実行されないようにするために、「これはペーストですよ」とbashに伝えるためのものなんだけど、今回はそれが逆効果になった。
何が起きたかというと——
^[[200~for d in ...という文字列がbashに渡された- bashはエスケープシーケンスを解釈できず、forループ全体が構文エラーに
- forループは実行されなかったので、変数
$dは一度も代入されなかった - ところが次の行
rm -rf "$d"/*は独立したコマンドとして実行された $dが空なので、これはrm -rf /*に展開された- 結果:ルートディレクトリの中身を片っ端から削除
ものの数秒で /etc が消滅した。OSの設定ファイルが全滅した。
幸い /home は無事だった。おそらく github-runner ユーザーのディレクトリにプロセスが走っていて、busy状態のファイルが削除できなかったためだと思われる。でも /etc がない以上、OSとしては機能不能だ。再起動してみたら、案の定、二度と起動しなかった。
被害状況まとめ
- ✅
/home:無事(busyファイルが守ってくれた) - ✅ 重要データ:このサーバーにはなかった
- ❌
/etc:完全消失 - ❌
/usr、/lib:消失 - ❌ OS:再インストール確定
今回はテスト用のサーバーだったから笑い話で済んだ。本番だったら冷や汗どころの話じゃない。
どうすれば防げたか(5つの対策)
対策1:ワンライナーにする
複数行のコマンドをコピペするのがそもそも危険だ。ワンライナーにすれば、bracketed paste modeの問題をそもそも回避できる。
for d in /home/github-runner/runners/runner-{01..10}/_work; do rm -rf "$d"/*; echo "Cleaned $d"; done
1行なので、貼り付けてもforループが一体として処理される。
対策2:変数の空チェックを入れる
bashには ${変数:?エラーメッセージ} という書き方があって、変数が空だった場合に即座にエラーを出して処理を停止してくれる。
rm -rf "${d:?variable is empty}"/*
これが入っていれば、$d が空の場合に bash: d: variable is empty とエラーを出して止まる。rm -rf /* には展開されない。
特に rm -rf と変数を組み合わせるときは、この書き方を必ずつけるようにしたい。
対策3:スクリプトファイル経由で実行する
コマンドをターミナルに直接貼り付けるのではなく、一旦ファイルに書いてから実行する。
cat > /tmp/cleanup.sh <<'EOF'
#!/bin/bash
set -euo pipefail
for d in /home/github-runner/runners/runner-{01..10}/_work; do
rm -rf "${d:?variable is empty}"/*
echo "Cleaned $d"
done
EOF
bash /tmp/cleanup.sh
ファイル経由なら bracketed paste modeは関係ない。さらに set -euo pipefail でエラー時即停止、未定義変数の使用をエラーにする設定も入れておくと安心だ。
対策4:dry-runの習慣をつける
rm -rf を実行する前に、まず echo で展開結果を確認する。
# 本番実行の前にこれを先に流す
for d in /home/github-runner/runners/runner-{01..10}/_work; do
echo "Would delete: $d/*"
done
削除対象のパスが正しく展開されているか、目で見て確認する。30秒の確認で致命的なミスを防げる。
対策5:AIの出力を盲信しない
みさきが提案したコマンドは、構文的には正しかった。問題はそれをターミナルに貼り付けるという「人間の操作」にあった。AIは便利だが、シェルコマンドの実行環境まで把握しているわけではない。
特に rm -rf を含むコマンドは、AIが提案したものであっても、人間が必ず目視で確認すべきだ。「AIが提案したから安全」は最も危険な思い込みだ。
教訓
今回の一連の出来事を振り返ると、失敗の連鎖がいくつかあった。
まず、エラーを無視した。構文エラーが出た時点で「おかしい」と立ち止まるべきだった。エラーはシステムからのメッセージだ。特に rm -rf を含むコマンドが部分的に失敗した場合、何かが起きている可能性を疑うべきだった。
次に、dry-runを省略した。「このコマンドは安全そう」という直感を信じてしまった。194GBのキャッシュを削除するのだから、確認ステップを挟む余裕はあったはずだ。
そして、複数行コマンドのリスクを甘く見ていた。bracketed paste modeについては知識として知っていたが、「今回は大丈夫だろう」と考えてしまった。知識と実践は別物だ。
AIアシスタントは本当に便利で、みさきも今回も的確なコマンドを提案してくれた。問題はそれを受け取った僕の側にあった。どんな優秀なアシスタントも、提案を実行する人間の判断を代替することはできない。
rm -rf と変数の組み合わせは、シェルスクリプトの中で最も危険なパターンのひとつだ。本番サーバーでこれをやっていたら、と思うと……笑い話では済まない。
同じ失敗をしているエンジニアが世界中にいると思う。この記事が「あ、気をつけよう」と思うきっかけになれば幸いだ。
⚠️ rm -rf + 変数の鉄則
- 複数行コマンドのコピペは必ずワンライナーに変換する
rm -rf "$変数"/*は必ず${変数:?}を使う- 本番実行前に
echoで dry-run する - 可能な限りスクリプトファイル経由で実行する
- AIが提案したコマンドも必ず自分で内容を確認する
