Syncing Protected Files Across Dev Machines, Without Putting Them in Git
- The Problem
- The Solution: Unison
- The SSH Setup
- The Integration
- The Workflow
- Why Not Just Put Everything in Git?
- Setup Summary
If you develop on multiple machines, you know the pain. Your code lives in Git, but some files can’t go there: API keys, project instructions for your AI assistant, internal documentation, marketing assets, screenshots. They’re essential to your workflow but have no business in a public (or even private) repository.
For months, I solved this the dumb way: zipping the files, sending them through a messenger, unzipping on the other machine. It worked, but it was tedious, error-prone, and I kept forgetting which machine had the latest version of what.
Here’s how I replaced that entire workflow with a single command.
The Problem
I develop my-project on two machines, a MacBook and a Linux box. Both run their own AI Agent instance. The codebase syncs through Git, but these files stay local:
- AGENTS.md: project instructions for the AI (architecture rules, conventions, patterns)
- skills/: slash command definitions for AI Agent
- docs/: internal documentation, todos, roadmap
- marketing/: banner images, hero concepts
- screenshots/: app screenshots for documentation
These files change constantly. Every session might update the docs, refine a skill definition, or add a screenshot. And when I switch machines, I need the latest version of everything, not yesterday’s version.
The naive approaches all have problems:
Git with .gitignore tricks? Defeats the purpose. These files are excluded from Git for a reason.
Manual copy via scp? Works once. Then you forget which direction you copied last, overwrite something, or miss a deleted file.
Syncthing or Dropbox? Overkill for a handful of directories, and I don’t want continuous sync running in the background for files that only matter during development sessions.
Rsync? Close, but rsync is one-directional. You always need to know: am I pushing or pulling? And it can’t detect deletions without the –delete flag, which in bidirectional mode is a footgun.
The Solution: Unison
Unison is a file synchronization tool built specifically for bidirectional sync. Unlike rsync, it:
- Tracks state between syncs (it remembers what the files looked like last time)
- Detects changes on both sides
- Knows the difference between “new file” and “deleted file”
- Handles conflicts (both sides changed the same file)
It’s been around since the late 1990s, written in OCaml, and it’s rock solid.
The entire setup is a 60-line shell script.
The Script
The script (sync-protected.sh) lives in the project root, excluded from Git via .gitignore. It does three things:
- Detects which machine it’s running on (macOS or Linux)
- Sets the other machine as the remote
- Runs unison with the right paths
The configuration is simple: two hosts, two users, a list of paths to sync.
LINUX_HOST=“192.168.1.50” LINUX_USER=“alice” MACOS_HOST=“192.168.1.51” MACOS_USER=“bob”
PATHS=(docs screenshots marketing skills AGENTS.md)
Unison is called with a few key flags:
-auto -batch (no interactive prompts, resolve automatically) -times (sync modification times) -perms 0 (ignore permission differences between macOS and Linux) -prefer newer (when in doubt, the newer file wins) -confirmbigdel (ask before deleting entire directories)
The -perms 0 flag is important. macOS and Linux have different users, different umasks, different filesystem semantics. Without it, unison sees “properties changed on both sides” for every file and skips them all.
The -prefer newer flag handles the rare case where both sides modified the same file. Instead of stopping and asking, it picks the newer one. For my use case, where only one machine is active at a time, this is always correct.
The SSH Setup
Unison uses SSH for transport. Both machines need passwordless key authentication to each other:
On macOS: ssh-keygen -t ed25519 ssh-copy-id alice@192.168.1.50
On Linux: ssh-keygen -t ed25519 ssh-copy-id bob@192.168.1.51
One gotcha: if you install unison via Homebrew on macOS, it lives in /opt/homebrew/bin/, which isn’t in the PATH when accessed over SSH. Fix it with a symlink:
sudo ln -s /opt/homebrew/bin/unison /usr/local/bin/unison
Both machines must run the same major version of unison (e.g., both 2.53.x). Different major versions refuse to talk to each other.
The Integration
The script works standalone, but I integrated it into my workflow as a slash command. A skill definition file tells the AI agent what to do when I type /sync-prot:
- Run ./sync-protected.sh
- Report the result.
That’s it. From either machine, I type /sync-prot and everything syncs. No direction to specify, no flags to remember, no questions asked.
The first run takes a few seconds longer because unison scans all files and builds its state archive. Every subsequent run is near-instant, it only checks what changed since last time.
Here’s what a typical sync looks like:
Syncing protected files… Local: /home/bob/projects/my-project Remote: alice@192.168.1.50
Looking for changes Reconciling changes changed ––> AGENTS.md changed ––> skills/finalize/SKILL.md
Synchronization complete (2 items transferred, 0 skipped, 0 failed)
Two files changed locally, pushed to remote. If the remote had changes too, they’d sync in the other direction simultaneously.
Deletions work correctly too. If I consolidate three documentation files into one and delete the originals, unison propagates the deletions to the other machine:
deleting docs/old-plan.md deleting docs/old-notes.md changed ––> docs/consolidated.md
The Workflow
My daily workflow across two machines now looks like this:
- Sit down at a machine
- /pull (git pull for all branches)
- /sync-prot (sync protected files)
- Work
- /sync-prot (push changes to the other machine)
- /push (git push)
When I switch to the other machine, same thing. The protected files are always in sync, the git repo is always in sync. Zero friction.
I also kept a backup step in my /finalize pipeline: it zips all protected files to ~/Downloads before any git operations. Belt and suspenders.
Why Not Just Put Everything in Git?
Some of these files contain instructions specific to my AI workflow. Others are large binary assets (screenshots, marketing banners) that would bloat the repository. Some contain internal notes I don’t want public. And AGENTS.md contains architectural decisions and conventions that are meant for my AI assistant, not for contributors.
The pattern applies to any project where you have files that are essential for your workflow but shouldn’t live in version control: configuration files with secrets (even if the secrets are redacted, the structure reveals information), internal documentation that’s not ready for public consumption, assets that are too large for Git.
Setup Summary
- Install unison on both machines (same major version)
- Set up SSH key authentication in both directions
- Create the sync script with your paths and hosts
- Optionally create a slash command or alias for convenience
- Run it whenever you switch machines
The whole setup took about 20 minutes, most of which was the SSH key exchange. The script itself is trivial.
For something I do multiple times a day, removing the friction of “how do I get my files to the other machine” has been worth every minute of setup. No more zipping, no more messenger transfers, no more “wait, which machine has the latest docs?”
Just /sync-prot and done.