Syncing across machines

protonfs is designed so several machines can sync the same directory tree to the same Proton Drive location. This page describes that workflow and the pitfalls worth knowing.

The sync contract

A protonfs-managed directory keeps its state in .protonfs/:

  • config.json — the remote root and defaults. Shared: commit it so every clone syncs to the same place.

  • ignore — which files are in scope (gitignore syntax). Shared: commit it so every clone syncs the same set.

  • index.json — this machine’s record of what it has uploaded (local hashes, mtimes, remote paths). Local only — gitignore it; it is rebuilt per device and would only cause churn and false conflicts if shared.

protonfs setup writes this split for you: a .protonfs/.gitignore that ignores index.json (and the transient refresh-state.json) while keeping config.json and ignore tracked, plus a .protonfs/.gitattributes that exempts these small control files from git-LFS — so a clone without an LFS pull gets the real config rather than pointer stubs. Both are written idempotently and preserve any lines you add yourself.

Setting up a subdirectory of a larger git repo is safe: setup runs the repo-wide git-LFS migration only when the protonfs root is the git toplevel. In a subdirectory it skips migration (and leaves any git-LFS pointer files there untouched); pass --migrate-lfs to force it or --no-migrate-lfs to always skip. setup also creates the configured remote_root on Drive if it does not exist yet (it must live under /my-files), so the first push works without hand-creating folders.

First-time setup on a client

cd <managed-dir>
git pull                 # get the shared contract (.protonfs/config.json + ignore)
protonfs doctor          # verify the proton-drive binary + OS keyring (add --fix if headless)
protonfs auth login      # only if not already authenticated
protonfs refresh         # learn what is already on Drive -> seeds THIS machine's index

refresh is the key cross-client primitive: metadata-only, no download, it makes a fresh machine aware of everything already on Drive so it will not re-upload what another client already sent.

Uploading data

protonfs status              # what is local-only vs locally-indexed (index only)
protonfs status --remote     # the same, checked against Drive
protonfs push <subpath>      # a subtree
protonfs push                # everything in scope that is new or changed

Important

A bare push is the normal form. A file that changed locally since this machine last pushed it is sent as a new revision of the same Drive file (Drive keeps the earlier revision in its version history), provided the remote copy is still the one this machine recorded. A file that changed on both sides is reported as a conflict instead, and only then is --resolve needed.

--resolve local (replace) moves the existing remote file to the trash and uploads a new one in its place, rather than adding a revision. Use it for a genuine conflict you want settled in favour of the local copy, not as a default.

Verifying an upload

proton-drive can report a file as transferred when it never landed, so push re-lists every directory it uploads into and only records a file once the remote copy’s plaintext size matches the local one. A file that fails that check is reported (under-delivered, or unverified when the listing carries no size at all), left unrecorded, and retried by the next push. The exit code is 1 whenever any file was not verified; with -v, compare the planned count on the uploading files=N line with transferred= on the summary line.

Removing files

protonfs rm <path>        # trash the remote copy (reversible)
protonfs rm -f <path>     # trash, then permanently delete

rm -f has one permanent limitation. proton-drive addresses a trashed node for permanent deletion by its path under /trash (/trash/<basename>), and offers no working way to target a specific trashed node by its stable UID. So when two or more trashed items share a basename, protonfs cannot safely tell which one is yours and refuses to permanently delete — it leaves the item trashed (still reversible) and tells you so.

To resolve a duplicate-basename case, empty that specific item from trash via the Proton Drive app or web UI, or just leave it trashed (trash is reversible, so nothing is lost). This is an upstream constraint, not a protonfs choice; a live probe test flags it automatically if a future proton-drive lifts it.

Keeping clients in sync

The standard loop on any client:

git pull                          # shared contract
protonfs refresh                  # reconcile local index with Drive
                                  #   (--prune drops entries for files deleted on Drive)
protonfs status                   # local-only / locally-indexed / conflict / ...
protonfs push                     # send local-only and locally-changed files up
protonfs pull --refresh           # bring remote-only down (if this machine wants them)

Resolving a divergence on pull

A file that was edited locally and changed on the remote since the last sync is a divergence. A bare pull never touches such a file — it leaves it in place, reports it, and exits non-zero — so a local edit is never silently overwritten. Choose a side explicitly with --resolve:

protonfs pull --resolve remote <path>   # overwrite the local copy with the remote one
protonfs pull --resolve local <path>    # keep local (it stays queued for the next push)
protonfs pull --resolve both <path>     # fetch the remote copy as <name>.remote to merge

--resolve=both writes the remote version alongside your file under a .remote suffix (untracked) so you can diff and merge by hand, then delete the suffixed copy. Files that changed only on the remote (your local copy is still in sync) are brought down normally by pull --resolve <any>; no local edit is at risk there.

status also sets an exit code so an unattended caller can branch without parsing the printed counts:

  • 0 — clean: every file is locally-indexed, metadata-only (deliberately not materialised here) or an LFS pointer stub (nothing to reconcile).

  • 1 — drift: non-conflict divergence exists (something to push, pull, or prune).

  • 2 — conflict: at least one file changed on both sides (both-modified, only with --remote), or changed locally with no remote view to say whether the remote moved too (conflict). A bare push uploads a conflict file as a new revision when the remote still holds the copy this machine recorded, and reports a real conflict otherwise; status --remote tells you which first.

Conflict outranks drift when both are present, e.g.:

protonfs status; case $? in
  0) echo "matches the index" ;;
  1) echo "drift -- run push/pull" ;;
  2) echo "conflict -- resolve first" ;;
esac