フォルダがひとつ、消えていました。AIエージェントに手伝ってもらいながらプラグインのバグを直していて、作業が終わってから気づいた。そのときの顛末は別に書きましたが、助かったのはNAS宛のTime Machineで、6時間前のバックアップから戻せました。
戻せたので、話はそこで終わってもよかった。ただ、あのとき効いたのは「Mac全体を面で守る仕組み」であって、消えたのは作業ディレクトリの中の一つのフォルダです。守備範囲が広すぎる網に、たまたま引っかかった。次も同じ運の良さに頼るのは気が進みませんでした。
それで、作業ディレクトリだけを狙う網をもう一枚張ることにしました。使ったのは restic です。
設計:何を、どこへ、どれくらいの間隔で
道具を選ぶ前に、条件を3つ決めました。
守る対象は、プラグインの作業ディレクトリだけ。写真も書類も要りません。Mac全体はTime Machineが見ているので、こちらは狭く深く。次に保存先は、Macとは物理的に別の場所。同じディスクの別ボリュームでは、ディスクごと失ったときに一緒に消えます。最後に間隔は、Time Machineより短く。前回6時間戻ったのは運が良かっただけで、あれが1日おきなら失うものはもっと多かった。
| Time Machine | restic | |
|---|---|---|
| 対象 | Mac全体 | 作業ディレクトリだけ |
| 保存先 | NAS(SMB) | 別のNAS(SFTP) |
| 間隔 | 1時間ごと(既定) | 6時間ごと |
| 戻す単位 | ボリューム・ファイル | スナップショット・ファイル |
| 役割 | 面で守る汎用の網 | 一点を狙う専用の網 |
restic を選んだ理由は、リポジトリ形式で世代を持てることと、保存先を選ばないことです。ローカルのディスクでも、SFTPの先でも、クラウドのストレージでも、同じコマンドで扱える。今回はNASへSFTPで置く構成にしました。
手順1:NAS側でSFTPを通す(ここで一度詰まった)
先にSSHを有効にしました。DSMのターミナル設定でSSHをオンにして、Macから ssh で入れることを確認。ここまでは素直に進みます。
そのままresticを走らせたら、繋がりませんでした。
原因はDSM側の設定でした。SynologyではSSHとSFTPが別々の場所で管理されていて、SSHを有効にしただけでは、SFTPは通りません。ファイルサービスの設定にSFTPの項目があり、そちらも有効にする必要がある。同じ鍵で同じホストに入れるのだから通るはずだ、と思い込んでいた分、原因にたどり着くまで遠回りしました。

確かめたのは DSM 7.3.2-86009 Update 4 です。SSHは「コントロールパネル > 端末とSNMP > 端末」、SFTPは「コントロールパネル > ファイルサービス > FTP」。同じポート22を使うのに、有効化するチェックボックスが別の画面にあります。DSMのバージョンによって配置が変わることがあるので、見つからないときは検索窓に SFTP と打つのが早いです。
鍵の設置は普通の手順です。専用の鍵を作って、ssh-copy-id でNASへ登録しました。パスワードを毎回聞かれる状態では、定期実行に載せられません。
| 1 2 | ssh-keygen -t ed25519 -f ~/.ssh/nas02 ssh-copy-id -i ~/.ssh/nas02.pub ユーザー名@NAS02.local |
手順2:リポジトリの置き場所でもう一度詰まった
SFTPが通ったので、バックアップ専用に作っておいた共有フォルダへリポジトリを作ろうとしました。これも失敗します。
SynologyのSFTPはchrootされていて、ログインした先から見えるルートが、共有フォルダの並ぶ階層になっています。Linuxのファイルシステムをそのまま辿る感覚でパスを書くと、そこには無い。しかも用意した共有フォルダは、そのユーザーに書き込み権限がありませんでした。権限を直す手もありましたが、ホームディレクトリの下なら確実に書けるので、そちらへ置くことにしました。
| 1 | restic -r sftp:ユーザー名@NAS02.local:/home/restic/plugins-repo init |
パスワードは環境変数かファイルで渡します。リポジトリのパスワードを忘れると中身は二度と取り出せないので、ここだけはパスワードマネージャーへ確実に入れておきました。暗号化されているという安心と、鍵を失えば自分も開けられないという事実は、同じことの表と裏です。
手順3:初回バックアップ
対象を指定して走らせます。
| 1 | restic -r sftp:ユーザー名@NAS02.local:/home/restic/plugins-repo backup /path/to/plugins |
初回は12,516ファイル、211MiB。転送量としては軽いほうで、思っていたより早く終わりました。ソースコードとドキュメントが主で、大きなバイナリを含まないディレクトリなので、この程度で収まります。
二回目以降は差分だけになります。走査こそ全ファイルに対して走りますが、変わっていなければリポジトリへ足されるのはごくわずか。実際の実行結果がこれです。

この軽さが、6時間ごとという間隔を選べた理由でもあります。重ければ、間隔を延ばすか対象を削るかの相談になっていました。

手順4:世代をどう残すか
取りっぱなしにすると、リポジトリは太り続けます。restic には古いスナップショットを間引く仕組みがあるので、方針を決めて渡しました。
日次を7、週次を4、月次を12、年次を3。直近1週間は日ごとに戻せて、1か月なら週単位、1年なら月単位、それより前は年単位。新しいものほど細かく、古いものほど粗くという考え方です。
この配分にしたのは、自分の失敗の形を思い出したからでした。前回気づいたのは作業が終わったあとで、消えてから数時間後です。ということは、細かく戻りたいのは直近だけ。半年前のドキュメントを1日単位で復元したい場面は、たぶん来ません。
手順5:launchdで6時間ごとに回す
macOSで定期実行するなら launchd です。~/Library/LaunchAgents/ に plist を置きました。ラベルは works.rapls.restic-backup。
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | { "Label" => "works.rapls.restic-backup" "ProgramArguments" => [ 0 => "/bin/bash" 1 => "/Users/[ユーザー名]/.local/bin/restic-backup.sh" ] "StartInterval" => 21600 "RunAtLoad" => true "EnvironmentVariables" => { "LANG" => "ja_JP.UTF-8" "PATH" => "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" } "LowPriorityIO" => true "Nice" => 5 "ProcessType" => "Background" "StandardOutPath" => "/Users/[ユーザー名]/Library/Logs/restic/launchd.out.log" "StandardErrorPath" => "/Users/[ユーザー名]/Library/Logs/restic/launchd.err.log" } |
ここで大事なのは、手で打つのをやめることそのものでした。前回の事故で分かったのは、自分が気づいたときにはもう遅い、ということです。気づいてから手動でバックアップを取るのでは間に合わない。だから間隔を決めて機械に任せる。
plistで意識して書いた箇所が3つあります。
ひとつめが PATH です。launchdから起動されるプロセスは、ターミナルで使っているシェルの設定を読みません。.zshrc も .zprofile も通らない。だから Homebrew で入れた restic の場所を自分で書いておかないと、コマンドが見つからずに失敗します。手元では動くのに定期実行だけ失敗する、という現象のほとんどはこれです。
ふたつめが RunAtLoad。StartInterval は「前回から6時間後」を数えるので、Macを閉じていた時間は進みません。ログインのたびに一度走らせておくと、しばらく開いていなかった日でも、使い始めた時点の状態が残ります。
みっつめが LowPriorityIO と Nice と ProcessType。作業中に裏で動く前提なので、ディスクとCPUの優先度を下げてあります。バックアップのせいでエディタが引っかかるようだと、そのうち止めたくなる。止めたくならない強さで動かすのは、前回書いたガードの話と同じ考え方です。
手順6:動いているかを確かめる
組んだあとに必要なのは、動き続けているかを見る手段です。
| 1 | launchctl list | grep restic |
返ってくる3列のうち、1列目がPID、2列目が前回の終了コードです。- 0 works.rapls.restic-backup なら、いまは走っていないが前回は成功した、という意味になります。ここが 0 以外なら、指定したログファイルを見にいきます。
ログの出力先も plist で決めてあります。~/Library/Logs/restic/ に標準出力とエラーを分けて残す設定です。定期実行はうまくいっているときほど存在を忘れるので、失敗したときに読む場所を先に決めておきました。
スクリプト側では、世代の整理と整合性の検証も間隔で制御しています。バックアップは6時間ごとでも、古いスナップショットの間引きと保存データの読み直しは毎回やる必要がない。実行ログに「世代整理はスキップ(前回から間隔未経過)」「整合性チェックはスキップ(前回から間隔未経過)」と出るのはそのためです。バックアップが取れているつもりで壊れている状態が一番こわいので、読み直しの仕組みだけは入れておきました。
組んでみて思ったこと
手順そのものは大したことをしていません。鍵を置いて、リポジトリを作って、定期実行に載せただけです。時間を食ったのは、SFTPが通らなかったところと、リポジトリを置けなかったところ。どちらもDSM側の設定で、restic の話ですらありませんでした。
そして、これで安全になったとも思っていません。網が2枚になっただけです。前回助かったのは汎用の網のほうで、専用の網は事故のあとに慌てて張りました。順番としては逆であってほしかった、というのが正直なところです。
もしあなたが、Gitにもクラウドにも置いていないディレクトリを手元に持っているなら、Time Machineの他にもう一枚あるかどうかを確かめてみてください。私は、消えてから確かめました。
検証環境:macOS 26.5.2 / restic 0.19.1 / Synology DSM 7.3.2-86009 Update 4 / launchd。2026年7月末に構築、8月1日時点の内容です。
関連: Claude Codeにフォルダを消された。安全網は3枚あって、3枚とも素通りした(この記事の発端)/フォルダを消されたあと、settings.jsonをどう書き直したか(同じ事故の、防ぐ側の対策)

コメント