AIOS · Fleet Storage · 2026-09-11
Five (possibly seven) external drives, one machine with no working backup, and a pile of old Time Machine archives holding photos that may exist nowhere else. This is the order of operations, the gates, and the exact commands — written for macOS 15.7.1 before any drive is connected.
Measured on 2026-09-11. These three numbers are the only verified storage facts in this document — everything about the external drives is unknown until Phase 0.
The one urgent problem
M2's Time Machine destination Backup is registered, but tmutil latestbackup returns:
Error Domain=com.apple.backupd.ErrorDomain Code=18 "Failed to mount destination."
M2 currently has no backup at all. A registered destination that cannot mount looks like a backup in System Settings and is not one. Phase 6 Step 1 fixes this before any consolidation work begins — every hour spent sorting old drives first is an hour of uninsured exposure on 337 GiB.
Caveat, stated honestly: the drives were unplugged when this was measured, and Code 18 with the drive absent is expected. Re-test with the drive attached before concluding the hardware is bad.
| Path | Size | Disposition |
|---|---|---|
~/AI_OS | 42G | Canonical tree — back up. Move working sets and build artifacts only to scratch. |
~/Pictures | 18G | Back up. Never exclude. |
~/Downloads | 9.7G | Best single candidate to relocate to the scratch drive. |
~/Library/Caches | 6.9G | Exclude from Time Machine. Regenerable. |
~/Movies | 592M | Leave alone. |
No passwordless sudo on M2
sudo -n true fails. Every step marked [SUDO] must be typed by Sean in an interactive Terminal. An agent cannot run them. The affected commands are tmutil setdestination, tmutil removedestination, tmutil setquota, tmutil startbackup, mdutil, and every diskutil erase*.
The two lanes claiming the 8TB are a no-go. cutroom/DaVinci and
8tb-drive (Mini-as-DAS) do not get the 8TB. Sean will allocate a 2TB
drive to cover both of those instead.
Consequence: the 8TB is free for this plan. Phase 5 below stands as written
— 8TB carries M2-TimeMachine + Archive. M2’s Time Machine is
direct-attached, not network TM over SMB, so no overnight-first-backup penalty and
no dependency on Mini being reachable.
Still to pin down when the drives are in front of you. Which 2TB goes to cutroom + Mini-DAS. Worth noting: DaVinci scratch is the most speed-sensitive workload in the whole set, and the Crucial X8 is the fast drive — so pointing the X8 at cutroom satisfies “repurpose the Crucial for fast working use” and “give cutroom a 2TB” with one move. That only works if a spare 2TB exists to take M2’s Time Machine, which is the drive-count question in Phase 0.
Prior context, now superseded but kept for the record: the 8TB had been scoped
2026-07-11 as Mini-as-DAS (/Volumes/AIOS-DAS/{delivery-assets, clone-staging, fleet-backups,
archive}), carried by two task cards open 63 and 26 days. That design moves to the 2TB.
Mini is currently unreachable over ssh, which no longer blocks anything in this plan.
None of these were connected while this plan was written. Nothing below the "known" column was measured.
Ambiguity that must be resolved first
Sean said "we have two two-terabyte drives," and separately named a 2TB Crucial X8 (M2's backup) and a 2TB drive that backs up Nano. It is not established whether those two ARE the pair, or whether two more 2TB drives exist beyond them. The drive count is unconfirmed.
This is not a detail. It decides whether M2's backup can live on different physical hardware than the archive. Phase 0 resolves it; Phase 5 branches on the answer.
| # | Drive | Known | Model / interface | Health | Contents | Proposed role |
|---|---|---|---|---|---|---|
| 1 | 2TB Crucial X8 | Fast. Currently M2's Time Machine target — the one that fails to mount. | unknown — confirm in Phase 0 | unknown — confirm in Phase 1 | unknown — confirm in Phase 2 | Fast scratch / working. Unregister from Time Machine. |
| 2 | 2TB (Nano's) | Nano's backup drive. | unknown — confirm in Phase 0 | unknown — confirm in Phase 1 | Nano Time Machine | KEEP AS IS — do not touch |
| 3 | 8TB | The big one. | unknown — confirm in Phase 0 | unknown — confirm in Phase 1 | unknown — confirm in Phase 2 | Archive + (Reading A) M2 Time Machine |
| 4 | 250GB | Older / slower. Holds photos and old-device TM backups. | unknown — confirm in Phase 0 | unknown — confirm in Phase 1 | unknown — confirm in Phase 2 | Offsite mirror of the irreplaceable subset |
| 5 | 120GB | Older / slower. Holds photos and old-device TM backups. | unknown — confirm in Phase 0 | unknown — confirm in Phase 1 | unknown — confirm in Phase 2 | Bootable macOS installer, or retire |
| 6–7 | Possible spare 2TB ×2 | Existence unconfirmed. If real, the architecture improves substantially — see Reading B. | M2 Time Machine + archive mirror | |||
Sean's stated intent
Backups belong on the slower drives; the fast Crucial gets freed for working use. But the slower drives currently hold photos and valuable data — "most of those are Time Machine backups of older devices." So the value has to come off before anything gets repurposed. That sequencing constraint is what makes Phases 3 and 4 the heart of this plan.
Goal: one table describing every drive, written without a single byte written to any of them. Resolve the drive count. Confirm Mini.
System Settings → Privacy & Security → Full Disk Access → enable Terminal → quit and reopen Terminal. The quit-and-reopen is mandatory; the entitlement is read at process launch. Without it tmutil returns empty backup lists and find silently skips protected directories — which makes every count in Phase 4 wrong in a way that is hard to diagnose.
mkdir -p /Users/SeanVargas/AI_OS/comms/outputs/2026-09-11-drive-consolidation/inventory cd /Users/SeanVargas/AI_OS/comms/outputs/2026-09-11-drive-consolidation/inventory diskutil list > 00-baseline-disklist.txt cat 00-baseline-disklist.txt
/dev/disk0 is M2's internal SSD (500.3 GB, GUID partition scheme); disk1 and disk3 are its APFS containers. Anything appearing later marked (external, physical) is one of your drives.
diskutil list external physical
Read the IDENTIFIER column carefully. disk4 is a whole drive; disk4s2 is a volume on it. Erase commands aimed at the wrong one destroy the wrong thing. Plugging in two drives at once is the single most common way people erase the drive they meant to keep.
DRIVE=disk4 # <-- substitute
diskutil info -all > 01-${DRIVE}-info-all.txt
diskutil info /dev/${DRIVE} | tee 02-${DRIVE}-whole.txt
diskutil list /dev/${DRIVE} | tee 03-${DRIVE}-layout.txt
system_profiler SPUSBDataType > 04-usb.txt
system_profiler SPThunderboltDataType > 05-thunderbolt.txtSPUSBDataType gives the enclosure's real product name and the negotiated link speed (Up to 10 Gb/s vs Up to 480 Mb/s). That negotiated speed is how you tell fast from slow as currently cabled — a 10 Gb/s NVMe drive on a USB-2 cable reports 480 Mb/s and behaves like a slow drive. Try a different cable and port before concluding a drive is slow.
| Filesystem | How it looks in diskutil list | Where real free space lives |
|---|---|---|
| APFS | Apple_APFS Container diskN plus a nested APFS Container Scheme block with volumes sharing one pool | Container Free Space — not the per-volume number, which can read as the whole pool on every volume at once |
| HFS+ | TYPE column reads Apple_HFS; File System Personality: Journaled HFS+ | Volume Free Space / Volume Used Space |
| exFAT | TYPE reads Microsoft Basic Data; File System Personality: ExFAT | Volume Free Space |
exFAT cannot hold a Time Machine backup
No permissions, no hard links, no resource forks. It also mangles macOS metadata on copy. If an old drive is exFAT, treat its contents as already metadata-degraded and plan to move them onto APFS.
# APFS — the container pool is the truth diskutil info /dev/disk4s2 | grep -E "Container Total Space|Container Free Space" diskutil apfs list | sed -n '/disk4/,/^$/p' # HFS+ / exFAT — the volume numbers are the truth diskutil info /dev/disk4s2 | grep -E "Volume Total Space|Volume Free Space|Volume Used Space" # Cross-check anything mounted df -h | grep -v "^map"
For any drive that is old, suspect, or precious, mount it read-only so macOS cannot write journal replays, Spotlight indexes, or .DS_Store files onto it.
diskutil unmount /dev/disk4s2 diskutil mount readOnly nobrowse -mountPoint /Volumes/RO_disk4s2 /dev/disk4s2 # confirm it actually took diskutil info /dev/disk4s2 | grep -E "Volume Read-Only|Media Read-Only" # want: Volume Read-Only: Yes
A read-only mount is a software guard, not a hardware one. It stops accidents, not a determined sudo.
sudo mdutil -i off /Volumes/<DRIVE> # -X removes the existing index store, which WRITES to the drive. # Skip -X entirely on any read-only or suspect drive. sudo mdutil -X /Volumes/<DRIVE>
Indexing a large old drive is hours of sustained random reads on hardware you are trying to be gentle with.
tailscale status | grep -i mini
ping -c 3 100.72.22.100
ssh -o ConnectTimeout=8 seanvargas@100.72.22.100 'hostname; df -h /; tmutil destinationinfo'
# M2's own registered destinations, and their UUIDs (needed in Phase 6)
tmutil destinationinfo
tmutil destinationinfo -XIf Mini does not answer, write "Mini: unreachable as of 2026-09-11, state unknown" into the inventory and proceed. Mini is not on M2's critical path, but you cannot claim a fleet-wide backup posture without it.
Tape and a marker. Every later phase refers to drives by number. Confusing drive 1 with drive 2 during Phase 6 is how people erase a backup they meant to keep.
Verify Phase 0
/Volumes.unknown — confirm in Phase 0. Never guess a value to fill a field.STOP IF
Goal: know which drives can be trusted with a write workload, and which get exactly one careful read. These are old drives. Some of them may be failing right now.
diskutil info /dev/disk4 | grep "SMART Status"
| Value | Meaning | Action |
|---|---|---|
| Verified | Drive reports itself healthy | Proceed — but SMART misses a large fraction of real failures, so this is not a guarantee |
| Not Supported | The USB enclosure does not pass SMART through. Most bridge chips (JMicron, ASMedia, Realtek) do not relay it. | Normal. Not alarming. You simply have no SMART data — judge the drive by behavior instead. |
| Failing | The drive is telling you it is dying | Rescue-read only. Never write. Never reformat. Copy off now and retire it. |
Optional deeper look, if you want it: brew install smartmontools then smartctl -d sat -a /dev/disk4 — the -d sat flag often gets through a bridge that diskutil cannot. If smartctl also fails, that confirms the enclosure blocks SMART. Move on.
diskutil verifyVolume /dev/disk4s2
diskutil verifyDisk /dev/disk4 # APFS container levelDo not run repairVolume on a suspect drive yet
repairVolume writes to the disk. On a dying drive the repair pass can be the thing that kills it. Correct order: verify → if errors, copy the data off first → only then consider repair.
log stream --predicate 'eventMessage CONTAINS "I/O error" OR eventMessage CONTAINS "disk4"' --style compact
Red flags: repeated I/O error or media-error messages · clicking or a spin-up/spin-down cycle every few seconds · transfer rate collapsing from tens of MB/s to single-digit KB/s and staying there · the volume unmounting itself mid-copy.
dd if=/dev/rdisk4 of=/dev/null bs=1m count=2000 status=progressTriple-check of=/dev/null
A dd with of= pointed at a disk is the single most destructive typo available on this system. If you are at all unsure, skip this step — it is optional.
Reference throughput: healthy spinning USB drive 60–140 MB/s · SATA SSD over USB 3 350–450 MB/s · NVMe over 10 Gb/s USB 700–1000 MB/s. Anything sustained under ~20 MB/s on a modern drive means a USB-2 link or a sick drive.
The rule for this entire playbook
A suspect drive gets read ONCE, carefully, copying off. It never gets a long write workload.
Why, concretely: reformatting a drive and writing 400 GB to it is hours of sustained writes — the heaviest possible load applied to hardware you already distrust. If it dies at hour three you lose the drive and whatever you were writing. If that write was the consolidated archive, you lose the consolidation too. Corollary: a drive that survived a rescue read is not thereby proven healthy. Old drives that held your only copy of something are retirement candidates, not backup candidates.
Verify Phase 1
Every drive carries a trust level: TRUST (safe for a write workload) / READ-ONCE (rescue only) / RETIRE. At least one TRUST drive is large enough to receive everything — Phase 5 depends on it.
STOP IF
Failing → stop everything else, rescue-read that drive first, and give it no future role.verifyVolume reports errors on a drive holding data you need → do not repair. Copy off (Phase 3), verify (Phase 4), then repair or retire.Goal: a written classification per drive into four buckets. Still no copying.
| Bucket | What it is | Disposition |
|---|---|---|
| (a) TM backups of old devices | Machines you no longer run | High value, high extraction difficulty → Phase 3 |
| (b) Loose archives | Photos, video, documents, .photoslibrary bundles, project folders | Copy + dedup → Phase 3 |
| (c) Regenerable junk | .dmg/.pkg, OS installers, node_modules, caches | Discard — but list it before discarding |
| (d) Unknown | Anything you cannot classify in 30 seconds | Treat as (b). Unknown is not "junk." |
du lies on APFS — use it for shape only, never for proof
du under-reports when files are clones (clonefile — Finder duplicate, cp -c, many installers), because shared blocks get charged to one path or to none. It over-reports apparent size on sparse files. Hard links inside Time Machine backups break it in the other direction.
This exact trap cost real time on this fleet: 18 Chrome clone directories showed as 24.8 GiB in du, and deleting one released 18 MiB. Use du -h -d 2 to eyeball a drive's shape. Never use it to prove a copy is complete — that is Phase 4.
DRV="/Volumes/<DRIVE>" # <-- substitute ls -la "$DRV/Backups.backupdb/" ls -la "$DRV/Backups.backupdb/"*/ # structure: Backups.backupdb/<Machine>/<YYYY-MM-DD-HHMMSS>/<Volume>/... # dated dirs share storage via hard links — apparent sizes will far exceed # the drive's capacity. That is normal.
ls -la "$DRV"/*.sparsebundle find "$DRV" -maxdepth 3 \( -name "*.sparsebundle" -o -name "*.backupbundle" \) # A .sparsebundle is a DIRECTORY that looks like a file in Finder. # Inside: bands/ (thousands of numbered chunks), Info.plist, token. # Never copy bands individually. Mount it — see Phase 3.
diskutil apfs list /dev/disk4 # look for a volume whose role includes Backup tmutil listbackups -d "$DRV" diskutil apfs listSnapshots /dev/disk4s1 # APFS TM backups are snapshots on a dedicated volume — not folders # you can browse with ls. tmutil or the Time Machine UI only.
ls "$DRV/Backups.backupdb/" # HFS+: dir name = machine name
plutil -p "$DRV"/*.sparsebundle/com.apple.TimeMachine.MachineID.plist
tmutil listbackups -d "$DRV" | headfind "$DRV" -maxdepth 4 -name "*.photoslibrary" -print find "$DRV" -maxdepth 4 -name "*.photolibrary" -print # iPhoto find "$DRV" -maxdepth 4 -name "*.aplibrary" -print # Aperture find "$DRV" -maxdepth 4 -name "*.migratedphotolibrary" -print
Anything found here follows the Phase 3 Photos rules. Do not enter these bundles.
# stills + RAW find "$DRV" -type f \( -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.heic" \ -o -iname "*.png" -o -iname "*.dng" -o -iname "*.cr2" -o -iname "*.nef" \ -o -iname "*.arw" \) | wc -l # video find "$DRV" -type f \( -iname "*.mov" -o -iname "*.mp4" -o -iname "*.m4v" \ -o -iname "*.avi" -o -iname "*.mts" \) | wc -l # documents find "$DRV" -type f \( -iname "*.pdf" -o -iname "*.doc*" -o -iname "*.xls*" \ -o -iname "*.pages" -o -iname "*.numbers" -o -iname "*.key" \) | wc -l # the biggest individual files — usually where the surprises are find "$DRV" -type f -size +500M -exec ls -lh {} \; | awk '{print $5, $9}' | sort -hr | head -40
Triage rule: if re-acquiring it costs under ten minutes and zero dollars, it is bucket (c). If you would have to email someone to get it back, it is bucket (b).
Verify Phase 2
A CONTENTS-drive-<N>.md exists per drive. Every top-level directory on every drive is assigned to a bucket. Bucket (d) is written down explicitly rather than silently ignored.
STOP IF
find floods with permission-denied → you skipped Full Disk Access. Fix and re-run, or every Phase 4 count will be wrong.This is where the irreplaceable data comes off. Every operation here is a READ from the old drive and a WRITE to the 8TB staging area. Nothing writes back to a source.
STAGE="/Volumes/<8TB>/_STAGING" # <-- substitute
mkdir -p "$STAGE"/{tm-extract,loose,photoslibs,manifests,quarantine}.photoslibrary bundles, copied intactKeeping source-drive provenance in the folder names is what makes dedup and verification tractable. Do not merge everything flat yet.
$STAGE/tm-extract/<machine>/.No Time Machine menu-bar icon? System Settings → Control Center → Time Machine → Show in Menu Bar, or open /System/Applications/Time Machine.app directly.
When this route works, use it. Slower to click through, but it restores metadata, permissions, and resource forks correctly and it cannot corrupt the source.
tmutil route — for bulk extractionDRV="/Volumes/<DRIVE>"
tmutil listbackups -d "$DRV"
tmutil listbackups -d "$DRV" -t # terse timestamps
tmutil latestbackup -d "$DRV"
BK="/Volumes/<DRIVE>/Backups.backupdb/<Machine>/2021-06-14-093211"
ls -la "$BK"
ls -la "$BK"/*/Users/# preferred: preserves ACLs, xattrs, Finder metadata tmutil restore -v "$BK/Macintosh HD - Data/Users/<olduser>/Pictures" \ "$STAGE/tm-extract/<machine>/Pictures" # fallback when tmutil restore refuses (common across machines/users) ditto "$BK/Macintosh HD - Data/Users/<olduser>/Pictures" \ "$STAGE/tm-extract/<machine>/Pictures"
Use ditto, not rsync, on this machine
Verified on M2: /usr/bin/rsync is openrsync (protocol version 29 / rsync 2.6.9 compatible), and no Homebrew rsync is installed (/opt/homebrew/bin/rsync does not exist). openrsync does not reliably honor -H (hard links), -A (ACLs), or -X (xattrs) — exactly the flags a Time Machine extraction needs. Use ditto for local drive-to-drive copies, or brew install rsync first. Do not assume rsync -aHAX behaves the way it does on Linux here.
.sparsebundle by handhdiutil attach -readonly "/Volumes/<DRIVE>/<name>.sparsebundle"
# mounts under /Volumes/<BundleVolumeName>, usually "Time Machine Backups"
ls -la "/Volumes/Time Machine Backups/Backups.backupdb/"
tmutil listbackups -d "/Volumes/Time Machine Backups"
hdiutil detach "/Volumes/Time Machine Backups"Use -readonly every time — it means a mistake inside the mounted backup cannot damage the backup. If detach says busy, close Finder windows and any Terminal whose working directory is inside it; -force is a last resort.
If the bundle refuses to attach, copy the whole .sparsebundle directory to the 8TB with ditto first, then run any repair attempt against the copy. Never against the original.
diskutil info /dev/disk4s2 | grep -E "FileVault|Encrypted|Unlocked"
hdiutil attach -readonly "/Volumes/<DRIVE>/<name>.sparsebundle" # will prompt
security find-generic-password -s "<sparsebundle name>" 2>&1 | head -5Also check Keychain Access.app → search the drive or backup name → look for a Time Machine or disk-image password entry.
STOP IF you cannot unlock an encrypted backup
Do not plan any drive reuse that assumes you will get that data back. There are exactly three honest options: keep hunting for the password, keep the drive as-is indefinitely and never reformat it, or consciously write the data off. Reformatting an encrypted backup you never unlocked is permanent, silent data loss.
Never copy individual files out of a .photoslibrary bundle
It is not a folder of pictures. It is a SQLite database (database/Photos.sqlite) plus originals/ (content-hashed, meaningless filenames) plus resources/derivatives/ (generated display-resolution JPEGs) plus non-destructive adjustment records.
What you lose by copying files out of the bundle:
originals/ are UUID-derived. The result is unusable as an archive.ditto "/Volumes/<DRIVE>/Pictures/Photos Library.photoslibrary" \
"$STAGE/photoslibs/<drive-N>-Photos Library.photoslibrary"ditto treats the bundle as the opaque unit it is. The library stays openable in Photos.app — that is what makes it a real archive rather than a pile of JPEGs.
Cmd-A to select all.$STAGE/loose/<drive-N>-photos-export/.Use Export Unmodified Originals, not plain Export. Plain Export writes rendered JPEGs with edits baked in and metadata partially stripped.
Do both when the library matters
Keep the bundle (Method 1) as the authoritative archive and produce an export (Method 2) as the thing you dedup and browse. Disk is cheap on an 8TB; a lost photo library is not recoverable.
Check before you count a library as rescued
If the old library was iCloud-optimized (Download Originals to this Mac was OFF), the local bundle may hold only low-res placeholders. Confirm before you treat it as a copy of anything.
Why filenames are useless here
Every iPhone produces IMG_0001.JPG through IMG_9999.JPG and then wraps around. Two completely different photos from two different phones routinely share a filename, and the same photo copied through three devices routinely has three different names. Filename dedup gives you both false positives (deleting a unique photo) and false negatives (keeping five copies).
cd "$STAGE" find "$STAGE/tm-extract" "$STAGE/loose" -type f -size +1k \ -not -name ".DS_Store" -not -path "*/.Spotlight-V100/*" -print0 \ | xargs -0 shasum -a 256 > "$STAGE/manifests/stage-$(date +%Y%m%d).sha256" wc -l "$STAGE/manifests/stage-$(date +%Y%m%d).sha256"
M="$STAGE/manifests/stage-$(date +%Y%m%d).sha256"
awk '{print $1}' "$M" | sort | uniq -d > "$STAGE/manifests/dupe-hashes.txt"
wc -l "$STAGE/manifests/dupe-hashes.txt"
# logical bytes across all copies — stat, NOT du
grep -F -f "$STAGE/manifests/dupe-hashes.txt" "$M" \
| sed 's/^[a-f0-9]* //' \
| while IFS= read -r f; do stat -f '%z' "$f"; done \
| awk '{s+=$1} END {printf "%.2f GiB across all copies\n", s/1024/1024/1024}'sort "$M" > "$STAGE/manifests/stage-sorted.sha256"
awk 'NR==FNR{d[$1];next} ($1 in d)' \
"$STAGE/manifests/dupe-hashes.txt" "$STAGE/manifests/stage-sorted.sha256" \
> "$STAGE/manifests/dupe-groups.txt"
head -60 "$STAGE/manifests/dupe-groups.txt"
mkdir -p "$STAGE/_dupes-pending-delete"
mv "/path/to/redundant/copy.jpg" "$STAGE/_dupes-pending-delete/"Each group of lines sharing a hash is a set of byte-identical files. Keep the copy with the most informative path — usually tm-extract/<machine>/…/Pictures/…, not quarantine/. Move, do not rm. Delete the holding folder only after Phase 4 passes and you have lived with the result for a week.
What hash dedup will NOT catch
The same photo re-encoded, resized, or re-exported has different bytes and the same picture. That is perceptual dedup — a harder, human-in-the-loop problem. Do not try to solve it during the migration. Byte-identical dedup is safe and mechanical; run anything beyond it against the consolidated archive later, once it is safely in place.
Verify Phase 3
Every processed drive has a corresponding tree under $STAGE · copy-failures.txt is empty or every entry is accounted for · every .photoslibrary found in Phase 2 exists as a whole bundle under photoslibs/ · a hash manifest exists.
STOP IF
$STAGE free space drops below 20% → stop and reassess. Check with diskutil info | grep "Container Free Space", not du.There is no Trash, no snapshot, and no diskutil unerase. Data-recovery services exist, cost four figures, and usually fail on SSDs because TRIM has already zeroed the blocks.
Nothing gets reformatted until every check on this page reads PASS.
The three things that DO constitute proof
File count · total logical bytes (stat -f '%z', the file's real size independent of allocation) · content hashes on a random sample. du is not on this list and never will be — see the APFS clone/sparse trap in Phase 2.
SRC="/Volumes/<DRIVE>/<folder>" DST="$STAGE/loose/<drive-N>/<folder>" SRC_N=$(find "$SRC" -type f -not -name ".DS_Store" | wc -l | tr -d ' ') DST_N=$(find "$DST" -type f -not -name ".DS_Store" | wc -l | tr -d ' ') echo "SRC files: $SRC_N / DST files: $DST_N" [ "$SRC_N" = "$DST_N" ] && echo "COUNT: PASS" || echo "COUNT: FAIL" # also dirs (catches empty dirs ditto created but never filled) echo "dirs SRC=$(find "$SRC" -type d | wc -l) DST=$(find "$DST" -type d | wc -l)" # also symlinks (a bad copy silently dereferences or drops them) echo "links SRC=$(find "$SRC" -type l | wc -l) DST=$(find "$DST" -type l | wc -l)"
bytes_of() {
find "$1" -type f -not -name ".DS_Store" -print0 \
| xargs -0 stat -f '%z' \
| awk '{s+=$1} END {printf "%d\n", s}'
}
SRC_B=$(bytes_of "$SRC"); DST_B=$(bytes_of "$DST")
echo "SRC bytes: $SRC_B / DST bytes: $DST_B"
[ "$SRC_B" = "$DST_B" ] && echo "BYTES: PASS" || echo "BYTES: FAIL — diff $((SRC_B - DST_B))"Count and bytes can both match while content is corrupt — a truncated-then-padded file, a bit-flip from a dying drive. Hashes are the only thing that catches that.
N=100 cd "$SRC" find . -type f -not -name ".DS_Store" | sort -R | head -$N > /tmp/sample-list.txt ( cd "$SRC" && tr '\n' '\0' < /tmp/sample-list.txt | xargs -0 shasum -a 256 ) \ | sort > "$STAGE/manifests/verify-src.sha256" ( cd "$DST" && tr '\n' '\0' < /tmp/sample-list.txt | xargs -0 shasum -a 256 ) \ | sort > "$STAGE/manifests/verify-dst.sha256" # FAIL CLOSED. Check the hashing actually RAN before trusting the diff: # if both manifests are empty, `diff` succeeds and you get a false PASS. SAMPLE_N=$(wc -l < /tmp/sample-list.txt | tr -d ' ') SRC_H=$(wc -l < "$STAGE/manifests/verify-src.sha256" | tr -d ' ') DST_H=$(wc -l < "$STAGE/manifests/verify-dst.sha256" | tr -d ' ') echo "expected $SAMPLE_N | src $SRC_H | dst $DST_H" if [ "$SAMPLE_N" -eq 0 ] || [ "$SRC_H" -ne "$SAMPLE_N" ] || [ "$DST_H" -ne "$SAMPLE_N" ]; then echo "HASH SAMPLE: INVALID - hashing did not run correctly. NOT a pass. Do not erase." elif diff -q "$STAGE/manifests/verify-src.sha256" "$STAGE/manifests/verify-dst.sha256" >/dev/null; then echo "HASH SAMPLE: PASS ($SAMPLE_N/$SAMPLE_N matched)" else echo "HASH SAMPLE: FAIL - content differs. Do not erase anything." fi
( cd "$SRC" && find . -type f -not -name ".DS_Store" -print0 | xargs -0 shasum -a 256 ) | sort > full-src.sha256 ( cd "$DST" && find . -type f -not -name ".DS_Store" -print0 | xargs -0 shasum -a 256 ) | sort > full-dst.sha256 [ -s full-src.sha256 ] && [ -s full-dst.sha256 ] && diff -q full-src.sha256 full-dst.sha256 >/dev/null \ && echo "FULL HASH: PASS" || echo "FULL HASH: NOT VERIFIED - empty manifest or mismatch. Do not erase."
You cannot meaningfully hash-compare the inside of a live library mid-flight, and you should not try. Verify it the way it will actually be used:
ditto the bundle to the 8TB.find.Step 3 is the real test. A byte-perfect copy of a corrupt library is a corrupt library.
Drive: <N> / <volume name> / <model> Source path: /Volumes/.../... Dest path: /Volumes/<8TB>/_STAGING/... Files: SRC=<n> DST=<n> PASS/FAIL Dirs: SRC=<n> DST=<n> PASS/FAIL Bytes: SRC=<n> DST=<n> PASS/FAIL Hash sample: <n> files, diff clean? PASS/FAIL Photos libs: opened in Photos.app and verified? YES/NO/NA Verified on: <date>
Time Machine needs 1.5× to 2× the source size to hold a useful history. At 1× it can hold one snapshot and then immediately starts deleting old backups. Below 1× it cannot complete a first backup at all. Time Machine does not compress. M2's source data is 337 GiB.
| Drive | Marketing | Real usable | Holds 337 GiB once? | Viable M2 TM target? (needs ≥ ~505 GiB) |
|---|---|---|---|---|
| 8TB | 8 TB | ~7.28 TiB | Yes | Yes — 21× headroom |
| 2TB Crucial X8 | 2 TB | ~1.82 TiB | Yes | Yes technically (5.5×) — but Sean wants it freed |
| 2TB (Nano's) | 2 TB | ~1.82 TiB | Yes | Off limits — Nano's |
| 250GB | 250 GB | ~232 GiB | NO | NO — smaller than M2's used data |
| 120GB | 120 GB | ~111 GiB | NO | NO — one third of M2's used data |
Said plainly
A 250GB drive cannot hold a 337 GiB Time Machine backup. Not compressed, not thinned, not ever. The same is more true of the 120GB. No amount of exclusions makes these backup targets — you would have to exclude ~/AI_OS (42G) and ~/Pictures (18G) and more, which means the "backup" excludes the things you most need backed up.
What they are good for: 250GB → second copy of the irreplaceable subset (photos + documents core, very likely under 232 GiB after dedup), or a single-project scratch disk. 120GB → bootable macOS 15 installer (~16 GB, genuinely useful when a Mac will not boot), sneakernet disk, or retirement. If the 120GB is an old spinner with unknown SMART, retire it — keeping marginal old drives around creates a temptation to trust them.
~/Downloads + AI_OS working sets, video scratch, VM images. Unregistered from Time Machine. Nothing on it is the only copy of anything.M2-TimeMachine and Archive. Single point of failure — see below.Archive volume. No backup duty. Risk unbundled.Reading B is materially better
It is the difference between "my backup and my archive are on the same device" and "they are not." That is why the Phase 0 drive count is not bookkeeping.
| # | Reason | Consequence |
|---|---|---|
| 1 | Time Machine consumes all free space by design. | It fills the destination, then thins old backups. Your scratch capacity shrinks to zero, then TM starts deleting backup history to make room for scratch files. |
| 2 | Time Machine wants an exclusive APFS volume. | On macOS 15 it claims the volume with a Backup role and manages snapshots on it. Sharing with active scratch I/O is unsupported territory. |
| 3 | Contention. | A backup is thousands of small writes plus snapshot bookkeeping. Running a video export against the same device makes both slow and can drop the volume on a USB bridge. |
| 4 | Blast radius. | Scratch drives get erased casually — that is the point. The moment scratch is also your backup, "just wipe the scratch drive" becomes a data-loss event. |
The standard is 3 copies, 2 different media, 1 offsite. Under Reading A the proposed end state gives you:
| Data | Copy 1 | Copy 2 | Copy 3 | Offsite? |
|---|---|---|---|---|
| M2 live system (337 GiB) | M2 internal SSD | 8TB M2-TimeMachine | — | No |
| Consolidated archive | 8TB Archive | 250GB (irreplaceable subset) | — | Only if you physically carry the 250GB elsewhere |
Under Reading A, the 8TB holds both M2's only backup and the only full copy of the consolidated archive. If that one drive dies, you lose your backup and your archive in the same instant.
Two APFS volumes on one container do not help. They share a container, a partition table, a controller, an enclosure, a cable, and a power supply. That is not a backup architecture — it is one drive with two names on it.
It is still strictly better than today, because today M2 has zero working backups. Take the improvement, then close the gap.
Immediately: copy the irreplaceable subset (photos + documents after dedup) to the 250GB, verify with Phase 4, and store it somewhere other than the desk the 8TB sits on. That is your "1 offsite" and it costs nothing.
Resolve the drive count. If Reading B is true, move M2's Time Machine to a spare 2TB and the single point of failure disappears entirely.
Cloud for the irreplaceable subset. The archive core is the part that cannot be rebuilt. Object storage for the photos+documents core is the only true offsite that survives a house fire. This is the one place where spending money is the right answer, and it is a small amount for the risk actually retired.
If Reading A holds and you can spend: a second 8TB as an archive mirror is the clean fix. Rotate it offsite.
Fleet-internal partial mitigation: Nano has 163 GiB free; Mini is unknown. Neither can hold the full archive. Do not pretend fleet copies substitute for a real second device — but a copy of the documents portion on Nano costs nothing and beats not having it.
Do not let the perfect block the good
Get M2 backed up to the 8TB today. Then work down that list.
/Volumes/Scratch/ aios-working/ # offloaded ~/AI_OS working sets — NOT the canonical tree downloads/ # relocated ~/Downloads (9.7G today) media-scratch/ # video / render intermediates vm/ # VM images, container storage _EPHEMERAL.txt # "Everything here is disposable. # If it matters, it does not live here."
Write that rule into _EPHEMERAL.txt on the drive itself so future-you finds it. Moving ~/Downloads and AI_OS build artifacts is the fastest relief for M2's 81%. Be careful with ~/AI_OS — per fleet convention it is the canonical tree, so move working sets and artifacts, not the tree. Add the scratch volume to Time Machine's exclusions so you are not backing up disposable data.
STOP IF
Dependency-ordered. Every step ends at a safe resting state — you can stop after any of them and be no worse off than when you started.
Precondition: 8TB exists, passed Phase 1, and Phase 2 says it is empty or holds only already-classified material.
Why first: every hour spent on consolidation before this is an hour of uninsured exposure on 337 GiB. If M2's SSD dies tonight, all of it goes.
# 1. confirm the identifier — only this drive connected diskutil list external physical # 2. create a dedicated TM volume inside the existing APFS container diskutil apfs addVolume /dev/disk<N> APFS "M2-TimeMachine" # 3. register it, APPENDING (-a) rather than replacing sudo tmutil setdestination -a /Volumes/M2-TimeMachine tmutil destinationinfo # 4. first backup, blocking so you can watch it succeed sudo tmutil startbackup --block --destination <destination-id> # 5. prove it tmutil latestbackup
If the 8TB already holds data you have not extracted, skip to Step 2 and come back. Do not erase it. If the 8TB is not APFS and must be converted, that is destructive — read the gate below.
Safe resting state after Step 1
M2 has a real, verified backup. This alone justifies the whole exercise. You can stop here for a week.
Proceed only if all are true:
diskutil list /dev/disk<N> output is on screen and you have read the identifier out loud# ERASES THE ENTIRE DRIVE. NO UNDO. # diskutil eraseDisk APFS "Archive" GPT /dev/disk<N>
Prefer diskutil apfs addVolume (non-destructive) over eraseDisk whenever the drive is already APFS.
Precondition: none — read-only, always safe. Output: complete drive table, count resolved, trust levels assigned, Mini's status recorded. Resting state: you know what you have; nothing changed.
Order: process the most suspect drive first. A dying drive's data is the most time-sensitive thing you own; healthy drives will wait, failing ones will not. Resting state: you know what is on each drive; nothing changed.
Precondition: Steps 1–3 done; $STAGE has ≥ 2× the source drive's used space free. Per drive: mount read-only → extract → hash manifest → erase nothing.
Rollback: the source is untouched behind a read-only mount. Delete the staging folder and start over — zero risk to the source. Resting state: that drive's data exists in two places.
Output: GATE-drive-<N>.md reading PASS on every line. Rollback: re-extract; source is still intact. Resting state: extraction is proven — and this is the last point at which everything is still recoverable from the original drives. Past this line, erasures begin.
Action: build the single Archive tree; move duplicates to _dupes-pending-delete/ and do not empty it yet. Rollback: move files back out — fully reversible.
Action: copy photos + documents core to the 250GB, verify with Phase 4, take it to a different physical location. Why here: this is the step that makes the erasures in Step 8 safe. Do not reorder it after them.
Do not enter Step 8 unless all of these hold:
_dupes-pending-delete/ has been reviewedtmutil latestbackup returns a real timestampRollback for Step 8: NONE. This is the point of no return. That is why every gate above it exists.
tmutil destinationinfo -X # find the destination ID for "Backup" sudo tmutil removedestination <destination-id> tmutil destinationinfo # confirm it is gone
A stale registration is exactly what makes M2 look backed up today while having no backup. Clear it.
Action: create the scratch layout, move ~/Downloads and AI_OS working sets, add TM exclusions. Rollback: move the data back — reversible. Target: M2 below 70% used (81% today).
Precondition: everything above done, plus a full week of normal use with nothing found missing. Action: delete _dupes-pending-delete/; collapse _STAGING into the final Archive layout. Rollback: none after deletion — which is why it is last, and why it waits.
sudo tmutil setdestination -a /Volumes/M2-TimeMachine # -a appends, keeps existing tmutil destinationinfo tmutil destinationinfo -X # plist, includes UUIDs sudo tmutil removedestination <destination-id> # clear a stale one sudo tmutil setquota <destination-id> 2000 # cap TM at 2000 GB
setquota matters on the 8TB under Reading A. Without a quota, Time Machine grows to fill the volume — eventually consuming the space you wanted for the archive.
tmutil addexclusion -p /Volumes/Scratch tmutil addexclusion -p /Users/SeanVargas/Library/Caches tmutil addexclusion -p /Users/SeanVargas/Downloads # only if truly disposable tmutil isexcluded /Volumes/Scratch /Users/SeanVargas/Library/Caches tmutil removeexclusion -p /Volumes/Scratch # undo
Without -p the exclusion attaches to a specific file/inode and is lost when the item is recreated. Use -p for directories. Do not exclude ~/AI_OS, ~/Pictures, or ~/Documents. Those are the whole point.
tmutil latestbackup # the one that matters tmutil listbackups tmutil listbackups -t tmutil latestbackup -d /Volumes/M2-TimeMachine tmutil compare # what changed between the last two tmutil compare -@ / /Volumes/M2-TimeMachine/<backup-path> # deeper diagnosis when a backup fails — always pipe through tail/grep log show --predicate 'subsystem == "com.apple.TimeMachine"' --last 24h --style compact | tail -100
| Result | Meaning |
|---|---|
| A path with today's or yesterday's timestamp | PASS — a real backup exists |
| Code 18 "Failed to mount destination", drive unplugged | Expected. Proves nothing about the drive. Re-test with the drive attached. |
| Code 18 with the drive mounted and visible in Finder | Real problem — corrupt TM volume, wrong destination ID, or a failing drive. Treat that drive as READ-ONCE per Phase 1. |
#!/bin/bash echo "=== $(date) ===" df -h / | tail -1 tmutil destinationinfo tmutil latestbackup || echo "!!! NO BACKUP — INVESTIGATE NOW !!!" tmutil listbackups 2>/dev/null | wc -l diskutil info /dev/disk<8TB> | grep -E "SMART Status|Container Free Space" stat -f '%Sm' /Volumes/<250GB>/irreplaceable 2>/dev/null || echo "250GB not connected"
Four questions it must answer every month. Any "no" is an action item, not a note.
tmutil latestbackup return a timestamp within the last 7 days?The restore test — the most-skipped, most-valuable step
Once a year, actually restore something non-trivial from Time Machine — a folder, not a single file — and confirm it comes back intact. An untested backup is a hypothesis, not a backup. Also re-run Phase 1 health checks on every drive, and re-read the 3-2-1 gap: has the mitigation actually been done, or is it still a plan?
Everything this plan assumes but could not verify. Each is a real hole, not a formality. Nothing in this document is a measurement of a drive.
| # | Unknown | Impact | Resolved by |
|---|---|---|---|
| 1 | Drive count — 5 or 7? | Decides whether M2's backup can live on separate hardware from the archive. The single most consequential unknown here. | Phase 0.3–0.9 |
| 2 | Contents of every drive. Nothing inspected; none connected. | Every size estimate, dedup expectation, and timeline in this plan is unvalidated. | Phase 2 |
| 3 | Mini's state. Unreachable, timed out twice. | Cannot claim a fleet-wide backup posture. Mini may be unbacked-up too. | Phase 0.8 |
| 4 | Whether any backup is encrypted, and whether the password is known. | An encrypted backup without its password is not recoverable data. Could invalidate a whole drive's "value extracted" assumption. | Phase 3.4 — check early |
| 5 | Drive models, interfaces, real speeds. Only the Crucial X8 is named. | "Fast" and "slow" are characterization, not measurement. A USB-2 cable makes a fast drive slow. | Phase 0.4–0.5 |
| 6 | Actual health of all drives. Age unknown, SMART unread, most USB enclosures report Not Supported anyway. | The architecture depends on the 8TB being trustworthy. If it is not, the plan does not hold. | Phase 1 |
| 7 | Why M2's TM destination fails to mount. | Code 18 with the drive unplugged is expected — or the X8 may be sick, which would also make it a poor scratch drive. | Phase 7.3, retested attached |
| 8 | Which old machines have backups here, and whether their data exists elsewhere. | Some extraction effort may be redundant with iCloud or existing copies. | Phase 2 |
| 9 | Whether old Photos libraries hold originals or iCloud placeholders. | An "archived" library of thumbnails is not an archive. | Phase 3.5 |
| 10 | Post-dedup size of the irreplaceable subset. | The 250GB-as-mirror plan assumes it lands under 232 GiB. Plausible, unverified. If it exceeds that, the offsite plan needs different hardware or cloud. | Phase 3.6 — measure with stat, not du |
| 11 | Free space on the 8TB right now. Assumed largely empty. | If it is full, Phase 3 staging has nowhere to go and Step 1 cannot proceed. | Phase 0.5 |
| 12 | Whether Sean has any cloud backup today. | Changes how urgent the offsite gap is. | Ask Sean |
| 13 | Nano's and Mini's backup freshness. Nano has two destinations registered; whether either completed recently is unknown. | Registered ≠ working. M2 proves that. | ssh SeanVargas@100.116.58.44 'tmutil latestbackup' |