Peer-to-peer file transfer, chat, folders and terminals. No account, no server holding your data.
Three things, and one verb for each: people, the machines each of them has, and the topics on each machine — a chat, a folder, an inbox, a note, a terminal.
drop person add # show your code; they take it with `drop person add <code>`
drop machine add # show a code a new machine of yours takes the same way
drop topic add work folder # a folder called work on this machine; --on adds it to anotherA machine becomes yours because your key signs it. The key is an SSH key you already have, or the
one in your YubiKey — drop me key lists what this machine has, and u in the interface picks one.
A machine then joins you one of three ways:
- it holds your key — the same SSH key, or your YubiKey held to it — and finds your other machines by itself, within a minute or two, with no code;
- a one-time code that a machine holding your key shows with
drop machine add; - one of yours asks it, on this network or among the devices you added, and its person says yes.
After that either device can reach the other from anywhere: across NATs, across networks, through address changes.
A device on the same network needs no code at all: drop nearby lists it, the phone shows it under
Nearby, and its person says yes with the same number on both screens. Your machines keep one
address book between them — whatever you add, rename, trust or remove on one of them happens on all
of them.
The Android app is the same node and the same three screens, with chat, files, and a live view of a terminal on the machine it is connected to.
A machine serves namespaces — the topics every screen shows: paths, each with a rule about who may reach it. What opening one does is its archetype — and the two are kept apart on purpose.
drop.mount("/", { access = "paired" })
drop.mount("/inbox", { type = "share", dir = "~/Downloads" })
drop.mount("/papers", { type = "files", dir = "~/papers" })
drop.mount("/standup", { type = "note", file = "~/notes/standup.md" })
drop.mount("/logs", { type = "stream", command = "journalctl -f" })
drop.mount("/term", { type = "tty", shell = "/bin/sh", input = false })Seven ship. Adding one is writing an implementation and registering it — nothing in the namespace layer, the config reader, the protocol or the wire gains a case. Write one in Lua and both ends load the same file; a machine that does not have it opens the namespace as whatever it says it speaks like, rather than failing.
bob:laptop:/chat bob's laptop, its /chat
laptop:/chat the machine called laptop
bob::/chat bob — whichever machine of his answers
/chat this machine
That last-but-one line works because pairing is with a person, not a machine. Both sides write down the other's user key, so a machine of theirs you have never met arrives with a name.
Identity comes from the hardware — a TPM if there is one, otherwise a board or drive serial.
$ drop me machine
named by the serial on the drive the system is on
survives a reinstall, because nothing about it is written downWipe the disk and it comes back as the machine it was, with no backup and nothing to restore. Carry a backup to another box and it does not, because the key was never in the backup. Replacing a machine is a signed handover the old one makes while it still can, so nobody has to pair again.
A note is one file, edited by several people; a files folder is a directory several people work
in. Changes are signed, causally ordered, and merged three ways — the same shape git merge-file
uses, and checked against it with 30,000 randomised merges.
$ drop path create /standup note --set file=~/notes/standup.md --share --keep
$ drop path join tron:/standup --at /standup --set file=~/notes/standup.mddrop never becomes an editor. It notices a save, signs what was saved, and writes back what all the changes together make.
One document per subject in docs/: how it works, what it cannot do, and where it
lives in the tree. Every claim in them was checked against the source or a running build, on two
machines of different architectures.
| Start here | a walkthrough: one identity across three machines, from nothing to talking |
| How it is put together | the layers, one daemon, and what happens when somebody opens a path |
| What names a machine | identity from the hardware, several people on one machine, moving to another |
| Namespaces and archetypes | an instance, a meaning, and the rule that keeps them apart |
| The wire | frames, the opening, and the shape each archetype speaks afterwards |
| The command line | people, machines and topics, and an address that reads right to left |
| The archetypes | share, files, chat, note, link, stream, tty — what each is for |
| Sharing a terminal | one shell and many watchers, the screen rather than a replay |
| The interface | people, then machines, then topics — and managing somebody |
| One thing, several machines | signed changes, merging, and how a history stays small |
| An archetype in Lua | a plugin both ends load, and the sandbox it runs in |
| Access rules | the vocabulary, how it inherits, and what a refusal means |
| Pairing | recognition against trust, badges, and being found without being findable |
| On the disk | where everything lives, the vault, and one writer at a time |
| Hardening | what is bounded before anybody is let in, and what is not defended |
| Configuration | init.lua, and the settings that are not namespaces |
| Testing it | three suites, fuzzing, and two real machines |
Nix. The flake provides a static Linux package for x86_64 and ARM64:
nix build .#drop
nix run . -- --helpUse inputs.drop.url = "github:termworks/drop/vX.Y.Z"; in another flake and add
drop.packages.${system}.default to its packages. Replace vX.Y.Z with a released
tag whose cache workflow has passed. Accept the flake's cache configuration when
prompted, or run cachix use termworks on the consumer machine. Consumers using
Drop as a flake input also need the cache configured on their machine.
Each package and architecture uses one stable pin, such as
drop-x86_64-linux, with --keep-revisions 5. The five newest pin revisions
are protected from cache cleanup; each binary retains its actual package version.
Older revisions become eligible for garbage collection and may need rebuilding.
The shared termworks cache is published only on v* tag pushes. Its workflow
builds on native x86_64 and ARM64 runners, checks version, help and static linkage,
uploads and pins the runtime closures, and verifies fresh downloads with builders
disabled. Publishing requires a Cachix per-cache write token in the repository's
CACHIX_AUTH_TOKEN Actions secret. Never commit that token. Different source
revisions or build inputs may require a new build.
Build. One Go module; the build is .make.lua, run with oslo make.
make build # the release binary, into ./drop
make test # the suite
make verify # fmt-check, check, test, build — the whole local gate
make install # into $PREFIX/bin, which defaults to ~/.localThe toolchain comes from the flake — nix develop, or direnv allow and it loads on cd.
Run. drop serve keeps the node reachable; install it as a user service so the device is
reachable whenever it is on.
install -m 0644 misc/drop.service ~/.config/systemd/user/
systemctl --user enable --now dropUse.
drop the interface: a person, a machine, a topic
drop topic ls bob:laptop what they share with you
drop connect bob:laptop:/chat open whatever is there
drop file get bob:laptop:/papers/thesis.pdfWith no config at all, drop serves a small default: /inbox to send to, /chat to talk in, /open
for links — and nothing that hands over a directory, runs a command or shares a terminal, because
those are decisions. misc/init.lua is a worked example with one namespace of
every archetype in it.
Discovery, pairing, transfer, shared folders and shared notes work, on amd64 and arm64.
- identity taken from the hardware, so a reinstall comes back as the same machine
- one machine, several accounts, each reachable as itself
- moving to new hardware without pairing again
- pairing over a one-time 60-bit code, hashed into its own rendezvous
- a private rendezvous per pair, rotating hourly
- transfer with blake3 verification, resume, and paired-only acceptance
- a push carries files, not whole directories, yet
The TPM path is written and has never run — neither machine this was built on has one. See what names a machine.