A useful configuration change should not become a copying exercise every time you switch computers. But a handful of personal machines hardly needs a synchronization platform either.

This guide uses one small routine: when you sit down at a computer, fetch changes from your other reachable computers, inspect them, and decide whether to accept them. Git records the changes; SSH transports them. A short Bash script handles the repetition.

The computers themselves are the peers. Here, “fleet” simply means the few computers you own and administer. Ordinary updates need only a fetch and confirmation. When histories diverge, the script prepares a temporary Git worktree for review; it can then send the accepted result back to the other device after a second confirmation.

Fetch from a known peer
          |
Review changes / resolve conflicts
          |
Accept the local result? [y/N]
          |
Optionally send the result back? [y/N]
          |
Peer checks its checkout before applying

The trade-off is explicit: direct peers must be online at the same time. Commit before leaving one machine, and keep it reachable until the next has fetched those commits. Nothing has to run continuously, but a sleeping laptop cannot serve its repository.

Changelog

DateChange
2026-09-10Initial Version: Guide to syncing dotfiles and a pass password store between SSH peers with interactive review, conflict resolution, and optional confirmed return updates.

1. Why Git Fits This Job

Git can fetch from another ordinary checkout over SSH. The remote need not be a hosted service or a bare repository. Fetching downloads history without replacing the current checkout; the downloaded commit is already the intermediate state needed for review. Git fetch documentation

A file synchronizer solves a different problem. Syncthing handles concurrent edits by retaining conflict copies; for configuration, this guide instead keeps the history and the decision to combine changes in Git. Keep live Git repositories outside folders managed by file synchronization software. Syncthing conflict handling

Use two repositories: one for dotfiles and another for the encrypted pass store. Only committed changes travel between peers. This is configuration-file management, not a complete system rebuild: installing packages and configuring system services remain separate tasks.

2. Shared Configuration, Host Differences, and Stow

Keep the shared files together and make the small host-specific parts explicit:

dotfiles/
├── fleet.conf
├── common/
│   └── .config/
│       └── example-app/
│           └── config
└── hosts/
    ├── desktop/
    │   └── .config/example-app/host.conf
    └── laptop/
        └── .config/example-app/host.conf

example-app is a placeholder: use your actual applications and their supported configuration syntax. Where an application supports includes, the shared file can include a local host fragment. Otherwise, keep its entire host-dependent configuration in the host package. Arrange packages so that they do not manage the same file.

GNU Stow links files from these packages into your home directory. Preview before applying, and select exactly one host package:

cd ~/.local/src/dotfiles
stow --simulate --verbose --no-folding --target="$HOME" common
stow --no-folding --target="$HOME" common
stow --dir=hosts --simulate --verbose --no-folding --target="$HOME" desktop
stow --dir=hosts --no-folding --target="$HOME" desktop

Use laptop on the laptop. Resolve existing-file conflicts deliberately before proceeding. --no-folding keeps the directory structure explicit and creates individual file links. Run Stow again when adding newly managed paths. GNU Stow manual

For machines that already have working configurations, start with a file-by-file inventory. Put reviewed, identical files in common; keep intentional differences in the respective host packages. A whole host-specific configuration file is a reasonable starting point when splitting it into includes would add unnecessary work. Track only explicitly selected files, not application caches, histories, session files, or entire configuration directories by default.

Before replacing an existing file with a Stow link, copy its contents and permissions into the intended package and make a separate dated backup outside the repository. Confirm that the package copy matches the live original, then move the original out of the target path. Run Stow’s simulation and apply steps, and verify that the new link resolves to the intended file with the original contents. If installation fails, remove only the links created during that attempt and restore the saved originals. Avoid using --adopt as a shortcut for merging different configurations: it moves existing target files into the package. Stow conflict handling and adoption

Treat theme definitions and hardware settings separately. Share colors, keybindings, and theme-switching scripts; preserve each machine’s DPI, monitor arrangement, and device names. For X resources, one practical arrangement is to assemble common defaults, the selected color palette, and the host’s overrides in that order, then load the result with xrdb. Keep DPI and other hardware values out of the palettes so switching between light and dark does not replace them. Check both modes and a new login. X.Org xrdb manual

Track the sources of generated configuration, rather than giving Stow and a theme switcher competing ownership of the same output. A switcher that replaces a file with mv or recreates a symlink can replace the link Stow installed. In this layout, theme palettes and scripts belong in the repository; the active theme links and assembled resource file are generated locally. Keep the Git checkout outside Syncthing-managed folders even if Syncthing distributes your standalone scripts.

First-Time Setup Is Separate from Everyday Sync

A new machine also needs the programs that read these files. Keep a small, explicit setup routine alongside the dotfiles: a reviewed package list, installation of supporting scripts and fonts, Stow application, and any required service setup. Run it deliberately when onboarding a machine or adding a dependency. The sync script does not install packages, create newly needed Stow links, or activate services. Keep the synchronizer executable itself as an installed copy, rather than a live link to its source in the repository it updates. Install a reviewed script update explicitly on each direct peer.

System configuration needs its own installation step. For example, share the intended appearance of a login manager, but install its configuration under /etc with explicit administrator privileges and a backup. Preserve host-specific session settings. An accepted Git commit must not automatically execute privileged setup or restart the active login session.

Validate workflows, not just file equality: a clipboard shortcut needs its daemon, a status-bar button needs its launcher and application, and a media-control bridge needs a reachable player or server. Record these dependencies in the setup notes. This keeps the everyday sync routine small while making the initial setup repeatable.

⚠️ REVIEW BEFORE UPDATING THE CHECKOUT

Stow links point into the repository. Updating an existing linked file changes the configuration available to the application immediately; there is no separate deployment barrier. Shell startup files can execute commands in the next shell. Review before accepting the merge, then reload applications as appropriate.

Keep private keys, tokens, and plaintext passwords out of the dotfiles repository. An ignore file is a convenience, not a secret detector: inspect what you stage.

For Vim swap files and editor backups, a small .gitignore at the repository root is enough to start:

*.swp
*.swo
*~

Commit this file with your configuration. Ignored, untracked files do not block the script’s clean-checkout check. Keep .orig and .rej files visible for inspection after conflict or patch work rather than ignoring them by default. Ignore rules do not stop tracking files already committed. Git ignore documentation

3. Set Up SSH and the First Repository

The examples assume Linux, Bash, Git, OpenSSH, flock from util-linux, GNU Stow, and pass with GnuPG. Vim supplies the default vimdiff merge editor; whole-file conflict choices do not require an editor. Install them through your distribution’s package manager. Enable the SSH server on machines that should serve repositories, following your distribution’s instructions.

All account names, machine names, paths, and domains below are examples. Replace them with your own. The examples use an ordinary account named user on machines named desktop and laptop, with repositories at the same relative location.

Set your Git author name and email if you have not already done so. On the first machine, create the dotfiles repository, populate it with reviewed configuration files, and make the first commit:

mkdir -p ~/.local/src/dotfiles
cd ~/.local/src/dotfiles
git init -b main
# Add your configuration packages before continuing.
git add common hosts
git commit -m 'Add initial configuration'

Use a separate SSH client key for each device. Verify the destination’s SSH host-key fingerprint through a trusted channel before accepting it. For example, compare the fingerprint displayed during the first connection with the server’s fingerprint viewed locally on that machine:

# Run locally on the destination; this reads its public host key.
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

Use the matching host-key type if the connection presents another type. Then install your client public key using ssh-copy-id user@desktop.local, and test noninteractive authentication:

ssh -o BatchMode=yes -o StrictHostKeyChecking=yes user@desktop.local true

The host key authenticates the destination; your client key authenticates your account access. This workflow trusts those known machines to serve the intended repositories. Manual review checks content; it does not establish the cryptographic authorship of each commit. OpenSSH client settings, ssh-keygen

Clone onto the second machine after SSH works:

mkdir -p ~/.local/src
git clone user@desktop.local:/home/user/.local/src/dotfiles ~/.local/src/dotfiles

Repeat the SSH setup in each direction you intend to fetch. A normal account is the simplest starting point; restrict access further if your threat model requires it. Do not expose SSH publicly just for this workflow.

Names ending in .local require working mDNS on the machines and network; they are not automatic merely because SSH is installed. mDNS is local-link discovery, so these names normally will not work from a café or across routed networks. Existing local DNS names or stable LAN addresses work too. Multicast DNS specification

4. One Peer List, One Interactive Script

Create fleet.conf at the root of the dotfiles repository. Each line contains the peer’s short hostname and its SSH repository URL, separated by whitespace:

# Short hostname    SSH repository URL
desktop    user@desktop.local:/home/user/.local/src/dotfiles
laptop     user@laptop.local:/home/user/.local/src/dotfiles

Commit this file so all clones receive the same list. Labels must match the hostname from uname -n up to the first dot on the corresponding machines, allowing the script to skip itself. Use unique short hostnames. URLs must contain no whitespace. Blank lines and full-line comments are allowed, as are trailing comments introduced by whitespace followed by #. A # within a URL remains part of the URL. The file is read as data, never executed as shell code; malformed entries stop the script with an error.

No git remote add is needed for these peers: Git accepts the URL directly. Adding a machine means adding a line after preparing its SSH access and initial clone. Each repository has its own list because its remote paths differ.

The complete implementation is one standalone Bash script: download fleet-sync. Read it before installing it on each direct peer:

mkdir -p ~/.local/bin
install -m 755 /path/to/downloaded/fleet-sync ~/.local/bin/fleet-sync

Make sure ~/.local/bin is on your interactive shell’s PATH. Run fleet-sync in a terminal; it defaults to ~/.local/src/dotfiles. The repository must already have commits on main and no pending local changes. The script does not automatically create commits for ordinary edits or stash unfinished work.

Before switching devices, commit the changes you want to carry over. For the placeholder layout above:

cd ~/.local/src/dotfiles
git status --short
git diff
git add -- common/.config/example-app/config
git diff --cached
git commit -m 'Adjust example-app settings'

Replace the example path with the specific files you reviewed. Once the checkout is clean, run fleet-sync here to offer the commit to other peers, or fetch it when you arrive at the next device. Equal commits produce no acceptance prompt. Git staging, Git commits

The normal decisions are:

SituationWhat the script offers
Both tips are equalNothing to do.
The peer is aheadShow the incoming diff and ask before updating this device.
This device is aheadShow the outgoing diff and ask whether to update the peer now.
Both made independent commitsOffer an interactive merge, then review the combined result and optionally send it back.

A failed fetch leaves its error visible and moves on. BatchMode=yes prevents interactive SSH authentication prompts; StrictHostKeyChecking=yes requires an already trusted host key. ConnectTimeout=3 limits connection establishment and the initial handshake, not the entire transfer or every name-resolution delay. OpenSSH settings

The peer list is loaded and validated before any peer is processed. The fetched commit ID is fixed for each review; another fetch cannot silently replace the proposal. Nonignored untracked files block updates too. Ignored files may remain, but a merge must not overwrite one that collides with an incoming tracked path.

Updating the Other Device Immediately

SSH access makes a return update possible. After reviewing an outgoing diff, answer the separate prompt:

Proposed update: desktop -> laptop
...
Send and apply this reviewed result on laptop now? [y/N]

This approval authorizes changing the other device’s checkout, including its existing Stow-linked configuration. No second terminal prompt appears there. Applications may read the updated files immediately; the script does not reload them or install new Stow links.

The same script must be installed at ~/.local/bin/fleet-sync on that peer. Git invokes its receiver mode over the existing SSH connection. It transfers the reviewed commit through a temporary Git ref, then the receiver checks that it is still on the expected clean main and applies only a fast-forward. If the target changed, contains unfinished work, is busy, or has a colliding ignored file, the update is refused. A failed return update leaves the accepted local result available for a later retry. The temporary ref is removed on normal completion or handled failure. Git’s custom receive command support provides the transport. Git push documentation

Answer n to leave the other device alone and let it fetch later. A forge with a restricted Git-only SSH service generally cannot run this helper; keep it fetch-only in this script and use an ordinary explicit git push when desired.

More Than Two Devices

Peers are processed sequentially. Each accepted result becomes the starting point for the next comparison; previous approvals are not rolled back if a later peer fails. A peer visited early may need another pass to receive changes discovered later in the list.

A nonblocking flock lock prevents two Fleetsync operations from editing the same repository simultaneously, including the receiver. A busy destination refuses the transfer instead of waiting indefinitely. This coordinates Fleetsync instances, not unrelated editor or Git processes: avoid concurrent manual Git operations in the same checkout. Checks immediately before applying catch common changes during review. flock documentation

5. Use the Same Routine for pass

pass keeps entries encrypted with GnuPG. Start on one machine with the intended recipients’ public keys already imported and a recovery plan for the corresponding private keys. Substitute real public-key fingerprints below:

pass init RECIPIENT_A_FINGERPRINT RECIPIENT_B_FINGERPRINT
pass git init
pass git branch -M main

pass git init records existing contents and configures Git integration; do not add a redundant initialization commit. Clone the resulting store onto another machine only if its destination does not already contain a password store:

git clone user@desktop.local:/home/user/.password-store ~/.password-store

Provision that machine’s decryption key securely and separately, then verify that it can read an entry. Do not put private keys in either repository. pass documentation

If both machines already contain password stores without a shared Git history, stop before cloning or initializing them independently. Back up both stores and compare the encrypted files and recipient configuration first. An entry present on only one device may be a new addition or an intentional deletion on the other; do not automatically take the union. Agree on the intended initial contents, resolve any differing entries deliberately, and create one initial history from that result. Keep the original stores until the replacement and decryption checks succeed.

Add and commit a fleet.conf inside this repository too:

desktop    user@desktop.local:/home/user/.password-store
laptop     user@laptop.local:/home/user/.password-store
pass git add fleet.conf
pass git commit -m 'Add peer list'
fleet-sync ~/.password-store

Keep the confirmation here as well. The script shows additions, modifications, and deletions by filename, plus the text diff for files other than .gpg entries. Inspect recipient changes in .gpg-id carefully: they influence future encryption. Changes to the peer list also deserve review.

A plaintext password comparison is possible: pass git init configures a GPG text-conversion diff driver locally. That Git configuration does not travel with a clone. The script explicitly disables text conversion and excludes encrypted entries from its content diff, so an ordinary sync does not automatically print passwords. Decrypt individual versions deliberately if you need to resolve a password conflict. pass implementation

For a recipient change, first bring the participating machines to a common commit. Run pass init with the complete new recipient list on one machine, inspect its generated commits, and let the others fetch that history. Re-encryption alone does not imply divergent history; independent commits do. Removing a recipient cannot retract old ciphertext or secrets that device already obtained. Rotate affected credentials if access must actually be revoked.

Encryption leaves filenames and Git metadata visible. Choose nonsensitive entry names and protect backups and key recovery material.

6. Resolve Conflicts Without Changing Live Files First

Divergence means both sides contain commits absent from the other. It does not necessarily mean they edited the same lines. The script first shows both histories and asks whether to prepare a merge.

Git performs that merge in a temporary detached worktree, sharing the existing object database. The active checkout and its Stow targets remain unchanged while you review. This is needed for preparing a combined result; ordinary fetch-and-review updates still need no second checkout. Git worktree documentation

Git combines nonconflicting changes. For each remaining conflict, the script identifies the file and offers:

Local = desktop; peer = laptop.
l = whole LOCAL file; p = whole PEER file; e = merge editor; a = abort.
Choose [l/p/e/a]:

l or p selects that side’s entire file, including deletion if it does not exist on that side. To combine individual changes within a text file, choose e. The default is Git’s vimdiff mergetool: LOCAL is this device’s version, REMOTE is the peer’s version, and MERGED is the result to save. You may choose another installed Git mergetool with FLEET_MERGETOOL, for example FLEET_MERGETOOL=meld fleet-sync. Git mergetool documentation

Encrypted .gpg files are never opened in this text editor or decrypted for the displayed diff. Select a complete encrypted version only if you know it is the intended one; otherwise abort and reconcile that entry separately.

After conflict resolution, the script shows the complete proposed change to the local checkout and asks:

Accept and commit this combined result locally? [y/N]

Only y creates the merge commit and fast-forwards the active checkout to it. A conflict-free merge of divergent histories still needs this approval. Aborting or rejecting the result discards the temporary worktree for that peer. After local acceptance, you may separately approve sending the result back. Both original histories remain ancestors of the merge commit, so other peers can receive the convergence through a fast-forward. Git merge documentation

7. Optional Remote and Backups

A private Forgejo repository can provide an extra copy reachable when the direct peers are asleep. If you already use one, push your converged branch to it explicitly:

git remote add forgejo git@forgejo.example.org:example/dotfiles.git
git push forgejo main

Add its SSH URL to fleet.conf under a label such as forgejo if the script should also fetch it. Decline the script’s direct-checkout return update for this remote and use the ordinary push command above instead. Prepare its host key and authentication first. Do the equivalent separately for the password store. This service is optional; it only has commits someone has pushed to it.

Keep independent backups. For a portable Git-history copy, create a bundle outside the checkout, for example on mounted encrypted backup media:

backup_dir=/path/to/mounted/encrypted-backup
bundle="$backup_dir/dotfiles-$(date +%F-%H%M%S).bundle"
git -C ~/.local/src/dotfiles bundle create "$bundle" --all
git -C ~/.local/src/dotfiles bundle verify "$bundle"

Use an existing destination and avoid overwriting an earlier backup. Repeat for the password store. A bundle can seed a clone, but it does not contain uncommitted files, local Git configuration, or private GPG keys; back up recovery material separately. Git bundle documentation

The everyday habit stays small: commit your work; fetch, review, and accept; optionally send the reviewed result back. Add signatures, a fetch-only timer, or VPN reachability later only if a concrete need appears. None is required for a few trusted computers exchanging reviewed changes on the same LAN.

Sources