Ferry
Open source · Apache-2.0 Pre-release No server, ever

Stop pasting .env files into Slack.

Ferry is a private channel between machines your team already trusts — paired once, verified by a phrase you read aloud, always there after. Send a file, a secret or a message straight from one machine to another: no cloud, no account, no server in the middle, and nothing sitting in a searchable chat log forever. Secrets carry a TTL and burn after the first open; every exchange lands in a content-free audit log instead.

It isn't a chat app and it isn't AirDrop. It's the channel for the things that shouldn't be in either — credentials, keys, anything with an expiry date.

Free and open source, Apache-2.0. One daemon, three ways in: the ferry command line, a VS Code extension, and a desktop app.

 # machine A ferry send laptop-b ./build/app.tar.gz ferry send laptop-b ./.env --burn # gone once they open it 
Download the desktop app How it works Read the source
316 Docker pulls 19 GitHub release downloads

Pre-release software

The wire format and the on-disk format are still changing, and none of it has had an outside security review yet. Play with it, don't depend on it — and don't put anything through it that you'd be upset to lose or leak. Compatibility between versions isn't promised until 1.0.

Why bother

Slack has excellent search. That's the problem.

A cloud drive, a paste site, Slack, email — the secret lands on a third party's disk, gets indexed, gets backed up, and is one breach, one export or one overzealous search away from being found by someone who never should have seen it. Ferry doesn't do any of that. The data leaves your machine and arrives on theirs, and that's the whole trip. There's no server to break into, no channel history to purge, nothing to subpoena.

The trade-off is real

Ferry only works when both machines can actually reach each other — the same office network, a phone hotspot, a VPN. There's no relay and there never will be, so it won't punch through the internet for you. If that's what you need, Ferry isn't the tool.

Straight answers

What it won't do

How it works

Security you can point at

One identity key per machine

Made the first time the daemon runs, never written to disk in the clear. Kept in the OS keychain, or in an age-encrypted file on a headless box.

Pairing is a deliberate ceremony

Someone reads a short code across; then both people confirm the same verification phrase shows on both screens. Never skipped, never defaulted to yes, never bypassed by “trust this network”.

The handshake hangs up on strangers

Every connection starts with a Noise handshake that drops any key not already in your roster — before it reads a single command. Enforced in the transport, never only in the UI.

Sealed with age, burned for real

Files are sealed with age. Secrets get their own throwaway key on top, and “burn after reading” shreds the wrapped key, then the ciphertext, then the row — an interrupted burn leaves unopenable ciphertext, never an openable orphan.

Expiry runs on the receiver's clock

TTL and burn-after-read are measured on the receiver's monotonic clock from the moment of delivery — never on a timestamp the sender supplied. If the clocks disagree after a reboot, the item expires.

The audit log is content-free

It records that something happened, who, which item, when, and the outcome — and nothing about what it contained.

The full security model →

Three surfaces

One local daemon, three ways to reach it

The daemon is the only thing that speaks the peer-to-peer protocol. Everything you touch talks to it over a local socket and never to another machine directly.

Command line

The ferry and ferry-daemon binaries. ferry send, ferry receive, ferry pair, ferry activity — any command starts the daemon itself on first use, except ferry status, which only ever reports the truth.

VS Code extension

Brings its own daemon — nothing else to install. A sidebar and a full panel, both the same app: pairing, an inbox, the send panel and a secret composer, all inside the editor.

Desktop app

A Tauri shell around the same UI, for machines without an editor in the loop. The daemon ships inside the installer as its own sidecar — nothing separate to run.

Pairing lives in the CLI, not the headless daemon — it needs a human to read a verification phrase aloud, and a step that structurally needs a person can't live in a process that structurally can't reach one.

The interface

The CLI, the extension and the desktop app render the same UI

One React app, three hosts. Shown here with mock data.

Ferry — the Pair a device dialog
Pairing is a dialog you can't click past. An out-of-band code, then a verification phrase both people confirm on their own screen — never skipped, never defaulted to yes.
Ferry — the Peers / Roster screen with fingerprints and reachability
The roster: the machines you've paired with, by key fingerprint, with how recently each was reachable. Anything not on this list is refused at the handshake.
Ferry — the Send screen with the transaction summary
Send a file, a secret or a short message to one peer. The summary states the TTL, whether it burns on the first open, and that it's sealed with age over a Noise XK transport.
Ferry — the Inbox showing a pending secret offered for acceptance
What's arrived. A secret is offered, not dropped on you — you accept or reject it before it's written anywhere.
Ferry — the Activity log of content-free audit events
The activity log: what happened, who, which item, when, and the outcome — including a denied auth request and an expired outbox item — and nothing about what was inside.
Ferry — a message thread with a paired peer
Short notes with a peer, threaded and delivered like everything else — over the same paired, Noise-encrypted connection, not a separate chat backend.
Ferry — Settings, this device's identity and the daemon's runtime state
This device: the identity fingerprint, the auto-accept policy, language, and the daemon's own runtime — listen port, data directory, discovery and transport health — all read straight off the same local socket everything else uses.

Opt-in

GitHub is opt-in, and stays off if you never opt in

By default Ferry makes no network call except a direct transfer to a peer. Two features can talk to GitHub's API — and only after you set remote_features_enabled = true and connect an account. Nothing leaves for GitHub before that, Ferry never falls back to GitHub on its own, and leaving it off costs you nothing but those two features.

Publish (to GitHub's gist API)

You choose, per item, to put an already-sealed copy on GitHub as a private gist and hand someone the link — for when you can't get on the same network. The trade-off is real: the ciphertext leaves your machine, burn-after-read can no longer be honoured, and GitHub learns the recipient's public key and the file size.

Team roster (from a URL you choose)

Point Ferry at a repo file — any URL you control. It fetches the signed roster, shows you who signed it and the exact add/remove diff, and imports only on a second confirmation.

Every exchange with GitHub is a choice you make for that one action. There is no background sync, no telemetry, and no automatic upload.

Architecture

The compiler owns the layering

Eight Rust crates in one Cargo workspace. Dependencies only ever point one way, so a violation is a build error, not a code-review habit.

dependency direction

daemon / cli  →  core  →  net / store / crypto  →  proto
crates/proto

Types, enums and error codes. Zero logic. The TypeScript bindings are generated from here; CI fails on any diff.

crates/core

Every domain decision. Drivable from the daemon, the CLI, or a fake-network test harness without change — never mentions a socket, a database, or React.

crates/net · store · crypto

The machinery: framing and transport, SQLite, the audited primitives.

crates/daemon · cli

Wire it together and nothing else.

crates/remote

The only crate that knows HTTP exists — behind the off-by-default GitHub features.

How “done” is measured

Nothing counts until it's run on two real machines

Ferry keeps a verification ledger: one row per user-facing flow, and a row is only green once it's been driven end to end on two real machines on a real network, including a disconnect and reconnect where that matters. Code compiling, unit tests passing, and the two-node harness passing all earn a lower mark — they tell you about the state machine, not about a hotspot with a sleeping laptop on it.

A full pair → send → burn → open cycle runs today between two daemons on one host, and between two containers on a multicast network in CI. Every flow is driven that way before it counts as working.

350+ Rust tests · in-memory two-node harness · two-container CI run · Windows + macOS builds

Getting it

One release pipeline, five ways to get it

Every channel below is produced from the same tagged release. Pick whichever fits — download an archive, pull an image, or build it yourself.

Command line

.tar.gz (Linux/macOS) or .zip (Windows) with ferry + ferry-daemon inside, executable bit intact. Extract, drop on your PATH.

Releases →

Docker

docker pull ghcr.io/firaslamouchi21/ferry, mirrored on Docker Hub. CLI and UI in one image, no Rust or Node toolchain.

View the image →

Desktop app

.deb / .AppImage / .rpm on Linux, .dmg on macOS, .msi / a setup .exe on Windows — the daemon ships inside. Unsigned for now; see what that means.

Release notes & checksums →

VS Code extension

Not on the Marketplace yet — grab the platform .vsix from the release and code --install-extension ferry-.vsix. Bundles its own daemon.

Releases →

build from source

 git clone https://github.com/firaslamouchi21/Ferry cd Ferry cargo build --release -p ferry-daemon -p ferry cargo test --workspace bash testing/local-two-peer.sh 

or the one-liner: ./dev.sh builds the daemon + CLI and opens the UI against them, no Docker or separate terminals.

A first run

From the terminal, start to finish

 ferry daemon start ferry pair listen --name laptop-a ferry pair connect 192.168.1.20:53124 482913 --name laptop-b ferry send laptop-b ./.env --burn ferry receive list ferry receive open <item-id> --out ./.env 

ferry send just writes the item to a local queue; the daemon delivers it the next time the other machine is around. A headless box — a CI runner, a NAS — keeps its identity in an encrypted file and gets paired from a laptop before it's deployed.

Contributing

A rough change that respects the invariants is easy to merge

There's a short list of things Ferry won't bend on — no server ever, roster enforced at the handshake, pairing always a code plus a confirmed phrase, real state on every item, content-free audit log, crypto from reviewed primitives only. A change that quietly breaks one gets sent back no matter how useful it is; everything else is welcome.

Apache-2.0, and also sold as packaged builds — contributions come in under the same terms, with a DCO sign-off (git commit -s).

Contact

Talk to Firas about Ferry

A bug, an idea, a security concern, or you want to help move Ferry toward v2 — this goes straight to Firas's inbox and you'll get an auto-reply to confirm. For vulnerabilities, use the address in SECURITY.md instead of this form.