Backup & Restore
This document explains how to backup and restore your dotfiles and system configurations.
Overview
The backup scripts copy your dotfiles, credentials, configurations and a few important files to a timestamped directory (local or a mounted share). This ensures you can:
- Restore configurations after a system reinstall
- Keep timestamped snapshots of your configuration
- Migrate to a new computer
Backups are not encrypted. A backup holds
~/.ssh,~/.gnupg(plus an exported GPG private key),~/.aws,~/.kube,~/.docker,~/.npmrc,~/.config/dotfiles/secrets.env, rta's key file if you have one, and your Claude Code history. The scripts run withumask 077and set the backup directory to mode700, so other local users cannot read it, but nothing is encrypted. Keep it on storage only you can access, and encrypt it (see Encryption) before it leaves the machine: cloud-synced folders, a NAS, a git remote.
Backup Scripts
Both scripts copy files with rclone, which the base profile installs.
backup-osx.sh and backup-debian.sh are short entry points to backup/backup.sh, which does the backup for both systems. They only set the system, which decides two things: the home directories that -f adds and whether the Brave profile has a step of its own (macOS). Everything else, flags included, is the same on both. backup.sh can also be run directly: it picks the system from uname (Darwin is macOS, Linux is Debian/Ubuntu), and BACKUP_OS=osx or BACKUP_OS=debian forces one.
macOS: backup-osx.sh
Location: backup/backup-osx.sh
Purpose: Backup macOS-specific configurations and dotfiles
Usage:
./backup/backup-osx.sh [OPTIONS]Options:
-f Full backup: copy Desktop, dev, Documents, Downloads, Movies, Music and
Pictures from the home directory, instead of only ~/dev. For personal use.
-n Dry run: show what would be copied without copying anything (no directory
is created).
-o DIR Output directory (default: current directory). The backup itself is
written to DIR/backup_<timestamp>.
-x FILE Extra rclone exclude patterns, one per line, applied to every directory
copy (default: $BACKUP_EXCLUDE_FILE). See Extra excludes.
-h Display help messageAny other flag prints the help and exits with status 1 (there is no -d option: the output directory is -o).
Environment: BACKUP_GPG_EMAIL selects the GPG keys to export (default: the user.email of your git configuration). BACKUP_EXCLUDE_FILE is the default for -x.
Examples:
# Backup to default location (a backup_<timestamp> directory in the current directory)
./backup/backup-osx.sh
# Preview what a backup would copy, without copying anything
./backup/backup-osx.sh -n
# Backup to specific directory
./backup/backup-osx.sh -o ~/backups/dotfiles
# Full backup (Desktop, Documents, Downloads, ...) to an external drive
./backup/backup-osx.sh -f -o /Volumes/Backup/dotfiles
# Export the GPG key of another identity
BACKUP_GPG_EMAIL=you@example.com ./backup/backup-osx.sh -o ~/backups/dotfiles
# Backup to a mounted share (not encrypted: see Security Considerations)
./backup/backup-osx.sh -o ~/mnt/nas/dotfilesDebian/Ubuntu: backup-debian.sh
Location: backup/backup-debian.sh
Purpose: Backup Debian/Ubuntu-specific configurations and dotfiles
Usage:
./backup/backup-debian.sh [OPTIONS]Options:
-f Full backup: copy Desktop, dev, Documents, Downloads, Music, Pictures and
Videos from the home directory, instead of only ~/dev. For personal use.
-n Dry run: show what would be copied without copying anything (no directory
is created).
-o DIR Output directory (default: current directory). The backup itself is
written to DIR/backup_<timestamp>.
-x FILE Extra rclone exclude patterns, one per line, applied to every directory
copy (default: $BACKUP_EXCLUDE_FILE). See Extra excludes.
-h Display help messageAny other flag prints the help and exits with status 1. BACKUP_GPG_EMAIL and BACKUP_EXCLUDE_FILE work as on macOS.
Examples:
# Backup to default location
./backup/backup-debian.sh
# Preview what a backup would copy, without copying anything
./backup/backup-debian.sh -n
# Backup to specific directory
./backup/backup-debian.sh -o ~/backups/dotfiles
# Backup to mounted network share (not encrypted: see Security Considerations)
./backup/backup-debian.sh -o /mnt/backup/dotfilesExit status
| Status | Meaning |
|---|---|
0 | Everything was copied (with -n: listed) |
1 | Nothing was backed up: unknown flag, missing -x file or unsupported system |
2 | The backup was written but is incomplete: at least one copy or GPG export failed. The end of the output lists them |
A run that stops on an unexpected error, such as an output directory it cannot create, exits with the status of the command that failed, never 0. A command chained with && (like the rclone sync of Automated Backups) therefore only runs after a complete backup.
What Gets Backed Up
The scripts run six steps in this order. A source that does not exist is skipped silently. A copy or a GPG export that fails does not stop the run: the next steps still run, the failures are listed in a warning at the end, and the exit status is 2 (see Exit status).
1. System files
/etc/hostsand/etc/resolv.confgo toetc/
2. GPG key export
- The keys are chosen by
BACKUP_GPG_EMAIL(default: the gituser.email): every secret key with a user ID that matches it is exported, so an older key you keep to decrypt old data is saved along with the current one - Each key is written as
public-key-<id>.ascandprivate-key-<id>.asc(<id>is the 16-character long key ID) togpg/in the backup, mode600. Nothing is written to~/.gnupg, so no extra copy of the private key stays on the machine - Each file is written to a temporary name first and renamed once gpg succeeded. An export that fails, typically because gpg cannot ask for the key's passphrase in an unattended run, leaves no empty or partial file: it is listed as a failure, the run goes on and exits with
2. The keyring itself is still copied with~/.gnupgin the next step, and earlier backups keep their own export - With no email, no matching secret key or no
gpgcommand, the step says so and is skipped
3. Dotfiles
Each entry goes to dotfiles/<basename>:
.aws- AWS CLI configuration (cli/cacheandsso/cacheexcluded).claude- Claude Code settings, instructions, agents, skills, memory and conversation history (projects/). Caches, shell snapshots, file history, backups, IDE state, plugin installs and sessions are excluded.config- everything under~/.config: gh, gh-dash, lazygit, nvim, mise, cheat,dotfiles/(functions andsecrets.env), gcloud and any other tool that keeps its configuration there, including the VS Code user settings on Linux. Cache and log directories (Cache,Caches,cache,GPUCache,Code Cache,DawnCache,logs,Logs) are excluded.copilot- Copilot CLI configuration (pkg,ideandsession-stateexcluded).docker- Docker client configuration (scout,sbom,scan,desktop,buildx,mutagenandrunexcluded).gitconfig.gitconfig.local- your git identity and signing settings for this machine (the repository does not ship it).gnupg- the GPG keyring.kube- Kubernetes configuration (cacheandhttp-cacheexcluded).mc- MinIO client configuration.npmrc.ssh- SSH keys and configuration.tsh- Teleport profiles.zshrc.zshrc.local- your machine-specific shell configuration (the repository does not ship it)
To keep one of them out of your backups, remove it from the DOT_FILES list in backup/backup.sh.
4. rta secrets store
- Only when an rta store exists (
$RTA_DATA_DIR, else$XDG_DATA_HOME/rta, else~/.local/share/rta):kv.age, the encrypted store, andkv.recipients, the public keys that can open it, go torta/. Nothing else from rta's data directory is copied (grants, audit log and plugin cache can be rebuilt) - The keys that open the store are in the backup too, copied in step 3, and how safe that is depends on which key you use:
~/.config/rta/kv.identity, the key thatrta kv init --generatecreates, has no passphrase. It is copied with~/.configon purpose, so that a restore can open the store, but it also means that whoever holds the backup can read every secret in the store. Keep the backup private (it is not encrypted)- An SSH key that has a passphrase, copied with
~/.ssh, cannot open the store without that passphrase. Locking the store to such keys, and movingkv.identityaway, makes the backup alone useless for reading the store: see the Passphrase and A daily key and a backup key paragraphs of Keeping secrets in rta. Backups made before that change still hold akv.agethat the old key opens: delete them once a new backup exists
5. Home directories
Each directory goes to home/<name>:
- macOS:
~/devby default. With-f:~/Desktop,~/dev,~/Documents,~/Downloads,~/Movies,~/Musicand~/Pictures - Debian:
~/devby default. With-f:~/Desktop,~/dev,~/Documents,~/Downloads,~/Music,~/Picturesand~/Videos
6. Brave (macOS only)
- If the
Defaultprofile exists (~/Library/Application Support/BraveSoftware/Brave-Browser/Default), it is copied tobrave/Default(Cache,Code Cache,GPUCache,DawnCache,Crashpadand the service worker cachesService Worker/CacheStorageandService Worker/ScriptCacheexcluded) - On Debian/Ubuntu Brave keeps its profiles in
~/.config/BraveSoftware, which step 3 copies with the rest of~/.config(and its exclusions)
Always excluded
Every directory copy skips OS metadata (.DS_Store, Thumbs.db), *.tmp, *.log, earlier backups (backup_*, so a backup written under a source directory never copies itself), .cache, .local/share/mise and regenerable project output: node_modules, __pycache__, *.pyc, .venv, venv, .pytest_cache, .mypy_cache, .ruff_cache, .tox, .turbo, .angular, .svelte-kit, .output, .terraform, .terragrunt-cache, target, dist, build, .next, .nuxt, .gradle, .parcel-cache and coverage. These patterns have no leading /, so they match at any depth: a project folder named build or dist is skipped too. Symbolic links are followed (their target is copied).
Extra excludes
Anything specific to your machine, such as local CI runner state or a scratch directory, belongs in a file of your own rather than in the script. Pass it with -x or set BACKUP_EXCLUDE_FILE. It holds one rclone pattern per line (# starts a comment) and applies to every directory copy:
# local act runner state: a registry cache and old artifacts
ci/act/docker/**
ci/act/artifacts/**./backup/backup-osx.sh -x ~/.config/dotfiles/backup-exclude.txt -o /Volumes/Backup/dotfilesThe script exits with status 1 when the file does not exist. A dry run (-n) shows what is left to copy.
Speed
The copies already run 12 files at a time with 24 checkers. In a local-disk test, raising those numbers or skipping checksums changed nothing, and rclone was about twice as fast as rsync or tar. What shortens a backup is copying fewer files: a dry run (-n) lists them, and the patterns above and your exclude file are the lever. A slow destination, such as a USB drive or a network share, sets the limit from there.
Not backed up
- Package lists. The scripts do not record installed packages (see Package Restore)
~/.bashrc,~/.profileand every shell file other than~/.zshrcand~/.zshrc.local~/.oh-my-zsh(the theme and plugins are reinstalled by the setup)~/.continue(including a.envyou may keep there) and, on macOS, the VS Code user directory~/Library/Application Support/Code/User- Anything under
/etcother than the two files above, and anything outside the lists above
Backup Structure
Each run creates one directory, backup_<YYYY-MM-DDTHH-MM-SS>, inside the output directory (-o, default: the current directory). Example on macOS:
backup_2026-10-10T13-26-17/
├── etc/
│ ├── hosts
│ └── resolv.conf
├── gpg/ (exported GPG keys)
│ ├── private-key-<id>.asc
│ └── public-key-<id>.asc
├── dotfiles/
│ ├── .aws/
│ ├── .claude/
│ ├── .config/
│ │ ├── dotfiles/ (functions, secrets.env)
│ │ ├── gh/
│ │ ├── lazygit/
│ │ ├── mise/
│ │ ├── nvim/
│ │ ├── rta/ (kv.identity, if you have one)
│ │ └── ...
│ ├── .copilot/
│ ├── .docker/
│ ├── .gitconfig
│ ├── .gitconfig.local
│ ├── .gnupg/
│ ├── .kube/
│ ├── .mc/
│ ├── .npmrc
│ ├── .ssh/
│ ├── .tsh/
│ ├── .zshrc
│ └── .zshrc.local
├── rta/ (only when an rta store exists)
│ ├── kv.age
│ └── kv.recipients
├── home/
│ └── dev/ (with -f: also Desktop, Documents, ...)
└── brave/ (macOS only, when Brave is installed)
└── Default/Entries only exist when their source exists on the machine. When the run ends, the script prints the location and size of the backup and reminds you that it is not encrypted. It says Backup incomplete. instead of Backup complete. when something failed, and lists what.
Backup Best Practices
Regular Backups
# Weekly backup to a local directory
./backup/backup-osx.sh -o ~/backups/dotfiles
# Use cron for automation (macOS/Linux)
# Edit crontab
crontab -e
# Add weekly backup (Sunday at 2 AM)
0 2 * * 0 /path/to/dotfiles/backup/backup-osx.sh -o ~/backups/dotfilesVersion Control
Do not commit a backup to a git repository, not even a private one: it holds private keys and tokens in clear text. Your configuration is already versioned by this repository (dotfiles/); backups are for what the repository deliberately does not contain.
Multiple Destinations
# Local backup
./backup/backup-osx.sh -o ~/backups/dotfiles
# Network backup
./backup/backup-osx.sh -o /Volumes/NAS/backups/dotfiles
# External drive
./backup/backup-osx.sh -o /Volumes/Backup/dotfiles
# Cloud-synced folder (Dropbox, ...): the backup is NOT encrypted and would be
# uploaded in clear text. Only do this with the encrypted archive from the
# Encryption section, never with the raw backup directory.
./backup/backup-osx.sh -o ~/Dropbox/dotfiles-backupEncryption
The scripts do not encrypt anything. Before a backup is stored anywhere you do not fully control (cloud storage, a shared NAS, a server), encrypt it:
# Backup first, to a local private directory
./backup/backup-osx.sh -o ~/backups/dotfiles
# The backup to protect (printed as "location" at the end of the run)
BACKUP=$(ls -d ~/backups/dotfiles/backup_* | tail -1)
# Encrypt with gpg (symmetric, passphrase)
tar czf - -C "$(dirname "$BACKUP")" "$(basename "$BACKUP")" | gpg -c > "$BACKUP.tar.gz.gpg"
# Or use age
tar czf - -C "$(dirname "$BACKUP")" "$(basename "$BACKUP")" | age -p > "$BACKUP.tar.gz.age"
# Upload only the encrypted archive
rclone copy "$BACKUP.tar.gz.gpg" remote:backups/To read it back: gpg -d <file>.tar.gz.gpg | tar xzf - -C ~/restored-dotfiles. An rclone crypt remote is another option: it encrypts file names and contents on the fly (see Using rclone). Once the archive is stored safely, delete the clear-text backup directory.
Restoring from Backup
In the commands below, BACKUP is the backup directory to restore from (the "location" printed at the end of the run):
BACKUP=~/backups/dotfiles/backup_2026-10-10T13-26-17Full Restore
- Clone dotfiles repository:
git clone https://github.com/this-is-tobi/dotfiles.git
cd dotfiles- Restore your local files before the setup. It never overwrites
~/.gitconfig.localor~/.zshrc.local, and when~/.gitconfig.localis already there it does not ask for your git identity:
for f in .gitconfig.local .zshrc.local; do
if [ -f "$BACKUP/dotfiles/$f" ]; then cp -p "$BACKUP/dotfiles/$f" ~/"$f"; fi
doneWhen the backup has no .gitconfig.local, the setup asks for your name and email. To set them yourself:
git config --file ~/.gitconfig.local user.name "Your Name"
git config --file ~/.gitconfig.local user.email "you@example.com"- Run setup script (profiles and dotfiles need the cloned repository):
./setup/setup-osx.sh -p 'base,devops,js' -d # Your profilesWith -d the setup installs the repository's dotfiles. A file it replaces is first saved under ~/.dotfiles-backup/<timestamp>/.
- Restore credentials and tool state (the repository does not ship them):
for item in .ssh .gnupg .aws .kube .docker .mc .tsh; do
[ -d "$BACKUP/dotfiles/$item" ] || continue
mkdir -p ~/"$item"
cp -Rp "$BACKUP/dotfiles/$item/." ~/"$item"/
done
[ -f "$BACKUP/dotfiles/.npmrc" ] && cp -p "$BACKUP/dotfiles/.npmrc" ~/.npmrc
# Claude Code conversation history (optional)
[ -d "$BACKUP/dotfiles/.claude/projects" ] && mkdir -p ~/.claude/projects \
&& cp -Rp "$BACKUP/dotfiles/.claude/projects/." ~/.claude/projects/- Restore your secrets file:
mkdir -p ~/.config/dotfiles
cp -p "$BACKUP/dotfiles/.config/dotfiles/secrets.env" ~/.config/dotfiles/secrets.env
chmod 600 ~/.config/dotfiles/secrets.envThe setup already installed the repository's ~/.config. Copy back from $BACKUP/dotfiles/.config/ only what you customized (for example gh or lazygit); copying the whole directory would replace the managed files, such as env.sh, with older versions.
- Restore the rta store (only if you use one):
RTA_DATA="${RTA_DATA_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/rta}"
mkdir -p "$RTA_DATA" && chmod 700 "$RTA_DATA"
cp -p "$BACKUP/rta/kv.age" "$BACKUP/rta/kv.recipients" "$RTA_DATA/"Then make sure what opens it is back: your SSH key, restored with ~/.ssh in step 4 (and RTA_KV_IDENTITY if you set it in the ~/.zshrc.local of step 2), or ~/.config/rta/kv.identity, which you copy from $BACKUP/dotfiles/.config/rta/ (mode 600). rta kv list then shows the keys, names only.
Check permissions:
~/.ssh,~/.gnupg, the private keys and~/.kube/configmust not be readable by others, see Permissions.Reload shell:
exec zshSelective Restore
Restore only specific configurations:
# Just shell config (the setup's -d would otherwise install the repository version)
cp "$BACKUP/dotfiles/.zshrc" ~/
# Just your machine-specific shell config and git identity
cp -p "$BACKUP/dotfiles/.zshrc.local" "$BACKUP/dotfiles/.gitconfig.local" ~/
# Just Git config
cp "$BACKUP/dotfiles/.gitconfig" ~/
# Just SSH
mkdir -p ~/.ssh && cp -Rp "$BACKUP/dotfiles/.ssh/." ~/.ssh/ && chmod 700 ~/.ssh
# Just Kubernetes config
mkdir -p ~/.kube
cp -p "$BACKUP/dotfiles/.kube/config" ~/.kube/config
chmod 600 ~/.kube/config
# Your projects
mkdir -p ~/dev && cp -Rp "$BACKUP/home/dev/." ~/dev/
# Brave profile on macOS (quit Brave first)
mkdir -p "$HOME/Library/Application Support/BraveSoftware/Brave-Browser/Default"
cp -Rp "$BACKUP/brave/Default/." "$HOME/Library/Application Support/BraveSoftware/Brave-Browser/Default/"Projects were backed up without node_modules, .venv, dist, build and the other regenerable output listed above: reinstall dependencies after restoring.
GPG Key
~/.gnupg is restored with the credentials above. To import the exported keys on their own:
gpg --import "$BACKUP"/gpg/private-key-*.asc
gpg --list-secret-keysIn a backup made by an earlier version of the scripts, the export is under dotfiles/.gnupg/backup/ instead of gpg/.
Package Restore
The backup contains no package lists. Reinstall the toolset by re-running the setup, which installs everything defined by the profiles:
./setup/setup-osx.sh -p 'base,devops,js' # macOS
./setup/setup-debian.sh -p 'base,devops,js' # Debian/Ubuntumise tools:
# The backed-up ~/.config/mise/config.toml holds the versions pinned with
# `mise use --global`, so restoring it and running install is enough:
mkdir -p ~/.config/mise
cp -p "$BACKUP/dotfiles/.config/mise/config.toml" ~/.config/mise/
mise installAnything you installed by hand is not recorded anywhere. If you want a list before wiping a machine, capture it yourself, for example:
brew bundle dump --file ~/Brewfile # macOS
apt-mark showmanual > ~/apt-manual.txt # Debian/UbuntuRemote Backups
The backup is not encrypted. Everything below moves it to another machine or provider: encrypt first (see Encryption) or use an encrypted rclone remote.
Using rclone
Setup rclone for cloud storage:
# Configure rclone (first time): create a remote for your provider, then a
# "crypt" remote on top of it (called "secret" below) so that names and
# contents are encrypted before they are uploaded
rclone config
# Backup locally, then push through the encrypted remote
./backup/backup-osx.sh -o ~/backups/dotfiles
rclone sync ~/backups/dotfiles secret:dotfiles-backup
# Restore from cloud
rclone sync secret:dotfiles-backup ~/restored-dotfilesSupported providers:
- Google Drive
- Dropbox
- OneDrive
- AWS S3
- Backblaze B2
- And many more
Using rsync over SSH
Backup to remote server:
# Initial backup
./backup/backup-osx.sh -o ~/backups/dotfiles
rsync -avz ~/backups/dotfiles/ user@server:/backups/dotfiles/
# Restore from remote
rsync -avz user@server:/backups/dotfiles/ ~/restored-dotfiles/The server then holds your keys and tokens in clear text: use a server (and disk) you trust as much as this machine, or sync an encrypted archive instead.
Using Git
Do not push a backup directory to a git remote (see Version Control). If you want history in git, commit only an encrypted archive.
Automated Backups
The GPG export may ask for the key's passphrase. In an unattended run (cron, launchd, systemd) gpg usually cannot ask, so the export only works when the key has no passphrase or gpg-agent still has it cached. Otherwise the export fails, the rest of the backup is still written (the keyring itself is in dotfiles/.gnupg) and the run exits with status 2: a chained && rclone sync does not push that backup, and a systemd service is marked as failed. With a passphrase-protected key, an unattended backup is therefore usually reported incomplete: check the warning and push it by hand, or run the backup yourself when you want the export.
cron (macOS/Linux)
# Edit crontab
crontab -e
# Daily backup at 2 AM
0 2 * * * /Users/you/dotfiles/backup/backup-osx.sh -o ~/backups/dotfiles
# Weekly backup on Sunday at 3 AM, pushed through an encrypted rclone remote
# (only when the backup is complete: the script exits with 2 otherwise)
0 3 * * 0 /Users/you/dotfiles/backup/backup-osx.sh -o ~/backups/dotfiles && rclone sync ~/backups/dotfiles secret:dotfileslaunchd (macOS)
Create ~/Library/LaunchAgents/com.user.dotfiles-backup.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.user.dotfiles-backup</string>
<key>ProgramArguments</key>
<array>
<string>/Users/you/dotfiles/backup/backup-osx.sh</string>
<string>-o</string>
<string>/Users/you/backups/dotfiles</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>2</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
</dict>
</plist>Load the agent:
launchctl load ~/Library/LaunchAgents/com.user.dotfiles-backup.plistsystemd timer (Linux)
Create /etc/systemd/system/dotfiles-backup.service:
[Unit]
Description=Dotfiles Backup
[Service]
Type=oneshot
User=you
ExecStart=/home/you/dotfiles/backup/backup-debian.sh -o /home/you/backups/dotfilesCreate /etc/systemd/system/dotfiles-backup.timer:
[Unit]
Description=Dotfiles Backup Timer
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.targetEnable the timer:
sudo systemctl enable --now dotfiles-backup.timerBackup Strategies
3-2-1 Rule
Keep:
- 3 copies of your data
- On 2 different storage types
- With 1 copy offsite
Example:
- Original files on your computer
- Local backup on external drive
- Encrypted cloud backup (an encrypted rclone remote, or an archive encrypted with gpg or age)
Incremental Backups
Every run writes its own backup_<timestamp> directory, so snapshots accumulate in the output directory. Prune the old ones:
# Create a snapshot
./backup/backup-osx.sh -o ~/backups/dotfiles
# Keep the last 7 days, delete older snapshots
find ~/backups/dotfiles -maxdepth 1 -name 'backup_*' -mtime +7 -exec rm -rf {} +Security Considerations
Sensitive Data
A backup contains:
- SSH private keys (
.ssh), the GPG keyring (.gnupg) and the exported GPG private keys (gpg/) - Cloud and cluster credentials (
.aws,.kube,.tsh,.docker,.mc,.npmrc) - Everything in
~/.config, including~/.config/dotfiles/secrets.envand rta'skv.identity, which opens the rta store copied next to it unless you locked the store to passphrase-protected keys (see rta secrets store) ~/.zshrc.local, with whatever you export there- Claude Code settings and conversation history, and Copilot CLI configuration
The scripts only keep other local users out (umask 077, backup directory mode 700). They do not encrypt.
Best practices:
- Keep backups on storage only you can access, and encrypt them before they go to a cloud folder, a NAS or a server
- Remove entries you do not want from the
DOT_FILESlist ofbackup/backup.sh - Delete backups you no longer need: they hold the keys and tokens of their day
- The GPG export is written only into the backup. A
~/.gnupg/backup/directory left by earlier versions of the scripts is still copied with~/.gnupg: delete it once a backup holdsgpg/ - Use secret management tools (1Password, Vault, the rta store) rather than long-lived tokens in files
Permissions
Ensure proper file permissions, especially after a restore:
# SSH keys
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_*
chmod 644 ~/.ssh/*.pub
# Kubernetes config
chmod 600 ~/.kube/config
# GPG keys
chmod 700 ~/.gnupg
# Local secrets
chmod 600 ~/.config/dotfiles/secrets.envTroubleshooting
Backup Fails
Check the flags:
# An unknown flag (for example -d) prints the help and exits 1: the output directory is -o
./backup/backup-osx.sh -o ~/backups/dotfiles
# Preview without copying anything
./backup/backup-osx.sh -n -o ~/backups/dotfilesCheck permissions:
# Ensure the scripts are executable
chmod +x backup/*.sh
# Check destination is writable
mkdir -p ~/backups && touch ~/backups/test && rm ~/backups/testCheck disk space:
# macOS
df -h ~/backups
# Linux
df -h ~/backupsCheck the final summary: an item that could not be copied (for example a broken symlink or an unreadable file) or a GPG export that failed does not stop the run. It is listed in a warning at the end of the output, which then says Backup incomplete., and the exit status is 2 (see Exit status).
Restore Fails
Verify backup integrity:
# Check backup directory exists
ls -la "$BACKUP"
# Verify files are present
find "$BACKUP" -type f | wc -lPermissions issues:
# Fix ownership
sudo chown -R $USER:$USER ~/restored-dotfiles
# Fix permissions
chmod -R u+rw ~/restored-dotfiles