A native macOS menu bar app that keeps local folders in two-way sync with a
Hetzner Storage Box over SFTP.
No rclone, no mounts, no helper tools: one Swift app, one SFTP connection per run.
The app's interface is in German for now. The code, comments and this README are in English. BoxSync is an independent project and is not affiliated with Hetzner.
Mounting a Storage Box as a drive (WebDAV, SSHFS, rclone mount, GUI mount tools) can be fragile:
writes may stall or get lost when the connection drops, and caches may go stale. BoxSync does it the boring way: it keeps a real local copy
of each folder, compares it with the Box, and only moves what changed.
- Several folder pairs, each with its own Box folder, exclusions and state. Example:
~/Documentsto the Box root and~/Notes(an Obsidian vault) toNotes/. - Two-way merge with a stored base state per file: a change on one side is copied to the other,
a change on both sides keeps the newer file and stores the other as
Name (Konfliktkopie YYYY-MM-DD HHMM).ext. - Never deletes for good. A deletion on one side moves the file to a trash on the other:
_trash/<date>/on the Box,~/.Trash/BoxSync/<date>/locally. Overwritten versions go there too. - First sync needs your go. The first run of a folder pair only shows the plan (uploads, downloads, conflicts). Nothing moves until you press Erstabgleich starten.
- Check only. Nur prüfen scans both sides and shows what a sync would do, without changing anything.
- Safe transfers: atomic uploads through a temp file and rename, resume after an interruption, SHA-256 verification, whole-second mtime comparison (SFTP v3), rename detection without re-upload.
- Mass-delete guard: a run that would trash a large share of known files at once is stopped.
- Security: the password lives in the Keychain only. The host key is pinned on first connect (trust on first use); a changed key aborts the connection. Paths coming from the Box are validated so a hostile name cannot leave the sync folder.
- Unicode aware: names are compared in NFC, so macOS (NFD) and Linux/iOS (NFC) spellings do not duplicate files.
- Change detection through FSEvents plus a timer (default 5 minutes), launch at login, notifications for errors and conflicts.
- macOS 14 or newer, Xcode 16+ / Swift 6 toolchain
- A Hetzner Storage Box with SFTP (port 23) and password login
scripts/build-app.sh # builds build/BoxSync.app (signed, see "Limits" for the certificate)
open build/BoxSync.appFor development: swift run BoxSyncApp (no notifications outside an app bundle).
On first start open Einstellungen… and enter host (uXXXXX.your-storagebox.de), user, password,
and add your folders. The Box folder field maps a local folder to a folder on the Box (empty = Box root).
Exclusions are one pattern per line: dir/** (everything below dir), a bare name or glob (any path component),
or a glob with / (whole relative path).
Never point two sync pairs at overlapping Box folders. If one pair maps the Box root, exclude the other pair's
folder there (for example Notes/**).
BoxSyncApp --check scans every folder pair in check-only mode (nothing is changed on either side), prints a report
and writes it to a file. No menu, no window, no clicks:
~/Applications/BoxSync.app/Contents/MacOS/BoxSyncApp --check # report in ~/Library/Logs/BoxSync/check.txt
~/Applications/BoxSync.app/Contents/MacOS/BoxSyncApp --check --out ~/check.txtExit code: 0 everything in sync, 1 work pending or an error, 2 the report could not be written.
The command runs inside the app binary so it uses the same Keychain approval as the menu bar app.
It opens its own connection, so it can run while the app is running.
swift test # unit tests only, no networkThe integration tests talk to a real Storage Box. They only create and remove folders under _boxsync_test/.
Give them a Box of your own through the environment:
BOXSYNC_TEST_HOST=uXXXXX.your-storagebox.de BOXSYNC_TEST_USER=uXXXXX BOXSYNC_TEST_PASSWORD=... swift testWithout the three variables they are skipped.
BOXSYNC_DEMO=1 build/BoxSync.app/Contents/MacOS/BoxSyncApp starts the app with invented data that touches neither
disk nor network. That is how the images in docs/images were made.
An Obsidian vault can be shared with the iPhone through the Remotely Save plugin (WebDAV). See docs/obsidian-iphone.md.
- Password login only (no SSH keys yet). Symlinks and empty folders are not synced.
- No on-demand download or automatic eviction: every folder is a full local copy.
A File Provider extension (placeholders) is the planned next step; the sync core is UI-free and sits behind the
SyncEngineprotocol. - Orphaned
.partfiles from interrupted uploads are not cleaned up automatically. - Without a signing certificate, builds are signed ad hoc, so every rebuild is a new app identity for the Keychain.
macOS then asks for your login Keychain password ("BoxSync wants to use confidential information") after each rebuild.
Choose Always Allow. Until you answer, the app waits at start-up.
Run
scripts/make-signing-cert.shonce: it creates a self-signed code-signing certificate "BoxSync Local Signing" in your login keychain, andscripts/build-app.shthen signs every build with it. The identity stays the same, so the prompt appears only once more after the switch (checked: a rebuild and reinstall afterwards raised no prompt). Remove it withsecurity delete-identity -c "BoxSync Local Signing". - Names that differ only in letter case (
Report.mdandreport.md, also folders) cannot coexist on the default case-insensitive macOS volume. BoxSync mirrors one spelling (the one you already have, else the byte-wise first), reports the other as an error and never touches or trashes it. Rename it on the Box to bring it down. A local case-only rename (report.mdtoReport.md) is treated as a new file and leaves the old name on the Box. - The interface is German only.
See ARCHITECTURE.md (German) for layers, the planner and the state store.
The sync logic is a pure function (SyncPlanner.plan) and is unit-tested without a network.

