Skip to content

Build1 publisher3 min readPublished

Exit 0 is not a health check: three weeks of macOS backups that copied nothing

A launchd job logged "no changes" and exited clean every Sunday while TCC blocked every write to ~/Documents. The success path and the total-failure path looked identical.

The Engineer · Build desk

Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction

What happened

  • The author's backup job ran every Sunday at 6:00 AM, logged "no changes", and exited 0; for three weeks straight it never copied a single file, macOS was blocking it, and nothing in the system reported the failure.
  • The old sync destination ~/Documents/claude-config-snapshots hit macOS TCC "Operation not permitted" when run under launchd and every copy failed, so the destination was relocated to ~/.claude/config-snapshots/ on 2026-06-01.
  • The script is ~/.claude/scripts/dotfiles-snapshot.sh; it rsyncs only the config files from ~/.claude to a separate directory and never touches ~/.claude itself, to avoid mixing with plugin cache noise.
  • The launchd job is labelled com.shun.dotfiles-snapshot, runs weekly on Sunday at 06:00, with ProcessType Background, Nice 10, and LowPriorityIO true.
  • Script behaviour: if there is a diff, commit it in conventional commits format; if there is no diff, write "no changes" to the log and exit.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

A weekly macOS backup job ran at 6:00 every Sunday, wrote "no changes" to its log, exited 0, and copied not a single file for three weeks running, according to a dev.to writeup by the person who built it [1]. The cause was macOS TCC: under launchd, the destination path under ~/Documents returned "Operation not permitted", every copy failed, and the sync target was moved to ~/.claude/config-snapshots/ on 2026-06-01 [2].

At one run per week, that is three consecutive scheduled executions that produced a clean log line and a zero exit status while doing nothing [11], leaving at least 21 days with no usable snapshot [13]. The job was not exotic. It was a launchd agent labelled com.shun.dotfiles-snapshot, weekly at 06:00, ProcessType Background, Nice 10, LowPriorityIO true [4], running a script at ~/.claude/scripts/dotfiles-snapshot.sh that rsyncs only config files out of ~/.claude into a separate directory and never modifies ~/.claude itself [3]. An explicit INCLUDE list is the canonical definition of what counts as config [9]. The design exists because ~/.claude is not under version control and putting the whole directory in git fails in practice: plugin cache, session history, telemetry and paste-cache balloon the repository to several GB and cannot rule out secrets landing in the cache [6]. What is worth keeping is real, though. settings.json, CLAUDE.md, hooks/, agents/, skills/auto/ and rules/ change in dozens of places every week [7].

The interesting part is the failure signature, not the permission. The script's documented behaviour is: if there is a diff, commit in conventional commits format; if there is no diff, write "no changes" to the log and exit [5]. That means the healthy quiet week and the week where the process could not write anywhere both terminate on the same line with the same status [12]. No alert fires because nothing failed, in the only sense the scheduler understands. This is the standard shape of macOS automation rot: TCC denies a file operation, the tool reports it as a copy that did not happen, and the wrapper interprets zero copies as zero changes.

The operator conclusion, which is mine and not the source's, is that exit codes are a liveness signal at best and never a health signal for anything that touches a protected directory. A job of this kind needs a proof-of-work assertion that is independent of the job's own reporting: check that the destination's newest file mtime is inside the expected window, or that the git repository has a commit or a verified clean tree from the last scheduled run, and treat "unchanged for N cycles" as a paging condition rather than a good week. If a backup cannot demonstrate that it read the source and wrote the destination, it has not demonstrated anything.

Two things to watch. The writeup does not say which TCC permission the launchd agent lacked or how consent was configured [14], so anyone copying the fix is copying a relocation, not a grant. And the snapshot job is deliberately kept separate from an existing SessionEnd auto-commit for ~/Documents/my-knowledge-base/ [8], which still sits inside the protected tree; that one runs from an interactive session today, and moving it under launchd would put it back on the same boundary. The reported side benefits of the git layer, tracing which change broke a hook with git log --oneline, spotting configuration drift, rebuilding on another machine [10], all depend on the snapshot actually happening.

Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories