# sgard — Shimmering Clarity Gardener A dotfiles manager that checkpoints files into a portable repository and restores them on demand. The repository is a single directory that can live anywhere — local disk, USB drive, NFS mount — making it portable between machines. ## Installation Homebrew: ``` brew tap kisom/homebrew-tap brew install sgard ``` From source: ``` git clone https://github.com/kisom/sgard && cd sgard go build ./cmd/sgard ./cmd/sgardd ``` Or install into `$GOBIN`: ``` go install github.com/kisom/sgard/cmd/sgard@latest go install github.com/kisom/sgard/cmd/sgardd@latest ``` NixOS (flake): ``` nix profile install github:kisom/sgard ``` Or add to your flake inputs and include `sgard.packages.${system}.default` in your packages. Binaries are also available on the [releases page](https://github.com/kisom/sgard/releases). ### Shell completion ```sh # Bash (add to ~/.bashrc) source <(sgard completion bash) # Zsh (add to ~/.zshrc) source <(sgard completion zsh) # Fish sgard completion fish | source # To load on startup: sgard completion fish > ~/.config/fish/completions/sgard.fish ``` ## Quick start ```sh # Initialize a repo (default: ~/.sgard) sgard init # Track some dotfiles sgard add ~/.bashrc ~/.gitconfig ~/.ssh/config # Checkpoint current state sgard checkpoint -m "initial" # Check what's changed sgard status # Restore from the repo sgard restore ``` Use `--repo` to put the repository somewhere else, like a USB drive: ```sh sgard init --repo /mnt/usb/dotfiles sgard add ~/.bashrc --repo /mnt/usb/dotfiles sgard restore --repo /mnt/usb/dotfiles ``` ### Locked files Some files get overwritten by the system (desktop environments, package managers, etc.) but you want to keep them at a known-good state. Locked files are repo-authoritative — `restore` always overwrites them, and `checkpoint` never picks up the system's changes: ```sh # XDG user-dirs.dirs gets reset by the desktop environment on login sgard add --lock ~/.config/user-dirs.dirs # The system overwrites it — status reports "drifted", not "modified" sgard status # drifted ~/.config/user-dirs.dirs # Restore puts it back without prompting sgard restore ``` Use `add` (without `--lock`) when you intentionally want to update the repo with a new version of a locked file. ### Directory-only entries Sometimes a directory must exist for software to work, but its contents are managed elsewhere. `--dir` tracks the directory itself without recursing: ```sh # Ensure ~/.local/share/applications exists (some apps break without it) sgard add --dir ~/.local/share/applications ``` On `restore`, sgard creates the directory with the correct permissions but doesn't touch its contents. ### Per-machine targeting Some files only apply to certain machines. Use `--only` and `--never` to control where entries are active: ```sh # Only restore on Linux sgard add --only os:linux ~/.bashrc.linux # Never restore on ARM sgard add --never arch:arm64 ~/.config/heavy-tool # Only on machines tagged "work" sgard tag add work sgard add --only tag:work ~/.ssh/work-config # Only on a specific host sgard add --only vade ~/.special-config # See this machine's identity sgard identity # Change targeting on an existing entry sgard target ~/.bashrc.linux --only os:linux,tag:desktop sgard target ~/.bashrc.linux --clear ``` Labels: bare string = hostname, `os:linux`/`os:darwin`, `arch:amd64`/`arch:arm64`, `tag:` from local `/tags` file. `checkpoint`, `restore`, and `status` skip non-matching entries automatically. ## Commands ### Local | Command | Description | |---|---| | `init` | Create a new repository | | `add ...` | Track files, directories (recursed), or symlinks | | `add --lock ...` | Track as locked (repo-authoritative, auto-restores on drift) | | `add --dir ` | Track directory itself without recursing into contents | | `add --only ` | Track with per-machine targeting (only on matching) | | `add --never ` | Track with per-machine targeting (never on matching) | | `target --only/--never/--clear` | Set or clear targeting on existing entry | | `tag add/remove/list` | Manage machine-local tags | | `identity` | Show this machine's identity labels | | `remove ...` | Stop tracking files | | `checkpoint [-m msg]` | Re-hash tracked files and update the manifest | | `restore [path...] [-f]` | Restore files to their original locations | | `status` | Show which tracked files have changed | | `diff ` | Show content diff between stored and current file | | `list` | List all tracked files | | `verify` | Check blob store integrity against manifest hashes | | `prune` | Remove orphaned blobs not referenced by the manifest | | `mirror up ` | Sync filesystem → manifest (add new, remove deleted) | | `mirror down [-f]` | Sync manifest → filesystem (restore + delete untracked) | | `version` | Print the version | ### Encryption | Command | Description | |---|---| | `encrypt init` | Set up encryption (creates DEK + passphrase slot) | | `encrypt add-fido2 [--label]` | Add a FIDO2 KEK slot | | `encrypt remove-slot ` | Remove a KEK slot | | `encrypt list-slots` | List all KEK slots | | `encrypt change-passphrase` | Change the passphrase | | `encrypt rotate-dek` | Generate new DEK and re-encrypt all encrypted blobs | | `add --encrypt ...` | Track files with encryption | ### Remote sync | Command | Description | |---|---| | `push` | Push checkpoint to remote gRPC server | | `pull` | Pull checkpoint from remote gRPC server | | `prune` | With `--remote`, prunes orphaned blobs on the server | Remote commands require `--remote host:port` (or `SGARD_REMOTE` env, or a `/remote` config file) and authenticate via SSH keys. The server daemon `sgardd` is a separate binary (included in releases and Nix builds). ## Remote sync Start the daemon on your server: ```sh sgard init --repo /srv/sgard sgardd --authorized-keys ~/.ssh/authorized_keys ``` Push and pull from client machines: ```sh sgard push --remote myserver:9473 sgard pull --remote myserver:9473 ``` Authentication uses your existing SSH keys (ssh-agent, `~/.ssh/id_ed25519`, or `--ssh-key`). No passwords or certificates to manage. ### TLS To encrypt the connection with TLS: ```sh # Server: provide cert and key sgardd --tls-cert server.crt --tls-key server.key --authorized-keys ~/.ssh/authorized_keys # Client: enable TLS (uses system CA pool) sgard push --remote myserver:9473 --tls # Client: with a custom/self-signed CA sgard push --remote myserver:9473 --tls --tls-ca ca.crt ``` Without `--tls-cert`/`--tls-key`, sgardd runs without TLS (suitable for localhost or trusted networks). ## Encryption Sensitive files can be encrypted individually: ```sh # Set up encryption (once per repo) sgard encrypt init # Add encrypted files sgard add --encrypt ~/.ssh/config ~/.aws/credentials # Plaintext files work as before sgard add ~/.bashrc ``` Encrypted blobs use XChaCha20-Poly1305. The data encryption key (DEK) is wrapped by a passphrase-derived key (Argon2id). FIDO2 hardware keys are also supported as an alternative KEK source — sgard tries FIDO2 first and falls back to passphrase automatically. ### FIDO2 hardware keys Build with `-tags fido2` (requires libfido2) to enable real hardware key support, or use `nix build .#sgard-fido2`: ```sh # Register a FIDO2 key (touch required) sgard encrypt add-fido2 # With a PIN-protected device sgard encrypt add-fido2 --fido2-pin 1234 # Unlock is automatic — FIDO2 is tried first, passphrase as fallback sgard restore # touch your key when prompted ``` The encryption config (wrapped DEKs, salts) lives in the manifest, so it syncs with push/pull. The server never has the DEK. See [ARCHITECTURE.md](ARCHITECTURE.md) for the full encryption design. ## How it works sgard stores files in a content-addressable blob store keyed by SHA-256. A YAML manifest tracks each file's original path, hash, type, permissions, and timestamp. ``` ~/.sgard/ manifest.yaml # human-readable manifest blobs/ a1/b2/a1b2c3d4... # file contents stored by hash ``` On `restore`, sgard compares the manifest timestamp against the file's mtime. If the manifest is newer, the file is restored without prompting. Otherwise, sgard asks for confirmation (`--force` skips the prompt). Paths under `$HOME` are stored as `~/...` in the manifest, making it portable across machines with different usernames. Adding a directory recursively tracks all files and symlinks inside. See [ARCHITECTURE.md](ARCHITECTURE.md) for full design details.