Resource guide
Keep your workspaces on your Mac
duetfs sync mirrors every workspace you belong to into a folder on your Mac and keeps both
sides live: edits in the web app or by an agent land in ~/duet/<workspace> within seconds, and
edits you make locally flow back. Install it once as a login service and forget it — new
workspaces you join appear on their own, and the mirror reconnects by itself across sleep, network
drops, and reboots.
This page installs and runs the daemon. The lower-level transfer API it drives — registering a device as a whole-drive replica — is documented under mirror devices; you do not call it directly.
macOS only. The bundled transfer engine ships for macOS in this release.
Before you start
You need a Duet account with membership in at least one workspace — the mirror set is exactly the
workspaces you belong to — and the duetfs CLI on your Mac:
npm install -g duetfsThe transfer engine is bundled with the CLI for your Mac's architecture; there is nothing else to install.
Install
Sync runs under one sync-device credential — the single user-wide key that reaches every
workspace you belong to, minted through the device flow. Log in once with --sync:
duetfs login --syncApprove the machine in the browser. The credential is written to ~/.duet/config.json with
owner-only permissions (0600), the same store every duetfs key uses. It is scoped to files only
(user:files:read, user:files:write) and reaches a workspace only while your membership holds —
see API keys for the full scope model. Because this one key reads and writes files in
every workspace you belong to, keep the file readable only by you and revoke the device if the Mac
is lost. Name the machine so you can recognize it later in the web device list:
duetfs login --sync --device-name "work laptop"Then install the login service:
duetfs sync installThis registers a per-user launchd agent that starts the daemon at login and keeps it running. It touches only your own login items; a Syncthing you already run, and any other launch agents on the machine, are left alone. The command is idempotent — run it again after an upgrade and it replaces its own agent in place.
The daemon starts mirroring immediately. Give it a minute on the first run: it downloads a full copy of each workspace drive.
What lands on your Mac
Each workspace mirrors into its own folder, ~/duet/<workspace>, as a whole-drive replica —
everything on the drive, in both directions, not a curated subset. Three classes of path are always
held back so machine-local state never crosses between machines:
.gitdirectories andnode_modules— excluded on both sides. See Git repositories below for why, and how history still reaches your Mac..duet— Duet's own per-workspace state directory.
Those three are the shared exclusions the workspace itself enforces (the same list shown under
mirror devices). On top of them, the daemon adds a Mac-local exclusion for Finder
and Spotlight clutter — .DS_Store, AppleDouble ._* sidecars, .Spotlight-V100, .Trashes — so
that macOS junk created on your laptop never travels up into the workspace.
Everything else replicates, including .env files and other secrets. A mirror is a private replica
of your own workspace, so its environment files come with it — unlike a workspace
share, which withholds them. Treat the folder on your Mac as holding every
secret the drive holds.
You will also see two small bookkeeping directories the transfer engine keeps inside each mirror,
.stfolder and .stversions. Leave them in place; they are how it marks the folder and stashes
old versions, and they are not synced anywhere.
Check status
duetfs sync status reports every workspace the daemon is tracking, one line each:
duetfs sync statusacme up to date ~/duet/acme
blog syncing 62% ~/duet/blog (8.4 MB to go)
notes 2 conflicts ~/duet/notes
old-project retired — not mirrored- up to date — the local copy and the workspace agree and the connection is live.
- syncing — a transfer is in flight; the percentage and remaining bytes come straight from the transfer engine.
- conflicts — both sides changed the same file before they could reconcile; see Conflicts.
- retired — the workspace is idle-retired on the server and is not currently mirrored; see Retired workspaces.
Add --json for a machine-readable form with the same fields. When an agent is editing a file in a
workspace, status can name it ("agent editing src/app.ts") — that line is informational, drawn from
file activity, and never affects what actually syncs.
Conflicts
If a file changes on your Mac and on the workspace before the two reconcile, the transfer engine keeps both copies rather than picking a winner or losing your bytes. Your local copy is renamed alongside the incoming one, following the engine's convention:
report.md
report.sync-conflict-20260713-142230-K5MNBGJ.mdstatus counts these per workspace so you can find and resolve them. Open both, keep what you want,
and delete the .sync-conflict-* copy. Nothing is deleted for you, and a conflict never blocks the
rest of the folder from syncing. The engine keeps up to ten conflict copies of a single file before
it starts pruning the oldest, so resolve them rather than letting them pile up.
Retired workspaces
A workspace that no one touches for a while is retired on the server: its drive is archived and
it stops mirroring. The daemon does not delete your local ~/duet/<workspace> folder — it keeps it,
marks the workspace retired in status, and re-checks on a slow cadence so that if the workspace
comes back the mirror resumes on its own.
Retirement is not un-done by editing your local copy. Local edits to a retired workspace wait on your Mac; to bring the workspace back, open it from the web app, then let the next status re-check pick it up.
Git repositories
A repository's working files sync live — edit src/app.ts on either side and it lands on the other
in seconds. What does not sync is the .git directory itself (and node_modules). This is
deliberate, and understanding it is the difference between a mirror you trust and a corrupted repo.
Why .git is excluded. A Git repository is not safe to replicate byte-for-byte between two
machines that both write to it. Its index caches inode numbers that differ per machine, its refs and
pack files have ordering rules no file-syncer honors, and two sides committing concurrently produce a
repository that is valid on neither. Syncing .git does not give you shared history — it gives you
conflict copies of HEAD and a repo you have to delete and re-clone. So the mirror leaves .git
alone on each machine.
Why node_modules is excluded. Installed dependencies are platform-specific and rebuildable.
Copying them across machines clobbers one architecture's binaries with another's. Run your installer
(bun install, npm install) on each side instead.
How your work still reaches your Mac. Two ways, and they complement each other:
- As files, immediately. Every file an agent or the web app writes appears in your local folder within seconds — before any commit. You see work in progress live, which is the whole point of the mirror.
- As history, on
git pull. Commits travel through a real Git remote, not through the mirror. Push from the workspace (or let an agent push), thengit pullon your Mac. Because the committed files are already sitting in your working tree from the live sync, the pull is fast and mostly updates Git's own bookkeeping to match.
Give every repo a remote. History can only reach your Mac if the repository has a real remote to
travel through. A repo with no remote has no path for its commits to cross — you will see the working
files but never the history. duetfs sync status flags a mirrored repository that has no remote so
you can add one:
git remote add origin <your-remote-url>Work one branch at a time, together. Because the working tree is shared but each side keeps its
own .git, keep the same branch checked out on both machines. A branch switch rewrites the
working files, and that rewrite syncs — checking out a different branch on one side rewrites the tree
the other side is also using. Decide on a branch, work it on both, and switch together. After pulling
new commits, a git pull on the other side catches its local Git bookkeeping up to the files that
already synced.
Harden repos for two machines. Two settings keep a shared checkout from breaking on paths that differ between your Mac and the workspace:
-
Use relative worktree paths so a linked worktree does not pin an absolute path that only exists on one machine:
git config --global worktree.useRelativePaths true -
Keep absolute machine-specific paths out of repo-local config (
.git/config, hooks). They resolve on one side and break on the other.
For the fuller picture — auto-bootstrapping remotes and following branches safely — that is planned, not shipped. Today the discipline above is the contract.
Uninstall
Remove the login service at any time:
duetfs sync uninstallThis stops the daemon and removes its launchd agent. It leaves your mirrored folders under
~/duet/ exactly as they are — uninstalling stops syncing, it does not erase your files. Re-running
uninstall when nothing is installed succeeds quietly.
The daemon mirrors every workspace you belong to; there is no per-workspace off switch in this version, and detaching a workspace by hand would only see it re-added on the next reconcile while your membership holds. To stop mirroring one workspace, leave that workspace from the web app — losing membership detaches its mirror on its own. To cut a lost machine off everywhere at once, revoke its sync-device credential, also from the web app.