Macへの移行でバックアップを作り直したら、削除ガードが動いていなかった

開発claude-code-tools

Macへの移行でバックアップを作り直したら、削除ガードが動いていなかった

朝の日記コマンドの出力に、見慣れないスタックトレースが1行だけ混じっていた。

記事の生成は止まらず、最後まで通った。 だから最初は気に留めなかった。 別の記事を並列で書かせている間に、そのエラーだけ調べさせた。

無害ではなかった。

朝のログに混じっていたエラー

Nodeがモジュールの解決に失敗したときのエラーだった。

設定ファイルには、削除コマンドを実行前に検査するフックのパスが書いてある。 そのパスが旧マシンのままで、指している実体はこちらのマシンに無かった。 フックの本体を置いてあるリポジトリごと、まだクローンしていなかった。

読み込みに失敗したフックは、拒否も許可も返さない。 拒否の判断が返らなければ、コマンドはそのまま実行される。 再帰削除を止める仕組みが、止める側だけ黙って外れていた。

同じ設定ファイルには、シェルコマンドを分類器に通すための禁止文言もある。 そちらも旧マシンのパスで書いてあり、文字列が一致しないので実質的に効いていない。

移行してからの数日、その状態で動いていたことになる。

保存先を1つに減らす

バックアップ一式をMacへ移してほしい、と頼んだ。 ただし今回は保存先が1つしかない。

Windows側では、暗号化したセッション履歴をクラウドストレージと外付けSSDの2か所へ送っていた。 SSDは持ってきていないので、クラウド側だけにしてほしいと伝えた。

2系統を前提にした箇所が2つ引っかかった。 1つは外付け側への送出そのもの。 もう1つは、どこまで送ったかを記録する目印の前進判定だった。

元の設計では、片方でも失敗したら目印を前に進めない。 次の実行で同じ範囲を送り直すための仕掛けだ。 ただし系統が1つになると、無効な系統の失敗を永久に待つことになる。 有効な系統だけを見て判定するように変えさせた。

Windows側のノートPCも同じリポジトリを使う。 上書きはせず、OSを見て分岐させる方針にした。

クラウド側にはデスクトップ同期のフォルダが無いので、API経由で送る形になった。

bashが全角の括弧を変数名として食う

移植で最初に転んだのは、シェルのバージョン差だった。

macOSに最初から入っているbashは3.2で、Windows側で使っていた4系とは挙動が違う。 暗号化も送出もせず、収集と完全性検証までを流すDRY_RUNで走らせたら、変数が空になる箇所が出た。 原因は日本語のログ出力だった。

# 3.2 は「MANIFEST(...」までを変数名として読む
echo "$MANIFEST(..."

# 直した形
echo "${MANIFEST}(..."

全角の丸括弧が変数名の直後に来ると、3.2はそれを変数名の一部として読む。 存在しない名前の変数を探しにいって、空を返す。

全スクリプトを走査させたら12箇所あった。 すべて波括弧で囲う形に直した。 Windows側の4系でも意味は変わらないので、片方だけ動く直し方にはならない。

ここでDRY_RUNが通り、59ファイル127MBの収録一覧が完全に一致した。 鮮度をチェックするスクリプトがGNUの date を前提にしていた箇所も、BSDの date で通るよう直した。

残りは本番で走らせてから出てきた。 同じ3.2でもう1つ引っかかる。 空の配列を "${arr[@]}" で展開すると、set -u が未定義として弾く。 こちらは env を挟む形に変えた。

パスフレーズが渡らない

暗号化のところでも止まった。

gpg 2.x は --passphrase-fd でパスフレーズを渡すとき、--pinentry-mode loopback を併記しないと受け取らない。 Windows側ではagentの設定で通っていたらしく、この指定が無いまま動いていた。 両方のOSで有効な形に直させた。

テストコマンドが自分のガードに弾かれた

削除ガードのフックも、Windows版を壊さないようOS判定で分岐させた。

判定が正しく効くかどうかは、実際にJSONを流して確かめさせた。 許可すべきものと拒否すべきものを9通り流して、すべて期待どおりだった。

設定ファイルは書き換える前に退避を取らせた。 フック2件を両OS対応に書き換え、失敗したときに止まる性質はそのまま残した。 分類器に渡す禁止文言にはこちらのマシンのパス基準を足し、Windows側の記述は消していない。

書き換えたあとで動作確認のコマンドを流したら、そのコマンド自体が拒否された。 文字列に ~/.claude の削除が含まれていたからだった。 確認したかったことが、確認の失敗という形で返ってきた。

復元できて初めてバックアップになる

本番で走らせた。

127MBがクラウドへ上がり、サイズが一致し、目印も前に進んだ。 保存先の最新が8月1日のままだったのが、この日のものに入れ替わった。 8月1日は、Windows側で最後に走った日だ。

ここで止めれば「バックアップが動いた」で終わる。 戻せるかどうかは別の話なので、復元テストも移植させた。 クラウドから実際に落として復号し、展開した。 60ファイル全部のSHA-256を突き合わせたら、全件一致した。

日次の自動実行はlaunchdに登録した。 懸念はここだった。 バックアップはパスワードマネージャからパスフレーズを取り、クラウドへの送出にも認証が要る。 どちらも普段はターミナルから叩いていて、launchdが渡す環境はそれとは違う。

実際に起動させたら exit 0 で通り、パスフレーズの取得もアップロードも抜けた。

復元テストのほうは10日周期で登録した。 セッション履歴の自動削除は無効にしてあり、6月19日分がまだ残っている。 消える心配は当面ないが、壊れていることに早く気づける周期のほうがいい。

消し忘れた登録

設定ファイルを開いていて、セッション開始時に走るスクリプトの登録が目に入った。

拡張子が .ps1 で、パスがWindowsのものだった。 Macでしか使わないのに、なぜPowerShellなのか分からなかった。

タイムスタンプが答えだった。 .ps1 は8月2日の08:39、同じ処理のshell版は09:15。 移行の初日にPowerShell版を先に書いて登録し、36分後にshell版へ作り直している。 そのとき、設定ファイルから古いほうの登録を消していなかった。

残骸なので消させた。 設定に名前だけが残って実体が無いのは、削除ガードと同じ形だ。

許可リストにも同じものが溜まっていた。 Windows固有のエントリが22件。 全部消すとノートPC側で許可が外れるので、危ないものだけを3件外して、macOS用を足した。

あるはずのコマンドが見つからない

同じ朝、支援先の案件を扱うリポジトリで作業していて、日記コマンドが候補に出てこなかった。

ユーザーレベルに置いてあるつもりでいたので、消えたのかと思って調べさせた。

消えていなかった。 そのコマンドはユーザーレベルではなく、このサイトのリポジトリのプロジェクトレベルに置いてある。 別のリポジトリで作業していたのだから、候補に出る対象ではない。

先月末に環境を丸ごと失っている。 だから見つからないものを見ると、まず消えたと疑う。 今回は、置き場所を間違えて覚えていただけだった。

日記コマンド自体にも手を入れた。 月次のデータベースバックアップを回すステップがWindows専用の資材に依存していて、Macでは実行できない。 その日は手で飛ばしたが、手順定義に「macOSではスキップ」と書かせた。

ステータスラインの数字が空だった

ステータスラインに、5時間と1週間、それに上位モデルの利用制限のパーセンテージを出している。

Macに移してから、この3つが出ていなかった。 WindowsとMacの互換性だろうと見当をつけて、調査と修正を頼んだ。

そのとおりだった。 違っていたのは認証情報の置き場所で、MacではOAuthのトークンがKeychainに入る。 スクリプトはWindowsのファイルだけを見ていた。 だから取得が丸ごと失敗して、3つとも空欄になっていた。

Keychainから無人で取り出せることを、中身は出さずキー名だけ見て確認させた。 そのうえでKeychain経由の取得を足した。 実際に走らせて、3つのパーセンテージが並ぶところまで見た。

移行で壊れたものは、壊れたと言ってこない

この日直した3つは、症状の出方が違うだけで形は同じだった。

削除ガードはスタックトレースを1行出した。 セッション開始のスクリプトは何も出さず、設定ファイルに名前だけ残した。 ステータスラインは、数字のあった場所が空欄になった。

質が悪いのは、エラーを出さないほうだ。 バックアップが動いていることは、送出のログを見れば分かる。 ガードが効いていないことは、消されるまで分からない。

だから復元テストを周期で回すことにしたし、フックは9通りのJSONを流して確かめた。 「設定に書いてある」と「動いている」は別のことだと、移行のたびに教わっている。