ある朝、3つのリポジトリの差分を見返していて、身に覚えのないファイルが並んでいることに気づきました。next.config.ts.bak。コミット履歴をたどると、前夜のリダイレクト整合性チェックを一括で走らせた処理が、.ts 本体と一緒にそのバックアップまで commit して push していたのです。
ビルドは通っていました。サイトも動いていました。だからこそ、誰も止めてくれませんでした。
私は2014年から個人開発でアプリとブログを並行運用していて、Dolice Labs では4つの技術ブログを Claude Code 経由のスクリプトで毎日更新しています。整形やリンク修正のような「機械的だけど壊れると怖い」処理を自動化していくと、この種の静かな混入が一番厄介になります。手を動かして直すミスより、自動化が淡々と運んでしまうゴミの方が見つけにくいのです。
何が混入したのか — --fix が残すバックアップ
きっかけは、自作の整合性チェッカーでした。削除済み記事へのリダイレクトと、復活した記事の MDX が衝突していないかを検査するスクリプトに、--fix オプションを付けて自動修正させていました。
このスクリプトは、書き換える前の next.config.ts を next.config.ts.bak という名前でリポジトリ直下に退避します。安全のためのバックアップです。問題は、その直後のコミット処理がこうなっていたことでした。
# 自動化パイプラインの末尾でやっていたこと
python3 redirect_integrity.py "$REPO" --fix
git add -A # ← ここで .bak まで巻き込む
git commit -m "Fix redirect integrity"
git push origin maingit add -A は「作業ツリーの変更すべて」をステージします。新規作成された next.config.ts.bak も当然「新しいファイル」なので、何の警告もなく追加されます。.bak は Next.js のビルドに無害なので、CI も Cloudflare も赤いランプを出しません。結果として、gemilab・antigravitylab・rorklab の3リポジトリにバックアップファイルが居座ることになりました。
なぜ気づきにくいのか
このバグの本質は、「壊れないこと」にあります。
.bak ファイルが1つ増えても、サイトは普通に表示されます。テストも通ります。エラーログにも何も出ません。リポジトリがほんの少し汚れるだけで、その汚れは次に同じ --fix を走らせたとき上書きされるだけで、自然には消えません。静かに溜まっていきます。
しかも .bak は本体とほぼ同じ内容なので、後から差分を眺めても「これは何だろう」と引っかかりにくいのです。私が気づけたのは、たまたま3リポを横断して差分を見比べていて、同じ名前のファイルが3つ並んでいたからでした。
自動化において、終了コードが 0 でログが緑であることは「正しさ」を意味しません。git push がエラーを出さないことと、リポジトリが意図通りであることは別の問題です。同じ「無音の失敗」という観点では、git push が成功表示なのに反映されない罠 も根が同じで、自動化では「緑のログ」こそ疑う対象になります。
再現してみる
手元で簡単に再現できます。
mkdir -p /tmp/bak-demo && cd /tmp/bak-demo
git init -q
echo "export const config = {}" > next.config.ts
git add next.config.ts && git commit -qm "init"
# in-place 修正ツールが .bak を残す典型例
sed -i.bak 's/{}/{ trailingSlash: true }/' next.config.ts
ls
# → next.config.ts next.config.ts.bak
git add -A
git status --short
# A next.config.ts.bak ← 意図せずステージされている
# M next.config.tssed -i.bak は macOS でよく使う書き方で、これも .bak を必ず残します。GNU sed の sed -i(拡張子なし)はバックアップを作りませんが、BSD/macOS の sed -i '' を書き間違えて sed -i.bak になっているケースは現場で本当によく見かけます。--fix 系の自作スクリプトも、安全のためにバックアップを書くものが多く、同じ罠を踏みます。
対処 — ステージ対象を「足し算」で決める
一番確実なのは、git add -A をやめて、コミットしたいものだけを明示的に列挙することです。
# Before: 全部入り(.bak も巻き込む)
git add -A
# After: 触ってよいパスだけを足し算で指定する
git add next.config.ts content/
git status --short # 余計なものが入っていないか目視
git commit -m "Fix redirect integrity"ブログ更新のパイプラインなら git add content/ のように、生成・編集する対象ディレクトリだけを指定します。設定ファイルを触る処理なら、そのファイル名を直接書きます。「全部追加してから余計なものを外す」より、「必要なものだけ足す」方が事故が起きません。
それでも git add -A を使いたい事情があるなら、修正ツールの直後にバックアップを掃除してからステージします。
python3 redirect_integrity.py "$REPO" --fix
rm -f "$REPO"/next.config.ts.bak # 退避ファイルを先に消す
git add -A再発防止 — .gitignore で構造的に締める
掃除や限定 add は「忘れたら破れる」運用ルールです。人間が手で回すなら覚えていられますが、毎日走る自動化では一度抜けると静かに復活します。構造で止めるのが本筋です。
リポジトリの .gitignore にバックアップ系の拡張子をまとめて入れておけば、git add -A を使っても二度とステージされません。
# in-place 修正ツールが残すバックアップ
*.bak
*.orig
*.tmp
*~これを4リポジトリすべてに同じ内容で入れておくと、どのスクリプトがどんなバックアップを残しても、コミット対象から自動的に除外されます。.gitignore の挙動でつまずいた経験がある方は、Claude Code で .gitignore のファイルが見つからないと言われる問題 も合わせて読むと、無視ルールと検索対象の関係が整理できます。
もう一段の保険として、ステージ後・コミット前に「想定外のファイルが混ざっていないか」を機械的に弾くガードを入れておくと安心です。自動化パイプラインの末尾にこの数行を置くだけで、.bak のような退避ファイルが紛れ込んだ瞬間に処理を止められます。
# コミット直前: バックアップ系がステージされていたら中断する
if git diff --cached --name-only | grep -qE '\.(bak|orig|tmp)$|~$'; then
echo "🛑 退避ファイルがステージされています。コミットを中止します。"
git diff --cached --name-only | grep -E '\.(bak|orig|tmp)$|~$'
exit 1
fi
git commit -m "Fix redirect integrity"人の目が入らない処理ほど、こうした「最後の関所」が効きます。私はこの数行を入れてから、同じ混入を一度も繰り返していません。
すでに混入してしまった .bak は、.gitignore を足しただけでは消えません。追跡対象から外す操作が別途必要です。
git rm --cached next.config.ts.bak # 履歴は残しつつ追跡解除
git commit -m "Remove stray backup file from tracking"自動化では「足し算 add」を既定にする
今回の混入は、たった1つのコミットコマンドの書き方から生まれました。git add -A は対話的に使うぶんには便利ですが、人の目が入らない自動パイプラインでは、対象を明示する git add <path> を既定にすべきだと考えています。コミットメッセージを Claude Code に生成させるような仕組みを組むときも、何をステージするかは自分で握っておくのが安全です(その設計はコミットメッセージ自動生成の運用で触れています)。
自動化は、正しい手順を黙々と繰り返してくれます。裏を返せば、間違った手順も黙々と繰り返します。何をリポジトリに入れるかという判断だけは、機械任せにせず手元に残しておきたいところです。
同じ課題に向き合っている方の参考になれば幸いです。