Last active
March 31, 2026 09:09
-
-
Save cescox/d76447e0677e5deedf9cd725a332334b to your computer and use it in GitHub Desktop.
macOS Time Machine exclusions for developers — Docker, OrbStack, node_modules, Homebrew, Xcode, package caches & more
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env zsh | |
| # ============================================================================= | |
| # tm-exclude-dev.sh — Complete Time Machine setup for macOS developers | |
| # ============================================================================= | |
| # | |
| # What this does: | |
| # 1. Excludes 70+ global cache/VM/tool directories from Time Machine | |
| # 2. Scans for per-project dependency dirs (node_modules, target, .venv, etc.) | |
| # 3. Installs & starts asimeow to auto-exclude new per-project deps every 6h | |
| # | |
| # Usage: | |
| # curl -sL <gist-raw-url> | zsh # Dry run | |
| # curl -sL <gist-raw-url> | zsh -s -- --apply # Apply everything | |
| # | |
| # # Or download and run: | |
| # chmod +x tm-exclude-dev.sh | |
| # ./tm-exclude-dev.sh # Dry run (show what would happen) | |
| # ./tm-exclude-dev.sh --apply # Apply exclusions + install asimeow | |
| # | |
| # Notes: | |
| # - Uses sticky exclusions ($TMUTIL addexclusion without -p), no sudo needed | |
| # - Sticky exclusions follow the directory if moved/renamed | |
| # - ~/Library/Caches is already excluded by macOS by default | |
| # - APFS local snapshots always include everything; exclusions only affect | |
| # what gets copied to the backup destination | |
| # - Safe to re-run — skips already-excluded paths | |
| # | |
| # References: | |
| # - asimeow: https://github.com/mdnmdn/asimeow | |
| # - asimov: https://github.com/stevegrunwell/asimov | |
| # - tmutil: man tmutil | |
| # ============================================================================= | |
| set -euo pipefail | |
| # --------------------------------------------------------------------------- | |
| # Ensure standard paths are in PATH | |
| # --------------------------------------------------------------------------- | |
| export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/bin:/opt/homebrew/bin:$PATH" | |
| TMUTIL=/usr/bin/tmutil | |
| DU=/usr/bin/du | |
| FIND=/usr/bin/find | |
| # Verify tmutil exists | |
| if [[ ! -x "$TMUTIL" ]]; then | |
| echo "Error: tmutil not found at $TMUTIL" >&2 | |
| exit 1 | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # Colors | |
| # --------------------------------------------------------------------------- | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[0;33m' | |
| BLUE='\033[0;34m' | |
| BOLD='\033[1m' | |
| NC='\033[0m' # No Color | |
| # --------------------------------------------------------------------------- | |
| # Global directories to exclude | |
| # --------------------------------------------------------------------------- | |
| # Format: "path|description|category" | |
| # | |
| # NOTE: ~/Library/Caches (and subdirs like JetBrains, Homebrew, pip, etc.) | |
| # are already excluded by macOS by default — no need to list them here. | |
| EXCLUSIONS=( | |
| # --- Container Runtimes & VMs --- | |
| "$HOME/Library/Containers/com.docker.docker/Data|Docker Desktop VM disk image|Container Runtimes & VMs" | |
| "$HOME/.docker|Docker CLI config and plugins|Container Runtimes & VMs" | |
| "$HOME/Library/Group Containers/HUAQ24HBR6.dev.orbstack|OrbStack VM and container data|Container Runtimes & VMs" | |
| "$HOME/.colima|Colima VM disk images|Container Runtimes & VMs" | |
| "$HOME/.lima|Lima VM data|Container Runtimes & VMs" | |
| "$HOME/.local/share/containers|Podman container storage|Container Runtimes & VMs" | |
| "$HOME/.vagrant.d|Vagrant global boxes cache|Container Runtimes & VMs" | |
| "$HOME/Library/Parallels|Parallels Desktop VMs|Container Runtimes & VMs" | |
| "$HOME/Documents/Virtual Machines|VMware Fusion VMs|Container Runtimes & VMs" | |
| "$HOME/.VirtualBox|VirtualBox config|Container Runtimes & VMs" | |
| "$HOME/VirtualBox VMs|VirtualBox VM images|Container Runtimes & VMs" | |
| "$HOME/Library/Containers/com.utmapp.UTM/Data/Documents|UTM VM images|Container Runtimes & VMs" | |
| # --- Package Manager Global Caches --- | |
| "$HOME/Library/pnpm|pnpm content-addressable store|Package Caches" | |
| "$HOME/.npm|npm global cache|Package Caches" | |
| "$HOME/.yarn/cache|Yarn cache|Package Caches" | |
| "$HOME/.cache/yarn|Yarn cache (alt location)|Package Caches" | |
| "$HOME/.pnpm-store|pnpm store (alt location)|Package Caches" | |
| "$HOME/.bun|Bun runtime and cache|Package Caches" | |
| "$HOME/.deno|Deno runtime cache|Package Caches" | |
| "$HOME/.cargo|Rust Cargo registry and build cache|Package Caches" | |
| "$HOME/.rustup|Rust toolchain installations|Package Caches" | |
| "$HOME/go|Go modules and build cache|Package Caches" | |
| "$HOME/.gem|Ruby gems|Package Caches" | |
| "$HOME/.rubies|Ruby versions (chruby)|Package Caches" | |
| "$HOME/.composer|PHP Composer packages|Package Caches" | |
| "$HOME/.cocoapods|CocoaPods spec repos|Package Caches" | |
| "$HOME/.pub-cache|Dart/Flutter pub cache|Package Caches" | |
| "$HOME/.nuget|.NET NuGet packages|Package Caches" | |
| "$HOME/.m2|Maven local repository|Package Caches" | |
| "$HOME/.gradle|Gradle global cache|Package Caches" | |
| "$HOME/.swiftpm|Swift Package Manager cache|Package Caches" | |
| "$HOME/.kotlin|Kotlin compiler data|Package Caches" | |
| # --- Version Managers --- | |
| "$HOME/.nvm|Node Version Manager|Version Managers" | |
| "$HOME/.fnm|Fast Node Manager|Version Managers" | |
| "$HOME/.volta|Volta (JS tool manager)|Version Managers" | |
| "$HOME/.asdf|asdf version manager|Version Managers" | |
| "$HOME/.sdkman|SDKMAN (Java SDK manager)|Version Managers" | |
| "$HOME/.jabba|Jabba (Java version manager)|Version Managers" | |
| "$HOME/.pyenv|pyenv (Python versions)|Version Managers" | |
| "$HOME/.rbenv|rbenv (Ruby versions)|Version Managers" | |
| "$HOME/.rvm|RVM (Ruby Version Manager)|Version Managers" | |
| # --- Python Environments --- | |
| "$HOME/.conda|Conda environments|Python" | |
| "$HOME/miniconda3|Miniconda installation|Python" | |
| "$HOME/anaconda3|Anaconda installation|Python" | |
| "$HOME/.local/share/virtualenvs|Pipenv virtualenvs|Python" | |
| # --- Homebrew --- | |
| "/opt/homebrew|Homebrew (Apple Silicon)|Homebrew" | |
| "/usr/local/Cellar|Homebrew (Intel)|Homebrew" | |
| # --- Xcode / Apple Dev --- | |
| "$HOME/Library/Developer/Xcode/DerivedData|Xcode build products and indexes|Xcode" | |
| "$HOME/Library/Developer/Xcode/iOS DeviceSupport|iOS debug symbols per version|Xcode" | |
| "$HOME/Library/Developer/Xcode/watchOS DeviceSupport|watchOS debug symbols|Xcode" | |
| "$HOME/Library/Developer/Xcode/tvOS DeviceSupport|tvOS debug symbols|Xcode" | |
| "$HOME/Library/Developer/Xcode/DocumentationCache|Xcode documentation cache|Xcode" | |
| "$HOME/Library/Developer/CoreSimulator|Simulator runtimes and data|Xcode" | |
| "$HOME/Library/Developer/XCPGDevices|Xcode Playground devices|Xcode" | |
| "$HOME/Library/Developer/XCTestDevices|Xcode test devices|Xcode" | |
| # --- Android --- | |
| "$HOME/.android/avd|Android Virtual Device images|Android" | |
| # --- IDEs & Editors --- | |
| "$HOME/Library/Application Support/JetBrains|JetBrains IDEs data|IDEs & Editors" | |
| "$HOME/.vscode/extensions|VS Code extensions|IDEs & Editors" | |
| "$HOME/Library/Application Support/Code|VS Code app data|IDEs & Editors" | |
| "$HOME/.cursor|Cursor editor data|IDEs & Editors" | |
| "$HOME/Library/Application Support/Cursor|Cursor app data|IDEs & Editors" | |
| "$HOME/.trae|Trae editor data|IDEs & Editors" | |
| # --- AI Tools --- | |
| "$HOME/.claude|Claude Code session data and caches|AI Tools" | |
| "$HOME/Library/Application Support/Claude|Claude Desktop app data|AI Tools" | |
| "$HOME/.gemini|Google Gemini CLI cache|AI Tools" | |
| "$HOME/.codeium|Codeium AI data|AI Tools" | |
| "$HOME/.continue|Continue AI extension data|AI Tools" | |
| # --- General Caches --- | |
| "$HOME/.cache|XDG cache dir (pip, pre-commit, Hugging Face, etc.)|General Caches" | |
| # --- Compiler Caches --- | |
| "$HOME/.ccache|ccache compilation cache|Compiler Caches" | |
| # --- IaC & Cloud --- | |
| "$HOME/.terraform.d|Terraform plugin cache|IaC & Cloud" | |
| "$HOME/Library/Application Support/sst|SST framework cache|IaC & Cloud" | |
| # --- Browsers (profile data, synced elsewhere) --- | |
| "$HOME/Library/Application Support/Google/Chrome|Chrome profile data|Browsers" | |
| "$HOME/Library/Application Support/BraveSoftware|Brave profile data|Browsers" | |
| "$HOME/Library/Application Support/Firefox|Firefox profile data|Browsers" | |
| "$HOME/Library/Application Support/Vivaldi|Vivaldi profile data|Browsers" | |
| # --- Misc --- | |
| "$HOME/.wine|Wine compatibility layer|Misc" | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Per-project dependency patterns (dirname|sentinel file) | |
| # --------------------------------------------------------------------------- | |
| DEP_PATTERNS=( | |
| "node_modules|package.json" | |
| ".venv|requirements.txt" | |
| ".venv|pyproject.toml" | |
| "venv|requirements.txt" | |
| "venv|pyproject.toml" | |
| "target|Cargo.toml" | |
| "target|pom.xml" | |
| "target|build.sbt" | |
| "vendor|composer.json" | |
| "vendor|Gemfile" | |
| "vendor|go.mod" | |
| "Pods|Podfile" | |
| "_build|mix.exs" | |
| "deps|mix.exs" | |
| ".build|Package.swift" | |
| ".build|mix.exs" | |
| ".terraform|main.tf" | |
| ".terragrunt-cache|terragrunt.hcl" | |
| "cdk.out|cdk.json" | |
| ".dart_tool|pubspec.yaml" | |
| ".packages|pubspec.yaml" | |
| ".stack-work|stack.yaml" | |
| ".parcel-cache|package.json" | |
| ".next|package.json" | |
| ".nuxt|package.json" | |
| ".turbo|package.json" | |
| "bower_components|bower.json" | |
| ".vagrant|Vagrantfile" | |
| ".gradle|build.gradle" | |
| ".gradle|build.gradle.kts" | |
| ".tox|tox.ini" | |
| ".nox|noxfile.py" | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Functions | |
| # --------------------------------------------------------------------------- | |
| print_header() { | |
| echo "" | |
| echo "${BOLD}${BLUE}=== $1 ===${NC}" | |
| } | |
| get_size() { | |
| local du_output | |
| du_output=$($DU -sh "$1" 2>/dev/null) || du_output="" | |
| local size="${du_output%% *}" | |
| size="${size## }" | |
| size="${size%% }" | |
| [[ -z "$size" ]] && size="?" | |
| echo "$size" | |
| } | |
| check_and_exclude() { | |
| local path="$1" | |
| local description="$2" | |
| local dry_run="$3" | |
| # Skip if path doesn't exist | |
| if [[ ! -e "$path" ]]; then | |
| return | |
| fi | |
| local size | |
| size=$(get_size "$path") | |
| # Check if already excluded | |
| local excluded | |
| excluded=$($TMUTIL isexcluded "$path" 2>&1) || excluded="" | |
| if [[ "$excluded" == *"[Excluded]"* ]]; then | |
| printf " ${YELLOW}skip${NC} %-8s %s ${YELLOW}(already excluded)${NC}\n" "$size" "$description" | |
| return | |
| fi | |
| if [[ "$dry_run" == "true" ]]; then | |
| printf " ${GREEN}todo${NC} %-8s %s\n" "$size" "$description" | |
| printf " %s\n" "$path" | |
| else | |
| if $TMUTIL addexclusion "$path"; then | |
| printf " ${GREEN}done${NC} %-8s %s\n" "$size" "$description" | |
| else | |
| printf " ${RED}fail${NC} %-8s %s\n" "$size" "$description" | |
| fi | |
| fi | |
| } | |
| install_asimeow() { | |
| local dry_run="$1" | |
| print_header "asimeow (automated per-project exclusions)" | |
| # Check if already installed | |
| if command -v asimeow &>/dev/null; then | |
| echo " ${YELLOW}skip${NC} asimeow is already installed" | |
| # Check if service is running | |
| if brew services list 2>/dev/null | grep -q "asimeow.*started"; then | |
| echo " ${YELLOW}skip${NC} asimeow service is already running" | |
| else | |
| if [[ "$dry_run" == "true" ]]; then | |
| echo " ${GREEN}todo${NC} Would start asimeow service (runs every 6 hours)" | |
| else | |
| echo " ${BLUE}....${NC} Starting asimeow service..." | |
| brew services start asimeow 2>/dev/null && \ | |
| echo " ${GREEN}done${NC} asimeow service started (runs every 6 hours)" || \ | |
| echo " ${RED}fail${NC} Could not start asimeow service" | |
| fi | |
| fi | |
| return | |
| fi | |
| # Check if asimov is installed (similar tool) | |
| if command -v asimov &>/dev/null; then | |
| echo " ${YELLOW}skip${NC} asimov (similar tool) is already installed" | |
| return | |
| fi | |
| # Check if brew is available | |
| if ! command -v brew &>/dev/null; then | |
| echo " ${RED}fail${NC} Homebrew not found — install it first: https://brew.sh" | |
| return | |
| fi | |
| if [[ "$dry_run" == "true" ]]; then | |
| echo " ${GREEN}todo${NC} Would install asimeow via Homebrew" | |
| echo " ${GREEN}todo${NC} Would start asimeow service (runs every 6 hours)" | |
| echo "" | |
| echo " asimeow auto-excludes per-project dependency dirs:" | |
| echo " node_modules, target/, .venv/, vendor/, Pods/, _build/," | |
| echo " .dart_tool/, .stack-work/, .terraform/, and more." | |
| echo " Configurable via ~/.config/asimeow/config.yaml" | |
| else | |
| echo " ${BLUE}....${NC} Installing asimeow..." | |
| if brew tap mdnmdn/asimeow 2>/dev/null && brew install asimeow 2>/dev/null; then | |
| echo " ${GREEN}done${NC} asimeow installed" | |
| echo " ${BLUE}....${NC} Running initial scan..." | |
| asimeow 2>/dev/null && \ | |
| echo " ${GREEN}done${NC} Initial scan complete" || \ | |
| echo " ${YELLOW}warn${NC} Initial scan had issues (non-fatal)" | |
| echo " ${BLUE}....${NC} Starting asimeow service..." | |
| brew services start asimeow 2>/dev/null && \ | |
| echo " ${GREEN}done${NC} asimeow service started (runs every 6 hours)" || \ | |
| echo " ${RED}fail${NC} Could not start service — run manually: asimeow" | |
| else | |
| echo " ${RED}fail${NC} Could not install asimeow" | |
| echo " Try manually: brew tap mdnmdn/asimeow && brew install asimeow" | |
| fi | |
| fi | |
| } | |
| # --------------------------------------------------------------------------- | |
| # Main | |
| # --------------------------------------------------------------------------- | |
| DRY_RUN="true" | |
| if [[ "${1:-}" == "--apply" ]]; then | |
| DRY_RUN="false" | |
| fi | |
| echo "" | |
| echo "${BOLD}Time Machine Developer Exclusions${NC}" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| if [[ "$DRY_RUN" == "true" ]]; then | |
| echo "${YELLOW}DRY RUN — showing what would happen. Run with --apply to apply.${NC}" | |
| else | |
| echo "${GREEN}Applying exclusions...${NC}" | |
| fi | |
| # ========================================================================= | |
| # Step 1: Global directory exclusions | |
| # ========================================================================= | |
| current_category="" | |
| for entry in "${EXCLUSIONS[@]}"; do | |
| IFS='|' read -r path description category <<< "$entry" | |
| if [[ "$category" != "$current_category" ]]; then | |
| print_header "$category" | |
| current_category="$category" | |
| fi | |
| check_and_exclude "$path" "$description" "$DRY_RUN" | |
| done | |
| # ========================================================================= | |
| # Step 2: Per-project dependency sweep (one-time) | |
| # ========================================================================= | |
| print_header "Per-Project Dependencies (one-time sweep)" | |
| dep_count=0 | |
| dep_limit=200 | |
| for pattern in "${DEP_PATTERNS[@]}"; do | |
| IFS='|' read -r dirname sentinel <<< "$pattern" | |
| while IFS= read -r dir; do | |
| parent="${dir%/*}" | |
| if [[ -f "$parent/$sentinel" ]]; then | |
| dep_count=$((dep_count + 1)) | |
| [[ $dep_count -gt $dep_limit ]] && break 2 | |
| check_and_exclude "$dir" "$dirname ($sentinel)" "$DRY_RUN" | |
| fi | |
| done < <($FIND "$HOME" -name "$dirname" -type d -prune \ | |
| -not -path "$HOME/Library/*" \ | |
| -not -path "$HOME/.Trash/*" \ | |
| -not -path "$HOME/.cache/*" \ | |
| -not -path "$HOME/.npm/*" \ | |
| -not -path "$HOME/.cargo/*" \ | |
| -not -path "$HOME/.nvm/*" \ | |
| -not -path "$HOME/.bun/*" \ | |
| -not -path "$HOME/.deno/*" \ | |
| 2>/dev/null) | |
| done | |
| if [[ $dep_count -gt $dep_limit ]]; then | |
| echo " ${YELLOW}...and more (capped at $dep_limit)${NC}" | |
| fi | |
| if [[ $dep_count -eq 0 ]]; then | |
| echo " ${YELLOW}none found${NC}" | |
| fi | |
| # ========================================================================= | |
| # Step 3: Install asimeow for ongoing coverage | |
| # ========================================================================= | |
| install_asimeow "$DRY_RUN" | |
| # ========================================================================= | |
| # Summary | |
| # ========================================================================= | |
| echo "" | |
| echo "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" | |
| if [[ "$DRY_RUN" == "true" ]]; then | |
| echo "${YELLOW}This was a dry run. To apply everything:${NC}" | |
| echo " ${BOLD}./tm-exclude-dev.sh --apply${NC}" | |
| else | |
| echo "${GREEN}All done!${NC}" | |
| echo "" | |
| echo "Verify any path with: $TMUTIL isexcluded <path>" | |
| echo "List all exclusions: sudo mdfind \"com_apple_backup_excludeItem = 'com.apple.backupd'\"" | |
| fi | |
| echo "" | |
| echo "Already excluded by macOS by default:" | |
| echo " ~/Library/Caches (+ all subdirs), ~/Library/Logs," | |
| echo " .Spotlight-V100, .Trashes, /private/var/vm, System volume" | |
| echo "" | |
| echo "Caution — Browsers: bookmarks & passwords are in the excluded" | |
| echo " profile dirs. Ensure browser sync is enabled before excluding." | |
| echo "" | |
| echo "Safe to re-run anytime — already-excluded paths are skipped." | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment