An interactive rsync wrapper (CLI-based) for moving files selectively between a local machine and a remote dev environment over SSH. Cherry-pick your sync!
(see §Testing for more information)
When working across a local machine and a remote dev environment over SSH, the workflow for moving files back and forth is a little clumsy. You either have to memorize rsync parameters or just move everything and sort through the details later. Plain scp has no incremental mode. What's missing is the middle step: see what's different and pick what moves.
This is a common situation for anyone working with SSH-accessible dev boxes, such as cloud instances, containers, Raspberry Pis, WSL-to-host, etc. At least one macOS app exists for this purpose, but I couldn't find anything else that is cross-platform, runs directly on the command line, and has a rich terminal UX ⌨️ 🤓
An interactive CLI that wraps rsync to provide a select-then-sync workflow:
- Compare local and remote directories (using
rsync --dry-run --itemize-changes). - Automatically exclude
.gitand honor.gitignoreentries - Show a human-readable list of differences (new/modified/deleted).
- Choose to sync all, none, or a subset of individual files.
- Execute the transfer for only the selected files (using
rsync --files-from) and report the outcome.
- rsync does the heavy lifting. This tool is a UX layer, not a reimplementation. It parses rsync's output and drives rsync's parameters for selective transfer.
- SSH is the transport. No additional daemon or agent on the remote side. If you can
sshto it, this tool works. - No opinion on direction. Push and pull are both first-class. Bidirectional diff display is a future goal.
- Minimal dependencies.
rsyncandsshmust be present on both sides. The tool itself should be easy to install.
rsyncandsshon both the local and remote machines- Go ≥ 1.26.3 if you want to build from source, otherwise prebuilt binaries are available
Download the binary for your platform from the latest release, make it executable, and put it on your PATH:
$ chmod +x cherry-sync_<version>_<platform> # match the version and platform you downloaded
$ [sudo] mv cherry-sync_<version>_<platform> /usr/local/bin/csyncEach release also publishes a checksums.txt that you can use to verify the download against. The binary is self-contained; run csync --license to print the license terms it ships with.
The macOS binaries are code-signed and notarized with Apple, so Gatekeeper allows them to run without any extra steps. The first run of a browser-downloaded binary needs a working network connection, because macOS verifies the notarization with Apple at that point.
Clone this repo or download the latest from main, and then:
$ go build ./cmd/csyncProduces a csync binary at the repo root.
$ csync ./local-dir user@host:/remote-dir$ csync user@host:/remote-dir ./local-dircsync compares the two directories and lets you choose what to transfer. In a terminal you get an interactive picker: arrow keys to move, space to toggle, a for all/none, Enter to sync, ctrl-c to cancel.
When input or output is redirected (such as a pipe, a file, CI), csync falls back to a typed prompt on stderr (so that the report on stdout stays uncluttered), where:
- press Enter (or
a) to sync every change norctrl-cto cancel- or pick a subset of changes by their numerical label:
- a single number
- a range like
1-3 - a comma list like
1,3 - or any combination thereof (
1-2,4)
If the two directories are identical, csync reports there is nothing to sync and exits cleanly. An invalid invocation prints an error that explains the problem and points to csync --help, then exits with code 2.
csync --help (or -h) prints a usage summary and exits. csync --version prints the version and exits. Both take precedence over any other arguments.
To avoid retyping a remote you sync with often, save it into .csync.toml in the project directory:
remote = "user@host:/remote-dir"Then, from that directory, csync push sends the project to the saved remote and csync pull brings it down:
$ cd ./local-dir
$ csync pushThe push/pull verbs take no other arguments. csync never offers the .csync.toml itself for transfer — like .git, it is held out of the comparison and reported as excluded. If the file is missing, malformed, or sets no remote, csync says so and exits non-zero rather than guessing.
csync logs every run that gets as far as comparing directories. These can be found in $XDG_STATE_HOME/cherry-sync/, or ~/.local/state/cherry-sync/ (when that variable is unset). At the end of the run, csync reports the name of the file it wrote, so you know exactly which one it was.
The directory is created 0700 and each log 0600 to keep details about your work tree private from others on the machine. The logs are plain text, and csync automatically caps the total number at 25 of the most recent. Each run deletes the oldest beyond that, and names what it removed in the log it is currently writing.
If the log cannot be written at all (for example, the state directory is read-only, or XDG_STATE_HOME points at something that isn't a directory) csync will let you know, but continues normally. The run ends with a Not logged: line explaining why, in place of the usual Log: line.
Near-term enhancements are tracked as open enhancement issues on GitHub.
Further out: bidirectional diff (showing which side is newer) and conflict flagging when a file has changed on both sides.
See the CHANGELOG for what has shipped in each release.
Disclaimer: I am a real person with many years of software engineering experience. I personally came up with this idea on my own, and I am the one driving the product design, reviewing code, managing issues, and maintaining documentation; however, I am working with Claude to write the code, suggest verbiage, and analyze security issues on this project. The evidence of this is sprinkled throughout.
The development process is mainly described in TESTING, with additional concerns covered in STYLE and SECURITY. Automated testing and a thorough CI workflow have been in place since the beginning in order to assure quality and reliability. The Gherkin specifications found under acceptance_tests/features/ are the canonical definition of expected behavior and usage for this application.
The project includes two workflows:
- On every PR and commit to
main, the unit and Gherkin tests are run (referred to as "Acceptance") against current GNU/Linux, as well as current and previous macOS; and - On every release, a quick smoke test is run against each built binary to ensure they are executable on the target platform.
| Environment | rsync implementation |
|---|---|
| linux/amd64 | GNU |
| macOS/arm64 | system (macOS 15, discovered) |
| macOS/arm64 | openrsync (macOS 26) |
Each test workflow run publishes a detailed report to the job summary, which includes rsync identity, Gherkin-versus-unit breakdown, per-package results, and any failures.
To run these tests locally:
go test -count=1 ./...The
-count=1is deliberate: the suite builds and execs thecsyncbinary rather than importing it, so Go's test cache doesn't notice production-code changes and a plaingo test ./...can report a stale pass. See TESTING for the details.
| Platform | How it runs |
|---|---|
| linux/amd64 | native runner |
| linux/arm64 | ephemeral Azure VM |
| macOS/amd64 | Rosetta on an arm64 runner |
| macOS/arm64 | native runner |
MIT — see LICENSE.