@patimweb/pi-localsend
LocalSend extension for the pi coding agent. Send and receive files over the local network with any LocalSend device, without a cloud service.
Package details
Install @patimweb/pi-localsend from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@patimweb/pi-localsend- Package
@patimweb/pi-localsend- Version
1.3.0- Published
- Sep 2, 2026
- Downloads
- 530/mo · 39/wk
- Author
- patimwep
- License
- MIT
- Types
- extension, skill
- Size
- 852.9 KB
- Dependencies
- 1 dependency · 0 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
],
"skills": [
"./skills"
],
"image": "https://raw.githubusercontent.com/Smotherer007/pi-localsend/main/screenshot.png"
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
pi-localsend

LocalSend extension for the pi coding agent.
Send and receive files between pi and any device running LocalSend — phone, tablet, or another computer — directly over the local network. No account, no cloud, nothing leaves the LAN.
Implements the LocalSend protocol v2.1 natively, with no runtime dependency beyond typebox.
Installation
# Install from npm
pi install npm:@patimweb/pi-localsend
# Install from local path during development
pi install /path/to/pi-localsend
Quick Start
Nothing to configure — every setting has a working default.
# See what is on the network (LocalSend must be open on the other device)
localsend_devices
# Send
localsend_send:
to: Pats iPhone
files: [/Users/pat/report.pdf]
# Receive one transfer
localsend_receive:
timeoutSeconds: 300
localsend_receive prints a PIN and the address to use, then waits. Enter the PIN on the sending device.
No background service
This is the design constraint the extension is built around: there is no daemon. Sockets are open only while a tool is running, and each tool closes what it opened before it returns.
| Tool | What it opens | When it closes |
|---|---|---|
localsend_devices |
UDP socket + short-lived HTTP server | when the scan ends (seconds) |
localsend_send |
outbound connections only | when the transfer ends |
localsend_receive |
HTTP(S) server + announcement socket | after one transfer, or at the timeout |
localsend_receive accepts exactly one session and then shuts down. Nothing survives the tool call, and nothing survives the pi session.
Tools
| Tool | Description |
|---|---|
localsend_setup |
Change alias, download directory, PIN policy, transport, port. Optional. |
localsend_status |
Show current settings and the addresses other devices can reach this machine at. Opens no ports. |
localsend_devices |
Scan the network for LocalSend devices and list their aliases and addresses. |
localsend_send |
Send files or a text snippet to a device, by alias or IP address. |
localsend_receive |
Wait for exactly one incoming transfer, save the files, shut down. |
Sending
localsend_send:
to: Pats iPhone # alias from localsend_devices
files:
- /Users/pat/report.pdf
- /Users/pat/screenshots # a directory sends the files directly inside it
# pin: "123456" # if the receiving device asks for one
Text goes straight across without touching disk first:
localsend_send:
to: 192.168.1.42
text: "The staging URL is https://staging.example.com"
# textFileName: staging.txt
When discovery does not work (guest Wi-Fi, VPN, firewall), give an address instead of an alias. Direct addresses default to port 53317 and https, which is what the LocalSend app uses.
Receiving
localsend_receive:
timeoutSeconds: 300 # default 5 minutes, max 1 hour
# downloadDir: ~/Desktop # just for this transfer
# noPin: true # accept without a PIN
The call blocks and returns the listening port, this machine's addresses, and a generated six-digit PIN. One window accepts one transfer; several files in a single send are fine.
Incoming file names are attacker-controlled, so they are sanitised before anything is written: path separators and traversal are stripped, and a name that collides gets a (2) suffix rather than overwriting. A sender that pushes more bytes than it declared is cut off.
Commands
| Command | Description |
|---|---|
/localsend |
Scan the network for LocalSend devices. |
/localsend-receive [minutes] |
Open a receive window (default 5 minutes). |
Skills
| Skill | Purpose |
|---|---|
localsend-transfer |
Finding devices, confirming the right target before sending, surfacing the PIN while the receiver is still waiting, and what to check when a device does not appear. |
Configuration
Settings are saved to ~/.pi/localsend-config.json (mode 0600) by localsend_setup. Only overrides are stored; anything unset falls back to a default.
| Setting | Default |
|---|---|
alias |
pi on <hostname> |
downloadDir |
~/Downloads, or the home directory if that does not exist |
deviceType |
desktop |
protocol |
http |
port |
0 — a free port is picked per transfer |
requirePin |
true |
Encryption
LocalSend normally runs over HTTPS with a self-signed certificate, where the device fingerprint is the hash of that certificate. Node cannot generate certificates on its own, and a crypto library is a lot of dependency for one optional feature, so:
protocol: http(default) is what this machine listens with. The protocol supports it; the fingerprint is then a stable random id.protocol: httpslistens with TLS instead.
Either way a certificate is generated on first use and cached in ~/.pi/localsend/, because sending to a LocalSend app always requires one regardless of this setting — see below.
The certificate is built in process (src/x509.ts) rather than by shelling out to openssl: a sender that cannot present a certificate cannot talk to a LocalSend app at all, and depending on a binary that is routinely absent on Windows would make the extension unusable there. Nothing in this package spawns an external process, so Windows, Linux and macOS behave identically.
It is an end-entity certificate (CA:FALSE) with a subjectAltName, clientAuth and serverAuth in its extended key usage, and a 365-day lifetime. None of that is cosmetic: LocalSend verifies the certificate a sender presents, and a webpki-based verifier (rustls) rejects a CA certificate, one without a SAN, or one without clientAuth — the handshake then ends in a certificate unknown alert (46). Apple platforms separately refuse server certificates valid for more than 398 days. A cached certificate that fails these checks, including one written by an earlier version, is detected and replaced automatically.
When connecting to a peer over https, certificate chain validation is off by design: LocalSend peers are self-signed, and the protocol's trust anchor is the fingerprint, not a CA.
Sending to an encrypted peer also needs a certificate on this side. The LocalSend app identifies the sender by the fingerprint of the certificate it presents, so its TLS server requests one; a sender that presents nothing never gets past the handshake and sees a tlsv13 alert certificate required (alert 116). This extension therefore presents its own certificate when talking to an https peer, and reports that certificate's SHA-256 as its fingerprint rather than the random id used in http mode — including when the local protocol setting is http.
Running alongside the LocalSend app
The LocalSend desktop app owns UDP/TCP 53317 while it runs. This extension listens on a free port instead and advertises it in its announcement, so both can coexist — the test suite covers receiving a transfer while 53317 is occupied. The one consequence is that scans may miss devices that answer by multicast rather than HTTP, because the multicast listening port is taken; the tools report that as a note rather than an error. Your phone will simply list both the app and this extension as separate devices.
Development
npm install
npm test
npm run test:coverage
The test suite includes a real loopback transfer: it starts the receiver, sends files to it through the send client, and asserts on what landed on disk — including path traversal, duplicate names, PIN rejection and the concurrent-session case.
License
MIT
