| name | localsend-go |
| description | Move files, text, and URLs across the LAN by driving localsend-go — a Go LocalSend implementation that runs as an agent-driveable CLI and a headless daemon, and interoperates with the official LocalSend apps. This skill is the operator's guide: how to send to a peer by alias, run a receiving node, discover peers over multicast, manage trust, and branch on the machine-readable exit-code and JSON contract. USE WHEN send a file over the LAN, send to a phone/laptop, localsend, local send, receive files, run a file-drop daemon, discover LAN peers, agent-to-agent file transfer, share text or a URL to a device, push a build artifact to another host, wire a received-file event into a pipeline, manage localsend trust/peers. NOT FOR cloud/internet file transfer (localsend-go is strictly LAN-local), continuous folder sync, or anything needing a GUI. |
localsend-go — operator skill
localsend-go moves files, text, and URLs across the local network with verified, atomic delivery,
and interoperates with the official LocalSend apps. You drive it as a CLI or run it as a daemon.
Every command is built to be run by a program: machine-readable output with --json, deterministic
exit codes, and no interactive prompts.
Repo: github.com/mattjoyce/localsend-go. Protocol: LocalSend v2. LAN-local only — there is no
relay and no cloud path.
First: locate or build the binary
Prefer an installed binary; fall back to building from the repo.
LSG="$(command -v localsend-go || true)"
if [ -z "$LSG" ]; then
( cd ~/Projects/localsend-go && go build -o /tmp/lsg ./cmd/localsend-go )
LSG=/tmp/lsg
fi
"$LSG" version
The contract you branch on
Always pass --json when a program consumes the output. Always check the exit code — it is a
stable 4-value contract:
| Exit | Meaning | Do |
|---|
0 | success | proceed |
1 | operational failure (network, I/O, verification) | retry, or surface the error |
2 | usage error (bad flags/args) | fix the invocation; do not retry unchanged |
3 | trust refusal (peer not authorised) | peers add to vouch, then retry; never blind-retry |
An exit 3 refusal on --json gives you everything needed to recover:
{"ok":false,"error":"peer_not_authorised","reason":"not_allowlisted",
"peer":{"alias":"printer","address":"192.168.1.20:53317","announced_fingerprint":"ab12…","verified":false},
"hint":"verify the fingerprint on the device, then: peers add printer --fingerprint ab12… and retry."}
Parse reason to decide the recovery, and use peer.announced_fingerprint if you choose to vouch.
Recipes
Send to a peer by alias (the common case)
Discovery, fingerprint pinning, and HTTPS are automatic. No IP, no fingerprint to hardcode.
"$LSG" send --to my-node --text "hello agent" --json
"$LSG" send --to my-node --url "https://x.example" --json
"$LSG" send --to my-node ./report.pdf ./data.bin --json
On exit 3, the peer is not yet trusted at your configured level — see Trust below.
Send to an explicit address (no discovery)
"$LSG" send --addr 192.168.1.20:53317 --tls --pin <FINGERPRINT> ./file
"$LSG" send --addr 127.0.0.1:53317 --text "smoke test"
Run a receiving node (daemon)
serve is an HTTPS receiver plus a multicast announcer sharing one certificate identity, so other
peers can discover it by alias and pin its fingerprint. It streams a typed JSON event on every
received transfer — consume stdout, or point it at a sink.
"$LSG" serve --inbox ./inbox --alias my-node --json
"$LSG" serve --inbox ./inbox --alias my-node --webhook https://hook
Each received-file event is a line of JSON; treat its arrival as the signal that a verified file is
in --inbox. Do not watch the folder — the event is the source of truth, and the file is already
whole (atomic rename) and integrity-checked when the event fires.
For a one-shot receiver without discovery: receive --addr 127.0.0.1:53317 --inbox ./inbox --json.
Preflight a node (doctor)
Before deploying serve on a host — or when a node misbehaves — run doctor with the same flags
the deployment uses. Six probes (interfaces, multicast join, port bind, identity, config, inbox
writability + free space) run the production code paths; exit is 1 iff any check fails.
"$LSG" doctor --json
"$LSG" doctor --inbox /var/lib/localsend/inbox --config /etc/localsend.yaml && echo deployable
A warn on the port check usually means your own daemon already holds it — health, not disease.
Discover peers on the LAN
Finds localsend-go nodes and official LocalSend apps that are announcing.
"$LSG" discover --duration 10s --json
"$LSG" discover --iface eth0 --duration 10s --json
Output includes each peer's alias, address, port, and self-asserted fingerprint. Treat the
discovered fingerprint as presence only — trust binds at the TLS handshake, not the announcement.
Manage trust (peers)
"$LSG" peers list --json
"$LSG" peers add my-node --fingerprint <FP> [--ttl 720h]
"$LSG" peers remove my-node
The trust ladder for send --to (via --trust, default tofu):
strict — allowlist only; send fails with exit 3 unless the peer was explicitly peers added.
confirm — refuse on first contact, print the fingerprint + a peers add hint; you vouch, then retry.
tofu — auto-pin on first contact and remember (LocalSend's interactive default).
failsafe — strict with a short TTL, re-verify periodically.
Invariant at every level: a changed fingerprint for an already-trusted alias is always refused,
never silently re-pinned. If a device legitimately rotated its key, peers remove then re-add.
Choosing the pattern
- One-off push to a known device →
send --to <alias>.
- A standing drop box that reacts to incoming files →
serve and consume the event stream.
- Unattended agent-to-agent with no first-contact risk →
serve --require-peer <fp> on the
receiver and --trust strict on the sender, with both fingerprints exchanged via peers add.
Troubleshooting
- Start with
doctor — it answers interface/multicast/port/identity/config/disk questions in
one shot, with the production code paths, before you theorize.
- Receiver returns 507 — the disk precheck: the declared payload would breach the node's
--min-free floor (default 256 MiB). Free space or raise/disable the floor.
send --to returns exit 3 — trust, not network. Read reason and hint; vouch with
peers add or use --trust tofu for an interactive first contact.
send --to finds no peer — the target must be announcing (serve, not receive), on the same
subnet (multicast does not cross subnets), and reachable on the chosen --iface.
- Official app cannot send to a localsend-go node — check
doctor: a node whose identity was
created by a pre-1.0 dev build carries an ed25519 cert the apps' TLS stacks cannot handshake
(this was misdiagnosed as a macOS defect for a while — it is OS-independent). Rotate: delete the
node's cert.pem/key.pem, restart, re-pin the new fingerprint on peers.
- A file did not appear — check the exit code and the event stream, not the folder; a non-
0
exit or a missing event means the transfer did not verify, and nothing partial is left behind.
Boundaries
- LAN-local only — no internet or relay transfer.
- No continuous sync, versioning, or conflict resolution — this is discrete send/receive.
- No GUI or TUI — output is for humans reading a terminal or programs reading
--json.