3 Weeks of Silent Backup Failure: Why launchd Can't Write to ~/Documents on macOS A developer discovered that their macOS backup job had silently failed for three weeks because launchd couldn't write to ~/Documents due to macOS TCC permissions. The developer solved the issue by moving the backup target to ~/.claude/config-snapshots/ and implementing a two-stage backup system that uses rsync to copy only config files to a separate directory under git, avoiding the noise of the full ~/.claude directory. Every Sunday at 6:00 AM, my backup job ran, 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 told me. The first problem you hit when you automate personal development work is "I can't roll back when my config breaks." Once you build out a Claude Code environment, hundreds of lines of configuration accumulate under ~/.claude/ . settings.json tool permissions, model selection, hook paths , CLAUDE.md the full text of global operating rules , the hooks/ directory automated checks like Stop hooks and PreTool hooks , agents/ specialized agent definition files , skills/auto/ skills that get self-generated during real usage , rules/ the ECC rules digest — dozens of places in these change every week. Claude Code updates, hook script improvements, new agent definitions. The more actively you use it, the more the configuration changes like a living thing. The problem is that ~/.claude/ isn't under version control. A Claude Code cache update wipes out a config file, a mistake in a hook script takes down every PreTool hook, a one-line typo in settings.json makes permission prompts explode everywhere — and when that happens, with no diff and no history, you can't even tell when it broke. Reconstructing by feel from "it was working a week ago" is pure lost time. The two approaches most people reach for here are "back it up manually" or "put the whole thing in git." Manual backups get tedious even at weekly cadence. And your environment breaks precisely during the week you skipped. "I was going to get around to it" doesn't help you recover. The whole thing in git doesn't work in practice, because ~/.claude/ mixes in noise like plugin cache, session history, telemetry, and paste-cache, which balloons the repository to several GB. It also can't rule out the risk of secrets ending up in the cache. The solution is a two-stage structure: rsync only the config files to a separate directory, and put only that directory under git. Never touch ~/.claude/ itself, and define the sync targets explicitly with an INCLUDE list. Even when the human forgets, launchd runs every Sunday at 6:00. If there's a diff, commit it in conventional commits format. If there's no diff, write no changes to the log and exit. This works because it takes human willpower out of the loop . Config files are too fine-grained to manage on the strength of "I should really back this up." But losing them costs hours to reconstruct. Automation fills that asymmetry, and launchd sits at the bottom layer of that automation as a highly reliable OS feature. There are side benefits too. Because the diffs stay in git, you can track down "which change last week broke the hook" with git log --oneline . You can discover configuration drift parts that changed unintentionally . You can reproduce the setup immediately when moving to another machine. You can build up improvements while checking "what did I do a week ago" with git diff . The header comment of the actual script ~/.claude/scripts/dotfiles-snapshot.sh reads like this: dotfiles-snapshot.sh — ~/.claude の設定だけを別ディレクトリに同期して git 管理 元の ~/.claude は触らない(plugin cache 等のノイズと混ざらないため) 同期先: ~/.claude/config-snapshots/ (旧 ~/Documents/claude-config-snapshots は launchd 実行時に macOS TCC で "Operation not permitted" になり全コピー失敗していたため 2026-06-01 に移設) 既存 ~/Documents/my-knowledge-base/ の SessionEnd auto-commit と分離管理 The line "never touch the original ~/.claude " expresses the design philosophy. The backup source directory itself isn't under git; only the necessary config files are copied to a separate directory, and only that becomes a repository. The INCLUDE list is the "canonical definition" of what counts as a config file. And the fourth line of that comment — the old ~/Documents/claude-config-snapshots hit macOS TCC "Operation not permitted" when run under launchd and every copy failed, so it was relocated on 2026-06-01 — is the actual subject of this article. Before that relocation on 2026-06-01, the backup ran every Sunday at 6:00 "looking like it worked" while doing nothing at all. Here's the structure of the current system. ┌─────────────────────────────────────────────────────────┐ │ launchd │ │ Label: com.shun.dotfiles-snapshot │ │ 毎週日曜 06:00 │ │ ProcessType: Background / Nice: 10 / LowPriorityIO: true│ └──────────────────────┬──────────────────────────────────┘ │ /bin/zsh -c