Claude Codeのセッションは、会話の記録を ~/.claude/projects/<project>/<uuid>.jsonl に随時書き出している。これがあるから、クラッシュしても claude --resume で続きから戻れる。だが、この保存がエラーも警告も出さずに止まる事故が報告された(起票 #73937、data-loss ラベル)。しかも、止まったことに利用者が気づく手段が用意されていない。
この記事は、何が起きるのか、なぜ怖いのか、そしてその場で気づくための検知を実際に動かして確かめた手順をまとめる。引用する事故は GitHub の公開 issue で番号から辿れる実在のものである。
先に結論だけ欲しい人へ。取り返しの付かないデータ消失は、Claude Code では「起きてから戻す」より「起きる前に検知して止める」ほうが確実に安い。同じ構造の事故(削除・上書き・保存停止・巻き戻り)を症状別にまとめ、検出と復旧の手順まで書いたのが Claude Code 事故防止ガイド(¥800・第3章まで無料・以降も毎月更新) だ。まず無料の cc-safe-setup の hook を入れておくと、この種の事故の多くを実行の手前で止められる。
会話履歴が消える仕組み
報告によれば、こういう流れだったという(#73937)。手元で再現したのは後半の検知の部分で、この事故の発生そのものは報告に基づく伝聞である。
- 長時間動かしていたセッションで、記録ファイルが約35 MB・約15,000行まで育っていた。
- セッションの途中で、Claude Max のアカウントを切り替えた。
- Claude Code は切替の前に
~/.claude.backup-before-account-switch-<uuid>.jsonlにバックアップを作った。 - ところが切替の後、動いている側の記録ファイルが、以前の日付の状態まで巻き戻り、それ以降いっさい書き込まれなくなった。セッションはそのまま2週間ほど対話に使われ続けたのに、である。
報告の内容からは、切替のバックアップの仕組みが、退避のコピーを作るだけでなく、生きている記録ファイル自体を古いスナップショットで上書きし、書き込みの経路を壊したように見える。
気づけないことが、いちばん怖い
- 巻き戻った時点より後に生成された会話は、モデルの文脈の中だけに存在し、ディスクには一度も残らない。セッションを閉じた瞬間に消える。
- クラッシュの後に
claude --resume <uuid>で開くと、古いスナップショットが読み込まれ、大量の履歴が欠けていることを示す表示は何も出ない。 - 記録ファイルの更新時刻は止まっているが、それはどこにも表示されない。利用者は、行数や時刻を手で突き合わせない限り「保存が止まった」ことに気づけない。
取り返しの付かないデータ消失の多くがそうであるように、この事故も「実行する前に確認する」「壊す前に警告する」という段が抜けていることに行き着く。
その場で気づく——保存の健全性を毎ターン確かめる
報告者はすでに、毎ターンの後に記録ファイルを別の場所へコピーする Stop hook を回避策にしている。これは正しい。ただし、コピーは内容を残すが、「いつ保存が止まったか」は教えてくれない。そこで、検知だけを足す小さな Stop hook を作った。
原理はこうだ。Stop はターンが1つ終わるたびに発火し、その入力には transcript_path が含まれる。だから Stop の瞬間は「たった今1ターン分の行が書かれたはず」の点検の好機である。この瞬間に、記録ファイルの行数と更新時刻が前回の Stop からまったく進んでいなければ、直前のターンがディスクに残っていない合図。行数が減っていれば、巻き戻りの合図である。
#!/usr/bin/env bash
# transcript-write-health.sh — Stop hook。沈黙の保存停止と巻き戻りを検知する(#73937)。
input=$(cat)
tp=$(printf '%s' "$input" | jq -r '.transcript_path // empty')
sid=$(printf '%s' "$input" | jq -r '.session_id // "nosid"')
[ -z "$tp" ] && exit 0
[ ! -f "$tp" ] && exit 0
state_dir="${HOME}/.claude/transcript-health"; mkdir -p "$state_dir"
state_file="${state_dir}/${sid}.state"
cur_lines=$(wc -l < "$tp" 2>/dev/null | tr -d ' ')
cur_mtime=$(stat -c %Y "$tp" 2>/dev/null || stat -f %m "$tp" 2>/dev/null)
warn=""
if [ -f "$state_file" ]; then
read -r prev_lines prev_mtime < "$state_file"
if [ "${cur_lines:-0}" -lt "${prev_lines:-0}" ]; then
warn="⚠ 記録が巻き戻りました(${prev_lines}→${cur_lines}行)。生きたファイルが古いスナップショットで置換された可能性(#73937)。復旧点を確認してください。"
elif [ "${cur_lines:-0}" -eq "${prev_lines:-0}" ] && [ "${cur_mtime:-0}" -eq "${prev_mtime:-0}" ]; then
warn="⚠ 直前のターンが $tp に保存されていません(行数・更新時刻が前ターンから不変)。保存が沈黙のうちに止まった可能性(#73937)。今の会話は文脈内だけの恐れがあるので、独立バックアップと再起動を検討してください。"
fi
fi
printf '%s %s\n' "${cur_lines:-0}" "${cur_mtime:-0}" > "$state_file"
[ -n "$warn" ] && jq -cn --arg m "$warn" '{systemMessage:$m}'
exit 0
settings.json で Stop に登録する(内容をコピーする既存のバックアップ hook と並べて両方動かしてよい)。
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "/絶対パス/transcript-write-health.sh" } ] } ] } }
手元で、使い捨ての HOME を使って4つの場合を実際に走らせて確かめた。状態は session_id ごとに持つ。
- 初回(前の状態なし)→ 何も出さず基準だけ記録する。
- 正常に伸びた(3→5行、更新時刻も進む)→ 何も出さない。
- 沈黙の停止(ターンの後も行数・更新時刻が不変)→「保存されていません」の警告を出す。
- 巻き戻り(5→2行)→「巻き戻りました」の警告を出す。
限界を正直に
- これは検知だけを足す。内容の復旧を可能にするのは、報告者のバックアップ hook のほうである。この hook は「いつバックアップに手を伸ばすべきか」を教えるだけだ。
- 検知はターンの境界(
Stop)で起きるので、巻き戻りの瞬間ではなく、その次のターンで気づく。それでも、2週間後に手で差分を取るよりはるかに早い。 - 根本の修正は提供側にある。報告者が求めているとおり、切替のバックアップは追記のみにして、生きているファイルを絶対に上書きしないのが本筋だ。hook は上書きを防げず、起きたことを見せるだけである。停止をブロックしないよう
systemMessageで警告している。
まとめ
削除・上書き・保存停止・巻き戻り——取り返しの付かないデータ消失は、症状は違っても「壊す前に数え、確認し、警告する」段が抜けている、という同じ構造に行き着く。だから個別の事故を追うより、構造で先回りして止めるほうが確実に安い。
同じ構造の事故を症状別に集め、検出・復旧・予防の手順まで、実際に起きた起票から逆算してまとめたのが Claude Code 事故防止ガイド(¥800・第3章まで無料・以降も毎月更新) だ。無料の cc-safe-setup の hook を入れて、もう一段深く予防したい人への手引きになっている。