服务调研关于实验室联系
← 返回商業調查
2026-11-01AI實戰·

一台工作 Mac 的災備方案備份鏈設計、推送閘門與五輪獨立審查

這台 Mac 上有記憶與規則檔案、幾十個程式碼儲存庫、二十多個定時任務、網站原始碼、路由器設定和各類 API token。開發者會面臨兩個風險:

  1. 如果 Claude Code、Codex 這類訂閱制 AI 帳戶被封,工作還能接上多少?
  2. 如果這台 Mac 壞了或丟了,還能找回多少?

前提是 Apple ID、iCloud、信箱和手機號碼都還在。

第一個問題答案很短:記憶和規則都是本機純文字,Claude 與 Codex 兩套設定早已並行維護,換一個 AI 當天就能接著幹。本文討論的是第二個問題。

本文速覽 · 摘要 · 定義 · FAQ

TL;DR

  • 審計結果:71 個儲存庫中 32 個沒有遠端,145 個提交未推送,Time Machine 未設定,環境設定和憑證沒有任何異地副本。
  • 備份範圍按資訊密度劃定:程式碼、規則、演算法、壓縮過的知識庫上雲;影片成品、AI 對話原始紀錄、可重新下載的行情資料只留本機。
  • 實現是一條每天 13:00–14:00 跑完的 launchd 備份鏈,不加常駐行程。新增兩個任務:git-sweep(每日掃描全部儲存庫,只推不提交)和 secrets-vault(每週把憑證打成 age 加密包傳到私有儲存庫的 Release)。
  • 所有自動推送共用一個閘門函式庫:push URL 白名單、現查遠端、逐提交掃路徑、掃物件體積、推送固定 OID,任何一步無法判定就當失敗(fail-closed)。
  • 程式碼由 Claude Code 撰寫,由 OpenAI Codex 唯讀審查五輪,阻斷級問題 10 → 5 → 1 → 1 → 0。
  • 恢復演練:下載加密包、校驗 SHA-256、解密清點 745 個條目全部一致,錯誤金鑰無法解開。高密度資產估計約 90% 可自動復原。

Definition

按資訊密度備份:判斷一份資料是否進入異地備份,只看一個問題——丟了之後能不能重新產生(重新下載、重新渲染、重新跑一遍)。不能重新產生的高密度內容(程式碼、規則、方法論、作品)每天上雲;能重新產生的大容量資料只留本機。它和「全碟鏡像」是兩種取捨:前者用很小的儲存空間和頻寬保住不可再生的部分,後者保住一切但成本和洩露面都更大。

FAQ

為什麼不直接開 Time Machine?

Time Machine 解決的是「同一地點的硬體故障」,Mac 遺失或連同外接硬碟一起損毀時幫不上忙。它可以作為補充,但不能取代異地備份。本方案落實時 Time Machine 仍未設定,見「已知局限」。

憑證為什麼不直接放進私有 git 儲存庫?

私有儲存庫的存取控制取決於 GitHub 帳戶和所有擁有存取權的 token。明文憑證一旦進入 git 歷史,就很難徹底清除。加密後再上傳,洩露 GitHub 側只會洩露密文。

為什麼要讓另一家的模型來審查?

同一個模型審查自己寫的程式碼,容易帶著同樣的假設。換一家模型、只給唯讀權限,更容易發現作者自測時沒想到的邊界情況。本次第一輪就判定「不通過」,列出 17 條問題。

這套方案適合多人團隊嗎?

不直接適合。它的威脅模型是「單人 Mac,防疏忽導致明文上雲、防備份靜默失敗」,不防已經控制本機的攻擊者,也沒有處理多人權限。閘門和加密包的思路可以遷移,具體腳本需要重新設計。

References and Further Reading

  • Filippo Valsorda,age 檔案加密工具與格式規範,github.com/FiloSottile/age、age-encryption.org/v1。
  • Betsy Beyer 等,Site Reliability Engineering,O'Reilly,2016,第 26 章 Data Integrity(備份的價值在於能恢復)。
  • US-CERT,Data Backup Options,2012(3-2-1 備份原則)。
  • Git 文件:git-log(-m、--root、-z)、git-ls-remote、git-rev-list --objects、git-cat-file --batch-check。
  • flock(2) 手冊頁。

01 · 現狀審計

審計的原則是實測,不信記憶。記憶檔案裡寫著「每天自動備份到 GitHub」,這只能說明有備份,說明不了備份涵蓋了多少。所以需要逐項掃描:每個儲存庫有沒有遠端、有多少提交沒推、Time Machine 狀態、iCloud 同步範圍、每個備份腳本實際做了什麼。

項目結果
沒有遠端的儲存庫71 個中 32 個,包括備份腳本自身所在的儲存庫
已提交未推送145 個提交,其中一個核心演算法儲存庫積壓 3 個月
Time Machine未設定
環境設定(SSH 主機表、自建腳本、定時任務、軟體清單)無異地副本
憑證(SSH 私鑰、API token、路由器設定)無異地副本

按資產性質可以把這台 Mac 分成四層:

四層資產與恢復度

記憶與規則層本來就有每日推送;問題集中在程式碼、環境和憑證三層。

審計報告本身也出過錯:初版寫「磁碟使用率 13%」,複查發現那是 APFS 唯讀系統卷宗的數字,資料卷宗實際已用 80%。此後報告裡的每個數字都要求能對應到一條命令輸出。

02 · 備份範圍:按資訊密度劃分

初版建議是把 AI 對話原始紀錄和影片專案的積壓提交都推上雲。複核後改為按資訊密度劃分:

上雲准入判斷

按這條規則重新計算:

這一步決定了後面所有腳本的邊界。如果先寫工具再定範圍,自動化會把不該上雲的東西也忠實地推上去。

03 · 架構:一小時內跑完的備份鏈

設計約束只有兩條:不增加常駐行程;盡量掛在已有的備份時間窗口上。

每日備份時間線

時間任務說明
13:00知識庫已有任務,提交並推送筆記與記憶
13:05設定鏡像改造:先在私有暫存目錄產生三份鏡像並檢測金鑰,通過後再寫入知識庫(次日 13:00 隨之上雲)
13:10書稿儲存庫已有任務,22 連接埠不通時改走 443
13:20git-sweep新增:掃描全部儲存庫,推送已提交未推送的分支
14:00對話與 Wiki已有任務,原始紀錄留本機,提煉結果推送
週日 13:30secrets-vault新增:憑證加密包

每個任務都由 launchd 排程,外面套一層等待網路就緒的包裝腳本,結束代碼非 0 時在本機儀表板告警。

git-sweep 的規則是只推不提交:什麼時候提交仍由開發者和各專案自己的規則決定,腳本只負責不讓已經提交的內容長期留在本機。它只推遠端已存在的同名分支;新分支首次推送需要手動 push -u,腳本記日誌但不代勞。

為了上線,開發者另外新建了 8 個私有儲存庫,並把 15 個儲存庫的積壓提交一次推平。批次推送前先查了一遍 Cloudflare,確認沒有網站設定了「推送即自動部署」,否則積壓的幾十個未發布提交會直接上線。

04 · 推送閘門

自動推送最大的風險是推錯東西。所以知識庫、書稿、對話、設定、git-sweep 這幾個推送入口都 source 同一個閘門函式庫 lib-backup.sh,不允許各自實作。

推送閘門

外層是儲存庫級互斥鎖。鎖用核心 flock,掛在 shell 持有的檔案描述子上,行程結束時自動釋放,不會留下過時的鎖。macOS 沒有 flock(1) 命令,這裡借用系統內建的 Perl:

exec 8>>"$gitdir/1984-backup.lock"
/usr/bin/perl -e '
  use Fcntl ":flock";
  open(my $fh, ">&=", $ARGV[0]) or exit 2;
  for (1 .. $ARGV[1]) { exit 0 if flock($fh, LOCK_EX | LOCK_NB); sleep 1 }
  exit 1;
' 8 300

鎖內依序過五道檢查:

  1. push URL 白名單。git remote get-url --push --all origin 回傳的每一個位址都必須屬於自己的 GitHub 帳戶,第三方上游一律跳過。
  2. 現查遠端。用 git ls-remote 對每個實際 push URL 查分支 tip,不信本機的 origin/* 快取,也不用 fetch URL 代替 push URL。遠端有本機沒有的提交、或已分叉,就跳過並記日誌。
  3. 逐提交掃路徑。只看最終 diff 會漏掉「中間某次提交加入、之後又刪除」的大檔案。這裡用 git log -z -m --root --no-renames --name-only 列出待推範圍內每個提交觸及的路徑,合併提交對每個父提交分別展開,根提交也包含在內。影音副檔名、對話原始紀錄目錄命中即拒推。
  4. 掃物件體積。git rev-list --objects --not 列出待推物件,再用 git cat-file --batch-check 取體積,任何 blob 超過 5MB 即拒推(GitHub 的硬上限是 100MB)。
  5. 推送固定 OID。檢查的是某個提交 ID,推送的也是這個 ID:git push :refs/heads/。檢查之後才出現的新提交不會被一起帶上去。

閘門函式的回傳碼有三種:0 通過,1 策略拒推,2 無法判定。2 按失敗處理。

fail-closed 是這套設計裡最重要的約定。 備份系統最危險的狀態不是失敗,而是失敗了卻回報成功。dry-run 時就遇到過一次:腳本裡 $branch 後面緊跟一個全形括號,macOS 內建的 bash 3.2 把它當成變數名稱的一部分,set -u 判定變數未定義,子 shell 結束,整個掃描根被跳過,而彙總行依然回報一切正常。修復分兩層:變數一律寫成 ${branch};每個掃描根跑完必須輸出一行 root_done,彙總時核對數量,少一個就計為失敗。

05 · 憑證:age 加密包

SSH 私鑰、API token、路由器設定不能以明文進 git,但 Mac 遺失後又必須能找回。這裡用 age 做非對稱加密。

age 加密模型

只拿到 GitHub 側只有密文;只拿到 iCloud 側只有金鑰沒有資料。兩邊同時可用,正好對應「Apple ID 和信箱還在」這個前提。

secrets-vault.sh 的流程:

tar -czf - --no-xattrs --no-mac-metadata -T "$LIST" | age -R "$PUB" -o "$OUT"
# 本機往返校驗:能解密、能列目錄、條目數 > 0;沒有私鑰就無法證明可恢復 → 失敗
age -d -i "$KEY" "$OUT" | tar -tzf - | wc -l
gh release upload "$TAG" "$OUT" "$OUT.sha256"
# 遠端體積與本機一致之後,才刪除除最新兩份以外的舊資產

幾個細節:

06 · 設定鏡像的金鑰檢測

環境設定(SSH 主機表、~/.local/bin 下的腳本、LaunchAgent plist、Claude Code 設定、Brewfile、pipx 清單)需要以明文進入知識庫,這樣新機器上讀一遍就能重建環境。難點是其中可能夾著金鑰。

最終結構是:

  1. 三份鏡像全部先在知識庫之外的私有暫存目錄(權限 700)中組裝。
  2. 結構化匯出:settings.json 用 jq 按欄位白名單匯出;plist 用 plutil 改值,EnvironmentVariables 中敏感鍵的值改為 REDACTED,結果仍是合法 XML。
  3. 能以自由格式藏金鑰的檔案(shell 啟動檔案、Codex 設定、secrets.env)不進入明文層,原件只進加密包。
  4. 對暫存目錄逐檔案檢測:已知 token 前綴、PEM 私鑰標頭、長 base64 行、敏感鍵賦值、命令列密碼參數、plist 參數對、TOML 多行字串。JSON 檔案先用 jq -r '.. | strings' 把字串值解碼成純文字再檢測,否則 \" 跳脫會讓正規表示式失效。
  5. 任一檔案可疑,整批不發布,知識庫保留上一版;檢測日誌只記錄檔案相對路徑和命中類型,不印出內容。

第 3 步是第二輪審查之後才加的,詳見下一節。

07 · 獨立審查:五輪紀錄

程式碼寫完後,開發者用 Codex(唯讀沙盒)通讀全部備份腳本做驗收。審查提示從第 4 輪起寫明了威脅模型:單人 Mac 的個人備份,防的是疏忽導致明文上雲和備份靜默失敗,不防已經控制本機的攻擊者。沒有這條邊界,審查會無限加碼。

五輪審查

輪次主要問題修復
117 條,其中 CRITICAL 2、HIGH 8。最嚴重的是設定鏡像「先寫入知識庫、再檢測金鑰」,檢測發現問題時明文已在每日推送的目錄裡;mkdir 鎖可能殘留;部分推送入口繞過閘門改為暫存區檢測、通過後再發布;抽出共用閘門函式庫,所有入口接入;推送固定 OID
2正規表示式脫敏總能被構造出新的繞過;本機快取的遠端位置不可信;合併提交未展開不再追加正規表示式,改為讓自由格式設定不進明文層;鎖換成 flock;ls-remote 現查;log -m --root
3命令列 --password 值 形式漏檢;帶引號的路徑可繞過禁推規則;上一輪新增的參數校驗與實際用法不符補命令列參數、plist 參數對、TOML 多行字串;路徑改用 log -z 按 NUL 分隔比對;修復迴歸
4JSON 中跳脫為 \"值\" 的密碼漏檢;ls-remote 查的是 fetch URL 而非 push URLJSON 先解碼再檢測;按實際 push URL 查詢,不在白名單計為拒推
5有條件通過:設定多個 push URL 時「本機落後」判斷會提前短路當場修復並模擬驗證

三點體會:

08 · 過程中的失誤

掃描時洩露了憑證。 掃描設定檔案中的金鑰時,命令把比對行輸出到終端機,指望後面接的 sed 打碼。但 macOS 的 BSD sed 不支援 \s,正規表示式靜默失效,4 個 token 以明文進入對話紀錄,只能逐一輪替。規則改為:掃描金鑰只輸出計數和檔名,任何情況下都不印出比對內容。

備份檔案差點放錯位置。 遷移一個寫死的 key 時,原檔案備份放在了原目錄,而那個目錄每天鏡像上雲。及時發現並移走。

新檢測器發現了舊問題。 改造後的檢測器第一次在真實資料上執行,就攔下了兩個半年前寫死在 AI 指令檔案裡的憑證。隨後對知識庫的全部 git 歷史做了一次掃描,又找到幾個已刪除但仍留在歷史中的舊憑證。處理方式:目前檔案中的遷入本機 secrets.env,原處改為引用環境變數;只在歷史中的透過輪替使其失效,不改寫歷史(需要強制推送,不可逆,且無法控制 GitHub 側快取)。

不可逆和外傳操作由人執行。 過程中 Claude Code 的安全分類器攔下了兩次操作:刪除遠端分支(不可逆)和上傳加密憑證包(資料外傳)。兩次都改由開發者手動執行。這個分工是對的。

09 · 恢復演練

備份能恢復才有意義。最後一步是模擬換機:從 GitHub 下載加密包,核對校驗和,解密並清點內容。

恢復流程

gh release download vault-latest -R <owner>/<vault-repo> -p 'secrets-vault-*'
shasum -a 256 -c secrets-vault-<stamp>.tar.gz.age.sha256
age -d -i vault.key secrets-vault-<stamp>.tar.gz.age | tar -tzf - | wc -l

結果:

實際耗時不到一分鐘。中間一次卡頓是演練裡一條列出檔案的命令在背景掛起,與備份系統無關。

10 · 已知局限

局限影響計畫
演練在原機完成,重用了已登入的 gh;「新機安裝 age、登入 GitHub、按清單重建環境」沒有在乾淨機器上走過第 4 步的耗時和缺口未知下次換機或用虛擬機補一次完整演練
新機下載私有 Release 需要登入 GitHub;如果 2FA 因子只存在於這台 Mac,恢復鏈會斷可能卡在第 2 步確認 2FA 復原碼與另一裝置上的驗證器
憑證包每週一次輪替 token 後最長 7 天包內是舊值輪替屬於人工操作,流程裡加一步:輪替完成後手動執行一次 secrets-vault.sh。不加檔案監聽觸發,避免誤觸發和額外告警
git-sweep 的「跳過」(新分支、本機落後、已分叉)只寫日誌,不影響結束代碼長期跳過的儲存庫不會告警儀表板顯示跳過數,持續多日即告警
規則與 skill 文件只做 token 形態檢測,不做「鍵=值」檢測(避免誤傷文件範例)非標準前綴的 token 寫進文件可能漏檢已知取捨;依靠「掃描只出計數」與定期全歷史掃描兜底
設定鏡像 13:05 產生,知識庫 13:00 推送設定變更約 24 小時後才上雲可接受;如需縮短,改為在 13:05 後補一次推送
Time Machine 仍未設定本機硬體故障時,大容量可再生資料需重新下載補一塊本機備份碟

另外,「約 90% 可自動復原」是按重要性加權的估計,不是測量值。

11 · 可重用的做法

七步工作流程

  1. 先實測再判斷。 記憶裡的「每天備份」不等於涵蓋完整;32 個無遠端儲存庫是掃出來的,不是想出來的。
  2. 先定範圍再寫工具。 一條資訊密度規則砍掉了約 496MB 的無效上雲。
  3. 報告裡的數字要能追溯到命令輸出。 「磁碟 13%」就是反例。
  4. 掛在已有時間窗口上,不加常駐行程。
  5. fail-closed。 無法判定就當失敗;每個掃描單元必須交回執。
  6. 縮小暴露面優於加強檢測。 敏感檔案不進明文層,比多寫十條正規表示式可靠。
  7. 作者和審查者用不同的模型。 唯讀權限,多輪,直到阻斷項歸零。
  8. 審查要有明確的威脅模型。
  9. 不可逆和外傳操作由人執行。
  10. 失誤寫成規則。 「掃描金鑰只輸出計數和檔名」現在是長期規則。

整個上午,AI 執行了幾百條命令、寫了約 500 行備份腳本、調度另一家模型做了五輪審查。真正改變方向的決定只有幾個:備份範圍按資訊密度劃定、哪些儲存庫值得新建遠端、哪些風險可以接受。實作和取證可以交給 AI,範圍和取捨需要開發者來定。

分享
← 返回商業調查列表

相關文章 · 長為試之

長為試之印(盖印版·自然崩口)
AI實戰2026-10-22
先立規矩,後寫程式碼 · 一套 A 股盤後監控系統的開發實錄與方法論
AI實戰2026-08-03
Codex、Claude Code、OpenCode 與中國 CLI 智慧體:AI 到底是如何操作電腦系統
AI實戰2026-10-02
從畫布到證據 · Running Lean 工具圖譜