lsend is a scriptable LocalSend-compatible command-line peer. It discovers
official LocalSend devices, sends files or text, and receives files in the
foreground without requiring a TUI.
The protocol implementation is a pinned snapshot of the official LocalSend
Rust core. lsend supplies the command contract, persistent identity,
non-interactive output, receive policy, and safe file publication around it.
This is an independent project and is not affiliated with or endorsed by the LocalSend maintainers.
The first interoperability milestone is implemented:
- LocalSend Protocol v2.2 over HTTPS with mutual TLS.
- Persistent certificate identity and certificate-fingerprint pinning.
- IPv4 and IPv6 multicast discovery, including scoped IPv6 channels.
- Direct HTTPS/HTTP targeting, aliases, and fingerprints.
- File and text sending with PIN support and cancellation.
- Foreground receiving with prompt or explicit auto-accept policy.
- Streaming transfers, optional sender checksums, receiver size/hash checks, sanitized names, temporary files, and collision-safe publication.
- Human output and JSON Lines events with stable error classes.
Pairing, browser share links, directory recursion, packaged releases, and a
doctor command remain later milestones.
- CLI reference
- Troubleshooting
- Architecture and security invariants
- Interoperability and testing
- Why a new CLI instead of
localsend-rs - Contributing
The documentation index separates user and maintainer guides.
Rust 1.93 or newer is required. The toolchain file selects the tested version.
cargo build --locked --release
cargo install --locked --path .The official core is vendored, so a clean build does not need to clone the large upstream application repository or its Flutter submodule.
The first command generates a private identity, which may take several seconds.
Later invocations reuse it from $XDG_CONFIG_HOME/lsend/identity.pem or
~/.config/lsend/identity.pem.
Find devices:
lsend discover
lsend discover --jsonSend files by alias or direct address:
lsend send --to "My Phone" report.pdf photo.jpg
lsend send --to alias:"My Phone" report.pdf
lsend send --to https://192.168.1.40:53317 report.pdfSend text explicitly:
lsend text --to "My Phone" -- "hello from the terminal"Receive with confirmation prompts:
lsend receive --output ~/DownloadsReceive one transfer non-interactively on a trusted network:
lsend --json receive \
--accept all \
--once \
--output ./incomingProtect the receiver with a PIN:
lsend receive --accept all --pin 123456
lsend send --to workstation --pin 123456 report.pdfLSEND_PIN can provide the sender or receiver PIN without placing it directly
in shell history. The value is never included in events or logs.
| Input | Meaning |
|---|---|
My Phone |
Case-insensitive alias or exact fingerprint discovered on the LAN |
alias:My Phone |
Explicit alias, including aliases containing : |
fingerprint:ABCD... |
Explicit full certificate fingerprint |
192.168.1.40 |
Direct HTTPS on port 53317 |
192.168.1.40:54444 |
Direct HTTPS on the given port |
https://host.example:54444 |
Direct HTTPS hostname |
http://192.168.1.40:53317 |
Explicit unencrypted HTTP |
There is no automatic HTTPS-to-HTTP downgrade. HTTP must be requested explicitly and produces a warning.
--accept prompt is the default and requires a terminal. Machine-readable
receive mode deliberately refuses that ambiguous combination:
--json receive requires --accept all
--accept all trusts every device that can authenticate to the listener and,
when configured, provide the correct PIN. It should be used only on a trusted
network.
Completed files are written under the selected output directory. Remote path components are discarded and illegal file-name characters are sanitized. Uploads first go to hidden temporary files and are only published after their declared size and optional SHA-256 checksum pass.
Collision policies:
rename(default):report.txt,report (1).txt, and so on.reject: fail the individual upload without changing the existing file.overwrite: atomically replace the existing destination where supported by the platform filesystem.
--json writes one JSON object per event to stdout. Diagnostics and tracing
remain on stderr. Typical send events are:
{"event":"resolving","target":"phone"}
{"event":"accepted","sessionId":"...","acceptedFiles":1,"acceptedBytes":42}
{"event":"fileComplete","fileName":"note.txt","size":42}
{"event":"complete","kind":"files","files":1,"bytes":42}The receiver emits ready before accepting traffic. With --port 0, its
port field contains the operating-system-selected port.
| Code | Meaning |
|---|---|
| 0 | Complete success |
| 1 | Internal failure |
| 2 | Invalid command input |
| 3 | Target not found or ambiguous |
| 4 | Declined, busy, or rate limited |
| 5 | PIN/authentication failure |
| 6 | Network or TLS failure |
| 7 | Size/hash/integrity failure |
| 8 | Cancelled or partial multi-file result |
cargo fmt -p lsend --check
cargo clippy -p lsend --all-targets --no-deps -- -D warnings
cargo test -p localsend --features full --locked
cargo test -p lsend --lockedSee CONTRIBUTING.md for change discipline and the interoperability guide for the release matrix and official-core update procedure.
The test suite includes the official core's unit and integration tests, direct
official-core oracle tests in both directions, and a real two-process
multi-file lsend -> lsend transfer. These tests open HTTPS listeners, use
independent persistent identities, complete mutual-TLS handshakes, pin receiver
certificates, stream file bodies, and compare the received bytes.
Self-interoperability is not treated as sufficient for releases. The next release gate is a manual and then automated matrix against current official desktop and mobile builds in both directions.
The current package is intentionally publish = false: the vendored official
core is not an independently published dependency. Install from a source
checkout with cargo install --path .; release archives and package-manager
formulas are a later milestone.
The exact source and update policy are recorded in
vendor/localsend-core/UPSTREAM.md and
NOTICE.md. Core updates must be imported as a reviewed snapshot
and pass the full bidirectional compatibility suite before the recorded
revision changes.