Documentation

Everything you need to know about running dotkeeper.

Why 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.

Compared to alternatives

Without dotkeeperWith dotkeeper
Git aloneMust remember to commit and push. Uncommitted work doesn't sync.All files sync instantly. Git runs on autopilot.
Syncthing aloneNo version history. Deletions propagate everywhere.Git history for every change. Roll back anytime.
Cloud syncThird-party server. File locking. No git. Costs money.Peer-to-peer. No accounts. Free forever.
rsync / UnisonManual or cron-only. No real-time. No history.Real-time sync + scheduled versioned backup.

Architecture

dotkeeper embeds the Syncthing library as a Go dependency. The entire sync engine compiles into a single binary — no separate Syncthing installation needed.

Multi-transport routing (v1.0+)

Three ways to move a change between peers coexist:

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.

Subscriptions (v1.2+)

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.

Dual sync strategy

Isolated ports

PortProtocolPurpose
18384HTTPREST API (loopback only)
12000TCPSyncthing file sync
11027UDPLocal discovery

These are separate from Syncthing's default ports (8384, 22000, 21027), so dotkeeper never interferes with an existing Syncthing installation.

Three config files

FileLocationPurposeSynced?
machine.toml~/.config/dotkeeper/Declarative local config: name, slot, peers, defaults, scan rootsNever
state.toml~/.local/state/dotkeeper/Tool-owned runtime state: Syncthing identity cache, learned peers, tracked overridesNever
.dotkeeper.tomlEach local managed repo copyPer-repo desired state. v1.2+ adds a [git] section with the canonical remote URL — the load-bearing identity for subscriptions.Never

Commands

Setup

dotkeeper initInitialize this machine. Creates runtime state and Syncthing identity. Flags: --name, --slot, --force, --no-service.
dotkeeper identityPrint this machine's name and Syncthing device ID.
dotkeeper peer add/list/removeManage imperative peers in state.toml. Declarative peers can live in machine.toml.

Repos

dotkeeper reconcileRun 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.

Subscriptions (v1.2+)

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 listShow the merged subscription list (declarative + imperative) with a SOURCE column.
dotkeeper offersList folders peers are advertising that this machine could subscribe to. Output includes a shell-paste-ready dotkeeper subscribe … action column.

Multi-transport (v1.0+)

dotkeeper transport listList 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 reposPer-(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-initConfigure peer-side git repos for direct git push (sets receive.denyCurrentBranch=updateInstead).

Service

dotkeeper startRun embedded Syncthing and the reconcile daemon in the foreground.
dotkeeper statusShow machine info, Syncthing status, repos, peers, and pending work.
dotkeeper healthOperational dashboard. Flags: --explain for guidance on warning kinds, --watch=DURATION for tmux-pane mode, --json for machine-readable output.
dotkeeper doctorRun self-diagnostic checks; use --json for machine-readable output.
dotkeeper versionPrint version and commit.

Quick start

First machine

dotkeeper init --name laptop --slot 0
dotkeeper identity
$EDITOR ~/.config/dotkeeper/machine.toml

Second machine

# 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.

Adding repos on the first machine

# 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

Adding repos on a fresh machine (v1.2+)

# 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>

Nix / Home Manager denylist

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.

Platform support

OSArchitecturesService manager
Linuxamd64, arm64, armv7, 386, riscv64, ppc64le, s390xsystemd or cron
macOSamd64, arm64 (Apple Silicon)launchd
Windowsamd64, arm64, 386Task Scheduler
FreeBSDamd64, arm64cron
OpenBSDamd64, arm64cron
NetBSDamd64, arm64cron

Pre-built binaries and packages (.deb, .rpm, .apk, .pkg.tar.zst) are available for Linux architectures in every release.

Git timer configuration

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.

Ignore patterns

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.

Security

Report vulnerabilities through GitHub's private security advisory form. See SECURITY.md for the full policy.

Contributing

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.