Install · Init · Config · Backup · Restore
demo.webm
Needs bash, git, rsync, tree, tput, sed, GNU coreutils, and gum
If gum is missing, dotkeep offers to install it: pacman -S gum when pacman exists, otherwise a GitHub release binary into ~/.local/bin
git clone https://github.com/metaory/dotkeep
cd dotkeepLayout: dotkeep is the control script (vars, flags, dispatch). Logic lives under lib/ (gum, ui, help, path, conf, size, git, tally, sync).
Then put it on PATH:
# a dir you already have on PATH
ln -s "$(realpath dotkeep)" /your/path/dir/dotkeep
# or we handle it
sudo ln -s "$(realpath dotkeep)" /usr/local/bin/dotkeepdotkeep <command> [--dry-run]
init [DIR] create state repo (home/ root/ + .dotkeep.conf + git)
config show .dotkeep.conf, offer to edit
check validate + resolve paths
backup copy live files into the state repo
restore copy state repo files onto the live system
help
# on a new empty directory
dotkeep init [DIR]Creates your state repo:
state/
├── .dotkeep.conf
├── .gitignore
├── home/
└── root/
mkdirhome/,root/- empty
.dotkeep.conf - a default
.gitignore git init
Remote is optional. Add one yourself when you want push/pull:
cd ~/state
git remote add origin <url>Tip
Or skip init:
mkdir a dir and write .dotkeep.conf yourself
Next: cd into that dir, dotkeep config, then dotkeep backup
dotkeep init ~/state
cd ~/state
dotkeep configdotkeep configShows the full path to .dotkeep.conf and its contents
Then asks to open it with $EDITOR, else nvim, vim, vi
Note
Plain file. One path per line. # comments
Every entry must start with home/ or root/
Caution
No ~, no absolute / forms, no $VAR expansion
Prefer explicit files over whole dirs
Unreadable files are skipped The rest of the dir is copied
Backup may chmod u+r on unreadable files you own first
If both a dir and paths under it are listed the dir wins and children are dropped
Important
Live source symlinks are skipped on backup (leaf)
Restore may replace a live symlink with a regular copy from the repo
devices, fifos, and sockets are not synced
Nested .git and node_modules are stripped (content only, not repo metadata)
Directory sync respects the state dir .gitignore
Preview/check compare repo ↔ live FS content (rsync -c), not git status
Notes look like +2 ~1 -0 (added / changed / deleted); dirty → diff; identical → same
Copy dotkeep.conf.sample or start from dotkeep init:
# shell / editor
home/.zshrc
home/.config/nvim
home/.config/tmux/tmux.conf
# git / terminal
home/.gitconfig
home/.config/starship.toml
home/.config/alacritty/alacritty.toml
# prefer files over whole dirs
home/.config/foo/config.toml
home/.config/foo/themes
# optional system paths
root/etc/hostshome/.zshrc is $HOME/.zshrc
root/etc/hosts is /etc/hosts
One file. .dotkeep.conf is the source of truth
One path per line, home/ or root/
Edit that file to add or drop a path
State tree. The backup dir looks like the live system:
home/.zshrc
home/.config/nvim
root/etc/hosts
Same names. Same nesting. Home and root in one list
Copies. backup and restore rsync both ways. Live paths stay regular files
Ask first. Both commands ask before writing (default no). Optional path pick via gum choose (default no = all ok). Restore parks live targets under /tmp/dotkeep.XXXXXX/ first. Missing parent dirs on the live side are created (mkdir -p). A bad path is skipped. The rest is copied
Git optional. The store is that file tree. After backup it asks to add, commit, and push (each opt-in, default no). Syncthing or a disk copy can hold the same tree
bare git, yadm. Store is $HOME. Live files are the work tree. git add -A can commit SSH keys and caches. /etc does not fit
Stow, rcm, homeshick, dotbot. Store is the repo. Live path is a symlink. An editor that writes a tempfile and renames it over the path replaces the link. The repo keeps the old file
chezmoi, dotdrop. Store is the repo. Live path is a copy. Names are dot_zshrc and templates. Use them for per-host files or encrypted secrets
dotkeep. Store is home/ and root/ in a separate dir. Live path is a copy. Names match the disk
Platform. Linux, bash 5, git, rsync, gum, GNU coreutils. No Windows
Root. The tool does not call sudo. Restoring root/ needs write access to that path
Skip. Live source symlinks on backup. Files over 10 MiB (DOTKEEP_MAX, cap 100). Restore may replace a live symlink with a copy.
Preview. Content delta vs live FS (rsync -c), not git. Dirty → diff with +N ~N -N; identical → same
Out of scope. Templates, encryption, per-host source
config / check / backup / restore run from your state dir
(the one with .dotkeep.conf). Not the tool install. Any path, any remote
dotkeep backupRun from your state dir. Flow:
- Git prep (optional; skipped if no
.git)fetchorigin if configured (fetch failure → continue local)- show status
- if behind: ask
pull --rebase(stash first if dirty). Default no
- Show manifest + preview (
machine → state); content delta notes; identical paths dropped - Optional pick a subset (no keeps all) via
gum choose(multi-select:xtoggles, enter confirms; all start selected) - Ask before write. Default no → abort
- Copy each listed path into
home//root/ - Prune ignored paths under
home//root/viagit clean -X(if git) - Rewrite
README.mdwith atreesnapshot of the state repo - Git ship (optional; only if
.gitand the tree is dirty)- ask
git add -A(default no) - ask commit message (default
snapshot YYYY-MM-DD HH:MM) - ask
git pushiforiginexists (default no; warn and skip if no remote)
- ask
- Show short status + last 5 commits (if git)
Important
Pull, add, commit, and push are all opt-in. Default is no. Git itself is optional. No remote means no push prompt. A disk copy or Syncthing can hold the same tree.
Warning
Files over 10 MiB skipped and warned
DOTKEEP_MAX (MiB) default 10, max 100
above 100 capped and warned
GitHub rejects over 100 MiB on push
committed blobs still fail (history is scanned and warned)
After yes on ship:
git add -A
git commit -m "snapshot 2026-09-16 15:40"
git push # only if origin exists and you say yesdotkeep restoreRun from your state dir. Flow:
- Same git prep as backup (optional; fetch + ask pull if behind)
- Show manifest + preview (
state → machine); content delta notes; identical paths dropped; live symlinks may be replaced with copies - Optional pick a subset (no keeps all) via
gum choose(multi-select:xtoggles, enter confirms; all start selected) - Ask before write. Default no → abort
- Copy existing live targets to
/tmp/dotkeep.XXXXXX/first - Prune ignored paths under state
home//root/(if git) - Copy each listed path onto the live system (
rsync --deleteunder those paths; parents created withmkdir -p) - Print the safety bak path when anything was saved
No commit or push on restore.
git clone <url> <state-dir>
cd <state-dir>
dotkeep restorecd <state-dir>
dotkeep restoreNote
Pull is opt-in when origin is ahead. Default is no.
Dirty trees are stashed before pull, then popped.
--delete means extras under a listed dir on the live side are removed.
Prior live copies stay under /tmp/dotkeep.XXXXXX/ until you clear them.
NO_COLOR disables color
EDITOR used by config, else nvim vim vi
DOTKEEP_MAX skip limit in MiB, default 10, max 100
above 100 capped and warned. GitHub rejects the push
DOTKEEP_DRY=1 same as --dry-run (rsync dry-run; no mkdir / git writes)
Flags come after the command:
dotkeep backup --dry-run
dotkeep restore --dry-runPrompts use gum (confirm, input, choose, log, style, table, pager)
