Part 6 of a series on the tools I actually run.

I’ve been promising this post for a while now, mostly at the bottom of other posts.

Here’s the short version: my shell config lives in a folder that Syncthing keeps identical across every machine I use. I edit .zshrc on the laptop and about four seconds later it’s changed on the Debian box too. No commit, no push, no pull, no remembering which machine has the newest version.

Git is the normal answer here and it’s a good one. It just isn’t the one I use, and I’ll get to why.

The one rule: don’t sync your home directory

This is the thing to get right before anything else.

Do not point Syncthing at ~. It will try to sync your caches, your SSH keys, your browser profile, every node_modules you’ve ever created, and a great many files that are actively hostile to being copied between machines.

Instead: one folder, ~/dotfiles, and symlinks pointing out of it.

~/dotfiles/
  zshrc
  zshrc.darwin
  zshrc.linux
  gitconfig
  mpv/
  bootstrap.sh

Syncthing syncs that folder and nothing else. Then on each machine, once, you link the files into place:

ln -sf ~/dotfiles/zshrc ~/.zshrc
ln -sf ~/dotfiles/gitconfig ~/.gitconfig
ln -sf ~/dotfiles/mpv ~/.config/mpv

That’s what bootstrap.sh is: those lines, plus a mkdir -p or two. Run it once per new machine and you’re done.

The symlinks stay local. Syncthing never touches them. That separation is what stops the whole thing turning into a mess.

If you’d rather not hand-roll the linking, stow does the same job with directory conventions instead of a script. I keep the script because it’s eight lines and I can read it.

Why not git

Git is better than this in most ways. Real history, real diffs, the ability to undo something from three weeks ago, and a copy on a server somewhere that survives all your machines dying at once.

What it isn’t is automatic. Every change needs a commit, and every machine needs a pull. Which is fine in theory and in practice means I’d edit an alias on the Mac, forget to push, spend a week wondering why it wasn’t on the server, and eventually make the same edit there too. Then have a conflict to resolve over something I didn’t care about in the first place.

Syncthing removes the ritual. I change a file and it’s changed everywhere. For something I touch in small ways several times a week, that turns out to matter more to me than history does.

Two things I do to get some of git’s safety back:

Turn on file versioning. In the Syncthing folder settings, set it to Simple with a handful of versions, or Staggered. That’s your undo, and it’s saved me at least twice.

Use a .stignore. Sitting in ~/dotfiles:

*.local
.DS_Store
*.sync-conflict-*

More on that first line shortly.

And an honest warning: Syncthing propagates mistakes just as fast as it propagates fixes. Break your .zshrc and it’s broken on every machine before you’ve noticed. This has happened to me. Versioning is how you get out of it.

Since everything’s on a tailnet anyway, Syncthing finds the machines over Tailscale without needing global discovery or relays. You can switch both off in settings and it just works.

Now the Mac and Linux problem

A single shared .zshrc across macOS and Linux does not work unmodified, because the two systems disagree about a lot of small things.

Rather than maintain two files and keep forgetting to update one of them, I keep one shared file that pulls in an OS-specific piece:

# ~/dotfiles/zshrc

# everything common goes here

case "$(uname -s)" in
  Darwin) source ~/dotfiles/zshrc.darwin ;;
  Linux)  source ~/dotfiles/zshrc.linux ;;
esac

# machine-specific, never synced
[ -f ~/.zshrc.local ] && source ~/.zshrc.local

Common stuff at the top, OS differences in their own file, machine differences in a file that never leaves the machine.

What actually goes in the OS-specific files

PATH. Homebrew lives at /opt/homebrew on Apple Silicon and /usr/local on Intel. Neither exists on Linux. Guard it:

# zshrc.darwin
[ -d /opt/homebrew/bin ] && eval "$(/opt/homebrew/bin/brew shellenv)"

ls colours. macOS wants ls -G. Linux wants ls --color=auto. There is no flag that works on both, which is a small daily annoyance dating back decades.

# zshrc.darwin
alias ls='ls -G'

# zshrc.linux
alias ls='ls --color=auto'

This is the BSD-versus-GNU thing from the Homebrew post showing up again, and it will keep showing up. Installing coreutils on the Mac helps, but only if you’ve put the gnubin directory on your PATH, which I haven’t.

Clipboard. pbcopy and pbpaste on macOS. xclip or wl-copy on Linux depending on X11 or Wayland. I paper over it:

# zshrc.darwin
alias clip='pbcopy'

# zshrc.linux
alias clip='wl-copy 2>/dev/null || xclip -selection clipboard'

Opening things. open on macOS, xdg-open on Linux. Aliasing open to xdg-open on the Linux side lets everything else in the config stop caring.

Sleep inhibition. The codemusic alias from the last post uses caffeinate -i, which is Apple’s. The Linux equivalent is systemd-inhibit --what=idle. So that alias is defined once per OS file rather than in the shared one. Worth watching for: any alias wrapping a system utility probably belongs in an OS file rather than the common one.

The local file matters more than you’d think

~/.zshrc.local is the pressure valve, and it’s the one file deliberately excluded from syncing.

It holds anything true of exactly one machine. API keys and tokens. A PATH entry for something installed only there. The hostname-dependent bits. On the HP box at the office, a couple of aliases that only make sense at the office.

Without this you end up either polluting the shared config with if [[ $(hostname) == ... ]] blocks, or giving up on syncing entirely. The *.local line in .stignore is what keeps it local, and it’s also why secrets never ride along with everything else.

Conflicts

Occasionally you edit the same file on two machines before they’ve talked to each other, and Syncthing leaves you a .sync-conflict-20260922-141530-ABCDEFG file beside the original.

It doesn’t try to merge. It just keeps both and lets you sort it out. For dotfiles this is almost always fine, because the two versions differ by one line and a quick diff settles it. Then delete the conflict file.

If this happened weekly I’d want git’s merge machinery. It happens maybe twice a year, so I don’t.

Is this a good idea

It’s a reasonable one.

If you want history, review, and a copy off-site, use git. It’s the better tool and I won’t argue otherwise.

But if what you actually want is for a change made on one machine to exist on all of them without you thinking about it, Syncthing does that and git structurally can’t. I have enough small rituals in my day. This one I was happy to delete.