@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.

Packages

Package details

extensionskill

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

pi.dev sending files over 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: https listens 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