LocalSend Has a CLI Now, and It's Exactly What Testing Needs
LocalSend wants a GUI on both ends. lsq doesn't — which turns out to be exactly what you want when you're pushing a test build to a phone.
If you’ve read the DocFlow writeup, you know I spend a lot of time moving files onto a phone — mostly debug APKs I want a tester (or myself) to install right away. My go-to for that has been LocalSend : open source, end-to-end encrypted, works entirely over local Wi-Fi with no account and no cloud step in between.
The one thing LocalSend has never had is a proper CLI. Every transfer means unlocking a phone, opening the app, tapping the right device, tapping accept. Fine once. Annoying the tenth time you’re re-sending a build after fixing a crash.
That’s what lsq fixes. It’s an unofficial, Rust-built command-line client that speaks the same LocalSend protocol (v2.1), so it talks to the normal app just fine — you can push a file from your phone to a headless server, or the other way around, without a GUI on either end.
Installing lsq
If you’re on Arch, it’s on the AUR:
yay -S lsq-binOtherwise, build it from source with a recent Rust toolchain:
cargo install --path .There’s also a Dockerfile if you’d rather run it as a receiver on a box that’s always on:
docker build -t lsq .
docker run --network host -v ~/inbox:/data lsq⚠️ Important Exception: Discovery uses UDP multicast, so the Docker container needs
--network host. Without it,lsq listwill just come back empty even though the app is running fine.
Sending a file
See who’s on the network first:
lsq listThen send:
lsq send app-debug.apkIf there’s more than one device around, lsq asks which one. To skip that and target a specific device:
lsq send app-debug.apk --to "Pixel 8"--to also accepts an IP address or ip:port, which is handy if discovery isn’t picking up the peer for some reason.
Receiving on a headless machine
This is the part that actually solves my original problem. On a build server or a Pi with no display, run:
lsq receive --dest /srv/inbox --yesThat turns it into an always-on receiver that accepts everything without a confirmation prompt.
💡 Why This Matters: LocalSend’s whole design assumes a person is sitting at both ends to tap “accept.” That’s the right default for a phone, but it’s a dead end for a CI box or a server you SSH into.
--yesis what actually makes lsq useful there.
Pushing to devices without LocalSend installed
The one I use most for testing: share hosts the files over plain HTTP and gives you a link, so a tester can grab the APK from a browser even if they don’t have LocalSend installed at all.
lsq share app-debug.apkThis prints a http://... URL. Anyone on the same network opens it and downloads directly — no app install required on their end.
💡 Quick Check: Open the printed URL yourself from another device on the same Wi-Fi first. If it loads, you’re good; if it times out, check that your machine’s firewall isn’t blocking the port lsq is listening on.
If you want to pull from the other direction — grabbing files someone else is sharing — that’s:
lsq pull "Kitchen Pi"Locking it down with a PIN
Since share serves over plain HTTP (browsers reject LocalSend’s self-signed certs, so it can’t use HTTPS the way app-to-app transfers do), it’s worth a PIN if you’re not on a fully trusted network:
lsq share app-debug.apk --pin 123456⚠️ Important Exception: The plain-HTTP link is what makes
sharework in any browser, but it also means the transfer itself isn’t encrypted the way a normal LocalSend-to-LocalSend send is. Fine on your home network, worth thinking twice about on a shared or public one.
Where this fits
For DocFlow specifically, this has quietly become part of my test loop: build the debug APK, lsq share it from my dev machine, scan or tap the link on the test phone, done — no cable, no cloud upload, no waiting on a GUI to notice the phone showed up. It’s a small thing, but it’s the kind of small thing that adds up when you’re iterating fast.
lsq still has some gaps — no IPv6, and it only covers protocol v2.1 — but for pushing files to and from devices on the same network from a terminal, it’s the piece LocalSend was missing.