ある朝、公開済みの記事を読み返していて、背筋が冷たくなりました。Claude の MCP についての記事の本文に、前日に書いた git のエラー対処の段落が、まるごと紛れ込んでいたのです。タイトルと導入は正しいのに、本文の真ん中だけが別の記事だった——しかもエラーログには何も残っていませんでした。
赤い文字が出るバグなら、まだ気づけます。この種の事故が怖いのは、すべての処理が「成功」と報告したまま、中身だけがすり替わっている点です。原因は、自動化スクリプトが使っていた一時ファイルの「名前」にありました。
何が起きていたか — 本文だけが古い記事のもの
私の記事生成は、長い MDX 本文を一度シェル変数やヒアドキュメントに収めるのではなく、いったん一時ファイルに書き出してから、フロントマターと結合する作り方をしていました。素朴に書くと、こうなります。
# 本文を一時ファイルに書き出す
cat > /tmp/body.md << 'BODY'
(ここに今回の記事本文)
BODY
# フロントマターと結合して記事を組み立てる
cat frontmatter.md /tmp/body.md > "content/articles/ja/claude-code/${SLUG}.mdx"一見すると問題はなさそうに見えます。ところが /tmp/body.md という名前が固定であることが、後で効いてきました。
複数の記事を1回の実行でまとめて生成していたとき、2本目の本文を書き出すステップが何らかの理由で失敗すると、/tmp/body.md には1本目の本文が残ったままになります。そして結合ステップはそのまま走り、2本目の記事に1本目の本文が入り込みました。
なぜ書き込みが「無音で」失敗するのか
cat > file << 'BODY' ... BODY のヒアドキュメントは、いくつかの条件で中身を書かずに終わります。しかも多くの場合、致命的なエラーを大きく出さずに通り過ぎます。
ひとつは、ヒアドキュメントの終端マーカーがずれるケースです。本文の中にたまたま BODY という行が含まれていたり、インデント付きで終端を書いていたりすると、シェルは終端を見失い、途中までしか書き込まないか、空のまま終わります。
ディスク容量の枯渇も典型です。書き込み中に空き容量がなくなると、ファイルは中途半端な状態で閉じられます。サンドボックスや CI の /tmp は容量が小さいことが多く、ここは特に踏みやすい地雷でした。容量まわりの切り分けはENOSPC でディスクが足りなくなったときの対処に詳しくまとめています。
権限の問題もあります。/tmp の所有者がプロセスと異なると書き込めず、けれど古いファイルは読めてしまう、という状況が起こり得ます。この所有権まわりは/tmp が書き込めないときに $HOME/repos へ退避する方法で扱いました。
共通しているのは、書き込みが失敗しても、その前に存在したファイルは消えずに残るという点です。cat > は書き込みに入る前にファイルを切り詰めますが、途中で失敗すれば中途半端な内容が、あるいはリダイレクト自体が走らなければ前回の内容が、そのまま居座ります。次のステップはファイルの「鮮度」を確認しないので、古い残骸を平然と読み込みます。
再現してみる
挙動を手元で確かめると、対策の勘所がつかめます。固定名のファイルに古い内容を残したまま、次の書き込みをわざと失敗させてみます。
# 1本目の本文を書き出す
echo "OLD ARTICLE BODY" > /tmp/body.md
# 2本目の書き込みを、存在しないディレクトリ経由でわざと失敗させる
cat > /tmp/nowhere/body.md << 'BODY' 2>/dev/null
NEW ARTICLE BODY
BODY
echo "write exit code: $?" # 1 が返る(でもスクリプトは止まらない)
# 鮮度を確認せずに /tmp/body.md を読むと…
cat /tmp/body.md # → OLD ARTICLE BODY が出てくる書き込みに失敗した(終了コードが 1)にもかかわらず、結合ステップが /tmp/body.md を読めば、古い本文が混ざります。set -e を付けていても、リダイレクト先のパスが別ファイルなら /tmp/body.md の残骸検出には役立ちません。
対処 — 「名前を毎回変える」と「書いた中身を照合する」
私が落ち着いたのは、二つの原則を組み合わせる方法です。発生を防ぐ「ユニーク名」と、起きても気づける「挿入後の照合」です。
1. 一時ファイルにスラッグを含むユニーク名を付ける
固定名をやめ、記事のスラッグ(と必要なら乱数)を含む名前にします。そもそも前回の残骸と同じパスにならなければ、混入は構造的に起こりません。置き場所も、容量と所有権が安定している $HOME 配下に移します。
# 記事ごとに固有のパスを作る。固定名 /tmp/body.md は使わない
WORKDIR="$HOME/.cache/article-build"
mkdir -p "$WORKDIR"
BODY_FILE="$WORKDIR/${SLUG}.body.md"
# 念のため、組み立て前に古いファイルを必ず消す
rm -f "$BODY_FILE"
cat > "$BODY_FILE" << 'BODY'
(今回の記事本文)
BODYmktemp を使うのも有効です。BODY_FILE=$(mktemp) なら衝突しない一時ファイルを確実に得られます。ただし自動化では、どの記事に対応するファイルか後から追えるよう、スラッグを名前に埋めておくほうがログを追いやすいと感じています。
2. 書き込み直後に「終了コード」と「中身」を二重で確認する
ユニーク名にしても、書き込み自体が失敗していれば空ファイルや中途半端なファイルが残ります。だから書いた直後に、終了コードとファイルの中身の両方を見ます。
cat > "$BODY_FILE" << 'BODY'
(今回の記事本文)
BODY
STATUS=$?
# (a) 終了コードを確認する
if [ "$STATUS" -ne 0 ]; then
echo "❌ 本文の書き込みに失敗しました(exit $STATUS)"
exit 1
fi
# (b) 空でないこと、今回のスラッグ固有の語が含まれることを照合する
if [ ! -s "$BODY_FILE" ]; then
echo "❌ 本文ファイルが空です: $BODY_FILE"
exit 1
fi
if ! grep -q "今回の記事を一意に表すフレーズ" "$BODY_FILE"; then
echo "❌ 期待した本文が入っていません。前回の残骸の可能性があります"
exit 1
fi3. 組み立て後の MDX も grep で照合する
仕上げに、結合してできた MDX が「正しい記事」になっているかを確認します。フロントマターの title や slug に対応するキーワードが本文側にも現れているか、逆に別記事に固有の語が混ざっていないかを見ます。
OUT="content/articles/ja/claude-code/${SLUG}.mdx"
# slug がフロントマターと本文の整合チェックに使える
if ! grep -q "slug: \"${SLUG}\"" "$OUT"; then
echo "❌ 生成された MDX の slug が一致しません"
exit 1
fi
# 直前に生成した別記事の固有語が紛れていないか(任意)
PREV_MARKER="前の記事に固有の見出し語"
if grep -q "$PREV_MARKER" "$OUT"; then
echo "❌ 別記事の本文が混入している可能性があります"
exit 1
fiこの push 直前の grep ガードという考え方は、SKILL.md に push 前 grep ガードを仕込む話で別の角度からも書きました。検索が空振りするときの落とし穴はgrep が結果ゼロを返すときの切り分けも合わせて読んでいただけると、照合の精度を上げられます。
自動化では「状態を引き継ぐファイル」をいちばん疑う
この一件で改めて感じたのは、自動化で最も危ういのは、複数の実行をまたいで生き残るファイルだということです。固定名の一時ファイルは、便利な反面、前回の状態を黙って次へ運んでしまいます。git push が何もしなくても成功するのと同じで、ファイルの「鮮度」もまた、コマンドの成功とは別の次元の問題です。終了コードだけを信じて止まっていた失敗の話は、git push が成功表示なのに反映されない罠にもまとめています。
アーティスト・クリエイターの廣川政樹です。2014年から個人開発を続けて、累計5,000万ダウンロードを超えるアプリ運営と、4つの技術ブログ(Dolice Labs)の自動更新を並行しています。手を動かして何かを作るとき、いちばん怖いのは派手な失敗ではなく、静かに正しくないものが出来上がっていることだと、宮大工だった祖父の仕事を思い出すたびに感じます。だからこそ、書いたはずの中身が本当にそこにあるかを、仕組みとして確かめておきたいのです。
まずは、自動化スクリプトの固定名の一時ファイルを一つ探して、スラッグ入りのユニーク名に変え、書き込み直後に grep -q を一行足してみてください。次に「成功ログ」が出たとき、その中身まで含めて信じられるようになります。
最後までお読みいただき、ありがとうございました。同じ無音のすり替えで青ざめる方が、一人でも減れば嬉しいです。