Everything you need to know about running dotkeeper.
If you work on multiple machines, you know the friction: code edited on the laptop but not pushed, config files that drift, dotfiles that only exist on one machine. You can solve pieces of this with git, Syncthing, or cloud sync — but each has gaps.
dotkeeper combines Syncthing's real-time P2P sync with git's versioned history into a single tool. Files sync instantly between machines. Git auto-commits on a schedule so nothing is ever lost.
| Without dotkeeper | With dotkeeper | |
|---|---|---|
| Git alone | Must remember to commit and push. Uncommitted work doesn't sync. | All files sync instantly. Git runs on autopilot. |
| Syncthing alone | No version history. Deletions propagate everywhere. | Git history for every change. Roll back anytime. |
| Cloud sync | Third-party server. File locking. No git. Costs money. | Peer-to-peer. No accounts. Free forever. |
| rsync / Unison | Manual or cron-only. No real-time. No history. | Real-time sync + scheduled versioned backup. |
dotkeeper embeds the Syncthing library as a Go dependency. The entire sync engine compiles into a single binary — no separate Syncthing installation needed.
Three ways to move a change between peers coexist:
git push over SSH (Tailscale-resolved by default). Lowest setup latency for small commits.mutagen is on PATH (detect-and-fallback; no setup needed).A cost model picks per change. It starts with sensible priors per transport family, learns from every observed transfer, and (since v1.1.22) actively benchmarks idle (transport, peer, repo) tuples in the background so the model self-tunes even for repos that rarely change.
A folder's identity is its canonical git remote URL. Every URL syntax for the same upstream repo collapses to one identity: HTTPS, SCP-style git@host:path, and ssh:// all yield github.com/foo/rag.
On a fresh machine, dotkeeper offers shows what peers are advertising. dotkeeper subscribe github.com/foo/rag declares the local intent. Reconcile matches, provisions the path, and Syncthing's BEP gossip seeds the working tree from the peer. One command per repo — no manual git clone + track dance.
Declarative subscriptions live in machine.toml's [[subscribe]] sections (Nix-friendly). Imperative ones live in state.toml via the CLI. The reconciler merges both sources; declarative wins on conflict.
| Port | Protocol | Purpose |
|---|---|---|
18384 | HTTP | REST API (loopback only) |
12000 | TCP | Syncthing file sync |
11027 | UDP | Local discovery |
These are separate from Syncthing's default ports (8384, 22000, 21027), so dotkeeper never interferes with an existing Syncthing installation.
| File | Location | Purpose | Synced? |
|---|---|---|---|
machine.toml | ~/.config/dotkeeper/ | Declarative local config: name, slot, peers, defaults, scan roots | Never |
state.toml | ~/.local/state/dotkeeper/ | Tool-owned runtime state: Syncthing identity cache, learned peers, tracked overrides | Never |
.dotkeeper.toml | Each local managed repo copy | Per-repo desired state. v1.2+ adds a [git] section with the canonical remote URL — the load-bearing identity for subscriptions. | Never |
dotkeeper init | Initialize this machine. Creates runtime state and Syncthing identity. Flags: --name, --slot, --force, --no-service. |
dotkeeper identity | Print this machine's name and Syncthing device ID. |
dotkeeper peer add/list/remove | Manage imperative peers in state.toml. Declarative peers can live in machine.toml. |
dotkeeper reconcile | Run one reconcile pass and apply the plan. |
dotkeeper track <path> | Bootstrap local .dotkeeper.toml, write local excludes, and register a git repo. v1.2+ populates [git] with the canonical remote URL automatically. |
dotkeeper untrack <path> | Remove an out-of-root tracked override. |
dotkeeper subscribe <git-url> | Declare this machine wants the named repo from any peer offering it. Any URL form (HTTPS / SCP / ssh://) is normalised. Flags: --name NAME for non-git folders, --path to override the mirror-convention default. |
dotkeeper unsubscribe <git-url-or-name> | Remove an imperative subscription from state.toml. Declarative entries in machine.toml must be edited there. |
dotkeeper subscriptions list | Show the merged subscription list (declarative + imperative) with a SOURCE column. |
dotkeeper offers | List folders peers are advertising that this machine could subscribe to. Output includes a shell-paste-ready dotkeeper subscribe … action column. |
dotkeeper transport list | List configured transports and their Available() state. |
dotkeeper transport status [peer] | Per-peer reachability + cross-repo cost-model parameters (probe RTT, learned setup-ms, learned MB/s, effective sample count). |
dotkeeper transport repos | Per-(transport, peer, repo) cost-model predictions with PRED@1KB / PRED@1MB columns. Supports --peer and --transport filters. |
dotkeeper bench-now [--folder=PATH] | Operator-triggered transport benchmark. Runs one 64 KB probe per (synchronous transport, peer) pair, records observations in the cost model. |
dotkeeper bare-init | Configure peer-side git repos for direct git push (sets receive.denyCurrentBranch=updateInstead). |
dotkeeper start | Run embedded Syncthing and the reconcile daemon in the foreground. |
dotkeeper status | Show machine info, Syncthing status, repos, peers, and pending work. |
dotkeeper health | Operational dashboard. Flags: --explain for guidance on warning kinds, --watch=DURATION for tmux-pane mode, --json for machine-readable output. |
dotkeeper doctor | Run self-diagnostic checks; use --json for machine-readable output. |
dotkeeper version | Print version and commit. |
dotkeeper init --name laptop --slot 0
dotkeeper identity
$EDITOR ~/.config/dotkeeper/machine.toml
# Put the first machine's device ID in machine.toml, or:
dotkeeper peer add laptop KFZZ7TP-GH5OLNL-WNSTHWQ-...
dotkeeper reconcile
Each machine keeps its own machine.toml. Home Manager, a flake activation script, or your own bootstrap script can generate it.
# Repos under scan roots get a local .dotkeeper.toml via track.
# v1.2+ also discovers the git origin URL and stores its canonical form.
dotkeeper track ~/Documents/GitHub/new-project
# For repos outside scan roots:
dotkeeper track ~/src/special-project
dotkeeper reconcile
# See what peers are offering you:
dotkeeper offers
# Subscribe by canonical git URL — any URL syntax works:
dotkeeper subscribe github.com/foo/repo
dotkeeper subscribe git@github.com:foo/repo.git
dotkeeper subscribe https://github.com/foo/repo
# Custom local path:
dotkeeper subscribe github.com/foo/repo --path=~/code/repo
# Reconcile picks it up (or wait for the next tick):
dotkeeper reconcile
Reconcile creates the local path, writes a .dotkeeper.toml stub, and adds the folder to Syncthing with the offering peer in share-with. BEP gossip seeds the working tree from the peer. Subscriptions can also be declared in machine.toml:
[[subscribe]]
canonical = "github.com/foo/repo"
# path = "..." optional; defaults to <first scan_root>/<basename>
For declarative setups, keep dotkeeper generic and put private topology in your own flake. Scan a few roots, generate local .dotkeeper.toml files for discovered Git repos, and list only the paths that must not sync.
{
scanRoots = [
"~/Documents/GitHub"
"~/.config"
];
dontSync = {
all = [
"~/Documents/GitHub/dotkeeper"
"~/Documents/GitHub/archive"
"~/Documents/GitHub/example-rag/workspace"
];
laptop = [
"~/Documents/GitHub/video-renderer"
];
};
}
Whole denied repos stay unmanaged on that machine. Denied paths inside a managed repo become [sync].ignore patterns, so generated data can stay local while the repo itself still syncs.
| OS | Architectures | Service manager |
|---|---|---|
| Linux | amd64, arm64, armv7, 386, riscv64, ppc64le, s390x | systemd or cron |
| macOS | amd64, arm64 (Apple Silicon) | launchd |
| Windows | amd64, arm64, 386 | Task Scheduler |
| FreeBSD | amd64, arm64 | cron |
| OpenBSD | amd64, arm64 | cron |
| NetBSD | amd64, arm64 | cron |
Pre-built binaries and packages (.deb, .rpm, .apk, .pkg.tar.zst) are available for Linux architectures in every release.
The default git backup schedule is set in machine.toml, with per-repo overrides in local .dotkeeper.toml:
default_git_interval = "daily" # hourly, daily, weekly, monthly, 2h, 6h, 12h
default_slot_offset_minutes = 5 # minutes between each machine's backup slot
Each machine gets a slot number (set during init). The timer fires at base_time + (slot * offset). With slot offset 5 and daily interval:
This prevents simultaneous pushes to the same git remote.
dotkeeper auto-generates .stignore files with sensible defaults. These are Syncthing ignore patterns — they control what Syncthing syncs, not what git tracks.
Default patterns include:
.git
.dotkeeper.toml
dotkeeper.toml
.stignore
.dkfolder
.stfolder
.syncthing.*.tmp
*.sync-conflict-*
node_modules
__pycache__
*.pyc
*.o, *.so, *.dylib, *.exe, *.dll
*.sqlite3, *.sqlite3-journal
.DS_Store, Thumbs.db, desktop.ini
target/, dist/, build/
.idea/, .vscode/ (volatile state only)
Customize per repo by editing the [sync].ignore list in local .dotkeeper.toml.
0600 permissions06000700127.0.0.1 only, authenticated via API key^\d+$)Report vulnerabilities through GitHub's private security advisory form. See SECURITY.md for the full policy.
dotkeeper is AGPL-3.0 licensed. Contributions are welcome.
git clone https://github.com/corbet-labs/dotkeeper
cd dotkeeper
make test # unit, integration, e2e, fuzz
make build # build the binary
The test suite includes unit tests, fuzz targets, golden file tests, adversarial input tests, concurrent sync tests, CLI smoke tests, and error injection tests. See CONTRIBUTING.md for the full guide.