Case study
sshsyncer
Go CLI that deploys a git branch over rsync+SSH with every credential held in a dedicated 1Password vault. Pinned-key auth, ownership-guarded vaults, exactly one biometric prompt per deploy.
- Go
- Cobra
- Charm (bubbletea, huh, lipgloss)
- 1Password CLI
- rsync
- SSH
- GoReleaser

What it is
A command-line deploy tool for the "one repo, one server" case. The kind of project where a full CI/CD pipeline is overkill and a shell script is a security hazard. sshsyncer keeps every credential (host, user, port, and the SSH key itself) inside a single 1Password vault. The .sshsyncer.json file you commit to the repo contains only op:// references; resolving them requires the user to be signed in to 1Password with access to the vault.
Only the last commit pushed to origin/<branch> is ever deployed. Local uncommitted changes never reach a server.
Why it exists
Most "deploy via SSH" scripts have at least one of these problems:
- Keys live on every developer's laptop, copied around manually.
- Credentials live in a shared file (or, worse, environment variables).
- The SSH agent offers every key it holds until one matches, which is slow and prompts for biometric approval on each attempt.
The third one is the interesting bug. SSH defaults to MaxAuthTries=6, which means if you have seven keys in your agent and the right one is at position eight, auth fails before it's tried. With 1Password's SSH agent you also get a biometric prompt for every key offer until one succeeds. Acceptable for a daily ssh me@server; unacceptable for a deploy tool that should feel like git push.
Pinned-key auth
sshsyncer reads the public key from the SSH Key item that the Server item points at, then forces the SSH client to offer exactly that one key:
- For rsync, it passes
-o IdentitiesOnly=yes -i /tmp/sshsyncer-pub-XXX.pubso SSH only offers the one key from the agent. - For the native Go SSH path used by
server testandlog, it filtersagent.Signers()down to the signer whose public key matches.
Result: exactly one biometric prompt per deploy, regardless of how many keys the user has loaded.
The ownership guard
A 1Password vault that holds deploy credentials should not also hold anything else. Mixing items risks a destructive operation hitting an unrelated personal item. sshsyncer tags every item it creates with the literal string sshsyncer, and refuses to operate on a vault containing items it did not create. The vault is sshsyncer's, or sshsyncer walks away.
This is enforced in internal/op/vault.go at every setup, share, and deploy operation. It's the kind of constraint that costs nothing in code and saves you from a 3 a.m. mistake.
Two auth modes
Detected at runtime in internal/op/auth_mode.go:
- Desktop app. 1Password 8 with the SSH agent enabled. Used for local development.
- Service account.
OP_SERVICE_ACCOUNT_TOKENset in the environment, with a separate SSH agent (e.g. GitHub Actions'webfactory/ssh-agent) atSSH_AUTH_SOCK. Used in CI.
sshsyncer doesn't care which mode is active as long as the pinned public key is loaded into whatever agent is on the socket. Same binary, same .sshsyncer.json, both contexts.
Architecture
Thin Cobra handlers under cmd/, doing nothing beyond flag parsing and orchestration. Real work happens in internal/:
internal/op/wraps the 1Password CLI (op://Vault/Item/Fieldreferences, vault CRUD, ownership guard, auth-mode detection).internal/git/handles branch detection, fetch, push-check, and archive.internal/rsync/builds argv, parses dry-run output, and runs the command.internal/ssh/is a Go-native SSH client with key pinning.internal/deploylog/appends and tails the remote.sshsyncer-log.
Everything that shells out (op, git, rsync, SSH) is gated behind a small interface or accepts an io.Writer, so the tests can fake it without subprocesses. The 1Password test suite uses a fakeRunner interface and runs entirely without the real op CLI installed.
Distribution
GoReleaser builds release artefacts for linux and darwin, amd64 and arm64. A Homebrew tap is wired up at davidjonidev/homebrew-tap with the formula depending on rsync and git. Both are gated until v1.0.
What I'd change
The Go SSH path falls back to InsecureIgnoreHostKey() on machines with no ~/.ssh/known_hosts. That's a documented limitation, not a design choice: first-time machines lack the data needed to verify the host. The right fix is to bootstrap host keys into the vault alongside the SSH Key item, so the first connection is verifiable. On the v1.0 list.