服务调研关于实验室联系
← Back to Research
2026-11-01AI 实战·

Disaster Recovery for a Working MacBackup Chain Design, Push Gates, and Five Rounds of Independent Review

This Mac holds memory and rule files, dozens of code repositories, more than twenty scheduled jobs, site source code, router configuration, and an assortment of API tokens. Its developer faces two risks:

  1. If a subscription AI account such as Claude Code or Codex gets banned, how much of the work can carry on?
  2. If this Mac breaks or goes missing, how much can be recovered?

The premise is that the Apple ID, iCloud, email, and phone number all survive.

The answer to the first question is short: memory and rules are plain local text, and parallel Claude and Codex configurations have been maintained for a long time, so switching to another AI means picking up the same day. This essay is about the second question.

Quick overview · TL;DR · definition · FAQ

TL;DR

  • Audit findings: 32 of 71 repositories had no remote, 145 commits were unpushed, Time Machine was not configured, and environment configuration and credentials had no off-site copy at all.
  • Backup scope is drawn by information density: code, rules, algorithms, and the compressed knowledge base go to the cloud; finished video, raw AI session transcripts, and market data that can be re-downloaded stay local.
  • The implementation is a launchd backup chain that finishes between 13:00 and 14:00 each day, with no resident processes added. Two new jobs: git-sweep (scans every repository daily; pushes, never commits) and secrets-vault (weekly, packs credentials into an age-encrypted bundle and uploads it to a Release in a private repository).
  • Every automated push shares one gate library: a push-URL allowlist, a live remote lookup, a per-commit path scan, an object-size scan, and pushing a pinned OID. If any step cannot reach a verdict, it counts as failure (fail-closed).
  • The code was written by Claude Code and reviewed read-only by OpenAI Codex over five rounds; blocking issues went 10 → 5 → 1 → 1 → 0.
  • Restore drill: download the encrypted bundle, verify the SHA-256, decrypt and inventory — all 745 entries matched, and a wrong key could not open it. An estimated ~90% of high-density assets can be restored automatically.

Definition

Backing up by information density: whether a piece of data enters the off-site backup depends on a single question — if it were lost, could it be regenerated (re-downloaded, re-rendered, re-run)? High-density content that cannot be regenerated (code, rules, methodology, finished work) goes to the cloud daily; bulky data that can be regenerated stays local. This and the "full-disk image" are two different trade-offs: the former protects the irreplaceable portion with very little storage and bandwidth; the latter protects everything, at a higher cost and with a larger exposure surface.

FAQ

Why not just turn on Time Machine?

Time Machine addresses "hardware failure in the same place." It is no help when the Mac is lost, or destroyed together with its external drive. It can be a supplement, but not a substitute for off-site backup. When this scheme went live, Time Machine was still not configured; see "Known Limitations."

Why not put credentials straight into a private git repository?

A private repository's access control depends on the GitHub account and every token with access to it. Once plaintext credentials enter git history, they are hard to purge completely. Encrypt first, then upload, and a leak on the GitHub side leaks only ciphertext.

Why have another vendor's model do the review?

A model reviewing code it wrote itself tends to carry the same assumptions. Switching to a different vendor's model, with read-only access, makes it likelier to catch edge cases the author never thought of while self-testing. This time the first round returned "fail" with 17 issues listed.

Does this scheme suit a multi-person team?

Not directly. Its threat model is "a single-user Mac, guarding against plaintext reaching the cloud through carelessness and against silent backup failure." It does not defend against an attacker who already controls the machine, and it does not handle multi-user permissions. The ideas behind the gates and the encrypted bundle transfer; the specific scripts would need to be redesigned.

References and Further Reading

  • Filippo Valsorda, age file encryption tool and format specification, github.com/FiloSottile/age, age-encryption.org/v1.
  • Betsy Beyer et al., Site Reliability Engineering, O'Reilly, 2016, Chapter 26, Data Integrity (a backup's value lies in being restorable).
  • US-CERT, Data Backup Options, 2012 (the 3-2-1 backup rule).
  • Git documentation: git-log (-m, --root, -z), git-ls-remote, git-rev-list --objects, git-cat-file --batch-check.
  • The flock(2) man page.

01 · Auditing the Current State

The audit's principle was to measure, not trust memory. A memory file said "automatically backed up to GitHub every day," which proves only that a backup exists, not how much it covers. So everything had to be scanned item by item: whether each repository has a remote, how many commits are unpushed, Time Machine status, the scope of iCloud sync, and what each backup script actually does.

ItemResult
Repositories with no remote32 of 71, including the repository that holds the backup scripts themselves
Committed but unpushed145 commits; one core algorithm repository had a three-month backlog
Time MachineNot configured
Environment configuration (SSH host table, custom scripts, scheduled jobs, software inventory)No off-site copy
Credentials (SSH private keys, API tokens, router configuration)No off-site copy

By the nature of its assets, the Mac divides into four layers:

Four asset layers and recoverability

The memory-and-rules layer already had a daily push; the problems concentrated in the code, environment, and credentials layers.

The audit report itself got something wrong too: the first draft said "disk usage 13%," and a recheck found that was the figure for the APFS read-only system volume — the data volume was actually 80% full. From then on, every number in the report had to map to the output of a specific command.

02 · Backup Scope: Dividing by Information Density

The first recommendation was to push both the raw AI session transcripts and the video project's backlog of commits to the cloud. On review, this changed to a split by information density:

Cloud admission test

Recalculated under this rule:

This step set the boundaries for every script that followed. Write the tools first and define the scope afterward, and the automation will faithfully push to the cloud things that should never go there.

03 · Architecture: A Backup Chain That Finishes Within an Hour

There were only two design constraints: add no resident processes; hang as much as possible on existing backup time windows.

Daily backup timeline

TimeJobNotes
13:00Knowledge baseExisting job; commits and pushes notes and memory
13:05Config mirrorReworked: first generates three mirrors in a private staging directory and checks for secrets, and only after passing writes them into the knowledge base (which goes to the cloud with the next day's 13:00 run)
13:10Manuscript repositoryExisting job; falls back to port 443 when port 22 is unreachable
13:20git-sweepNew: scans all repositories and pushes committed-but-unpushed branches
14:00Sessions and WikiExisting job; raw transcripts stay local, distilled results are pushed
Sundays 13:30secrets-vaultNew: encrypted credential bundle

Every job is scheduled by launchd, wrapped in a script that waits for the network to be ready, and raises an alert on the local dashboard on any non-zero exit code.

The rule for git-sweep is push, never commit: when to commit is still decided by the developer and each project's own rules; the script only makes sure committed work doesn't sit locally for long. It pushes only to same-named branches that already exist on the remote; a new branch's first push needs a manual push -u, which the script logs but does not perform on the developer's behalf.

To go live, the developer also created 8 new private repositories and flushed the backlogged commits of 15 repositories in one pass. Before the bulk push, Cloudflare was checked to confirm no site was configured for "deploy on push" — otherwise dozens of backlogged, unreleased commits would have gone straight to production.

04 · Push Gates

The biggest risk of automated pushing is pushing the wrong thing. So the push entry points — knowledge base, manuscripts, sessions, config, and git-sweep — all source the same gate library, lib-backup.sh; none may roll its own.

Push gates

The outer layer is a per-repository mutex. The lock uses the kernel's flock, attached to a file descriptor held by the shell, so it is released automatically when the process exits and never leaves a stale lock behind. macOS has no flock(1) command, so the system's bundled Perl is borrowed:

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

Inside the lock, five checks run in order:

  1. Push-URL allowlist. Every address returned by git remote get-url --push --all origin must belong to the developer's own GitHub account; third-party upstreams are always skipped.
  2. Live remote lookup. git ls-remote queries the branch tip at each actual push URL, rather than trusting the local origin/* cache or substituting the fetch URL for the push URL. If the remote has commits the local side lacks, or the two have diverged, the repository is skipped and logged.
  3. Per-commit path scan. Looking only at the final diff misses a large file "added in some intermediate commit and later deleted." Here git log -z -m --root --no-renames --name-only lists the paths touched by every commit in the range to be pushed; merge commits are expanded against each parent separately, and root commits are included. Any hit on an audio/video extension or the raw session-transcript directory blocks the push.
  4. Object-size scan. git rev-list --objects --not lists the objects to be pushed, then git cat-file --batch-check retrieves their sizes; any blob over 5MB blocks the push (GitHub's hard limit is 100MB).
  5. Push a pinned OID. What was checked is a specific commit ID, and what gets pushed is that same ID: git push :refs/heads/. New commits that appear after the check are not carried along.

The gate functions have three return codes: 0 pass, 1 policy rejection, 2 undecidable. 2 is treated as failure.

Fail-closed is the single most important convention in this design. The most dangerous state for a backup system is not failure, but failure reported as success. A dry run hit exactly this once: in the script, $branch was immediately followed by a full-width parenthesis; the bash 3.2 bundled with macOS treated it as part of the variable name, set -u declared the variable undefined, the subshell exited, an entire scan root was skipped — and the summary line still reported everything as fine. The fix had two layers: variables are always written as ${branch}; and every scan root must emit a root_done line on completion, with the summary reconciling the count — one missing counts as a failure.

05 · Credentials: The age-Encrypted Bundle

SSH private keys, API tokens, and router configuration cannot enter git in plaintext, yet must be recoverable after the Mac is lost. Asymmetric encryption with age handles this.

The age encryption model

Obtain only the GitHub side and you have only ciphertext; obtain only the iCloud side and you have the key but no data. Both being available together corresponds exactly to the premise that "the Apple ID and email still exist."

The flow of secrets-vault.sh:

tar -czf - --no-xattrs --no-mac-metadata -T "$LIST" | age -R "$PUB" -o "$OUT"
# Local round-trip check: must decrypt, must list contents, entry count > 0; without the private key recoverability cannot be proven → failure
age -d -i "$KEY" "$OUT" | tar -tzf - | wc -l
gh release upload "$TAG" "$OUT" "$OUT.sha256"
# Only after the remote size matches the local one, delete old assets other than the latest two

A few details:

06 · Secret Detection for the Config Mirror

Environment configuration (the SSH host table, scripts under ~/.local/bin, LaunchAgent plists, Claude Code settings, the Brewfile, the pipx list) needs to enter the knowledge base in plaintext, so a new machine can rebuild the environment just by reading it. The difficulty is that secrets may be tucked inside.

The final structure:

  1. All three mirrors are first assembled in a private staging directory (permissions 700) outside the knowledge base.
  2. Structured export: settings.json is exported field by field against an allowlist using jq; plists are modified with plutil, setting the values of sensitive keys in EnvironmentVariables to REDACTED, and the result remains valid XML.
  3. Files that can hide secrets in free-form text (shell startup files, Codex config, secrets.env) do not enter the plaintext layer; the originals go only into the encrypted bundle.
  4. The staging directory is checked file by file: known token prefixes, PEM private-key headers, long base64 lines, assignments to sensitive keys, command-line password arguments, plist argument pairs, TOML multi-line strings. JSON files are first decoded to plain text with jq -r '.. | strings' before checking — otherwise \" escaping defeats the regexes.
  5. If any file is suspect, the whole batch is withheld and the knowledge base keeps the previous version; the detection log records only the file's relative path and the type of hit, never its content.

Step 3 was added only after the second round of review; see the next section.

07 · Independent Review: A Record of Five Rounds

Once the code was written, the developer had Codex (in a read-only sandbox) read through every backup script as acceptance review. From round 4 on, the review prompt spelled out the threat model: personal backup on a single-user Mac, guarding against plaintext reaching the cloud through carelessness and against silent backup failure — not against an attacker who already controls the machine. Without that boundary, a review escalates without end.

Five review rounds

RoundMain issuesFix
117 issues, including 2 CRITICAL and 8 HIGH. The most serious: the config mirror "wrote into the knowledge base first, then checked for secrets," so by the time a problem was detected the plaintext was already in a directory pushed daily; the mkdir lock could be left behind; some push entry points bypassed the gatesSwitched to checking in staging and publishing only on pass; extracted a shared gate library and wired in every entry point; push a pinned OID
2Regex redaction can always be bypassed by some newly constructed input; the locally cached remote position is untrustworthy; merge commits were not expandedStopped adding regexes and instead kept free-form config out of the plaintext layer; switched the lock to flock; live ls-remote lookup; log -m --root
3Command-line --password value forms went undetected; quoted paths could slip past the push-blocking rules; the argument validation added in the previous round didn't match actual usageAdded command-line arguments, plist argument pairs, and TOML multi-line strings; paths now matched NUL-separated via log -z; fixed the regression
4Passwords escaped as \"value\" in JSON went undetected; ls-remote queried the fetch URL rather than the push URLDecode JSON before checking; query the actual push URL, and count anything not on the allowlist as a rejection
5Conditional pass: with multiple push URLs configured, the "local is behind" check short-circuited too earlyFixed on the spot and verified by simulation

Three takeaways:

08 · Mistakes Along the Way

Credentials leaked during a scan. While scanning config files for secrets, a command printed the matching lines to the terminal, counting on a downstream sed to mask them. But macOS's BSD sed doesn't support \s; the regex silently failed, 4 tokens entered the session transcript in plaintext, and each had to be rotated. The rule became: a secret scan outputs only counts and filenames, and never, under any circumstances, prints matched content.

A backup file nearly landed in the wrong place. While migrating a hardcoded key, the backup of the original file was placed in the original directory — a directory mirrored to the cloud every day. It was caught in time and moved.

The new detector found old problems. The first time the reworked detector ran on real data, it stopped two credentials that had been hardcoded in an AI instruction file half a year earlier. A subsequent scan of the knowledge base's entire git history turned up several old credentials that had been deleted but still lived in history. The handling: those in current files were moved into a local secrets.env, with the original locations changed to reference environment variables; those only in history were invalidated by rotation, without rewriting history (that would require a force push, is irreversible, and can't control caching on the GitHub side).

Irreversible and outbound operations are performed by a human. During the process, Claude Code's safety classifier stopped two operations: deleting a remote branch (irreversible) and uploading the encrypted credential bundle (data leaving the machine). Both were performed manually by the developer instead. This division of labor is right.

09 · Restore Drill

A backup is only meaningful if it can be restored. The final step simulated a machine swap: download the encrypted bundle from GitHub, verify the checksum, decrypt, and inventory the contents.

Restore flow

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

Results:

Actual time taken was under a minute. The one stall was a file-listing command in the drill hanging in the background, unrelated to the backup system.

10 · Known Limitations

LimitationImpactPlan
The drill ran on the original machine, reusing an already-logged-in gh; "install age on a new machine, log in to GitHub, rebuild the environment from the inventory" has never been walked through on a clean machineTime and gaps for step 4 are unknownRun a complete drill at the next machine swap, or in a virtual machine
Downloading a private Release on a new machine requires logging in to GitHub; if the 2FA factor exists only on this Mac, the recovery chain breaksCould stall at step 2Confirm 2FA recovery codes and an authenticator on another device
The credential bundle runs weeklyAfter a token rotation, the bundle holds the old value for up to 7 daysRotation is a manual operation, so add a step to that process: run secrets-vault.sh manually once rotation is done. No file-watch trigger, to avoid spurious runs and extra alerts
git-sweep's "skips" (new branch, local behind, diverged) are only logged and don't affect the exit codeA repository skipped for a long time raises no alertShow the skip count on the dashboard; alert if it persists for several days
Rule and skill documents get only token-shape detection, not "key=value" detection (to avoid flagging documentation examples)A token with a non-standard prefix written into a document could slip throughA known trade-off; backstopped by "scans output only counts" and periodic full-history scans
The config mirror is generated at 13:05, the knowledge base pushes at 13:00Config changes reach the cloud only about 24 hours laterAcceptable; to shorten it, add an extra push after 13:05
Time Machine is still not configuredOn local hardware failure, bulky regenerable data must be re-downloadedAdd a local backup drive

Also: "about 90% automatically restorable" is an importance-weighted estimate, not a measured value.

11 · Reusable Practices

Seven-step workflow

  1. Measure before you judge. "Backed up daily" in memory doesn't mean complete coverage; the 32 repositories without remotes were found by scanning, not by thinking.
  2. Define scope before writing tools. A single information-density rule cut about 496MB of pointless cloud upload.
  3. Every number in a report must trace back to command output. "Disk at 13%" is the counterexample.
  4. Hang on existing time windows; add no resident processes.
  5. Fail closed. If it can't be decided, it's a failure; every scan unit must return a receipt.
  6. Shrinking the exposure surface beats strengthening detection. Keeping sensitive files out of the plaintext layer is more reliable than writing ten more regexes.
  7. Author and reviewer should be different models. Read-only access, multiple rounds, until blocking issues reach zero.
  8. Review needs an explicit threat model.
  9. Irreversible and outbound operations are performed by a human.
  10. Turn mistakes into rules. "Secret scans output only counts and filenames" is now a standing rule.

Over the course of one morning, the AI executed several hundred commands, wrote about 500 lines of backup scripts, and orchestrated five rounds of review by another vendor's model. The decisions that actually changed direction were few: drawing the backup scope by information density, deciding which repositories deserved a new remote, deciding which risks were acceptable. Implementation and evidence-gathering can be handed to AI; scope and trade-offs are for the developer to decide.

Share
← Back to Research

Related · TryWay Labs

長為試之印(盖印版·自然崩口)
AI 实战2026-10-22
Rules Before Code · A Field Record of Building an After-Hours A-Share Monitoring System, and the Method Behind It
AI 实战2026-08-03
Codex, Claude Code, OpenCode, and China's CLI Agents: How AI Actually Operates a Computer System
AI 实战2026-10-02
From Canvas to Evidence · A Running Lean Tool Atlas