Zero-config terminal messenger for LAN networks — no servers, no cloud, no setup.
Zero-config terminal messenger for devices on the same LAN.
No servers. No cloud. No accounts. No setup. Just run it.
/share; receiver accepts or rejects; pause, resume, and cancel supported at any point/focus locks onto one peer so you can type freely without prefixing every message/all sends a message to every online peer in one command/history shows recent messages, optionally filtered by peer/notify or --no-notify--space <name> isolates a group of peers so only same-space instances discover each other--profile# Global install (recommended)
npm install -g lnchat
lnchat
# One-off with npx (no install needed)
npx lnchat
# Check version
lnchat --version
Requirements: Node.js 18+. Works on macOS, Linux, and Windows.
$ lnchat
_ _ _
| |_ __ ___| |__ __ _| |_
| | '_ \ / __| '_ \ / _` | __|
| | | | | (__| | | | (_| | |_
|_|_| |_|\___|_| |_|\__,_|\__|
v2.0.0
ℹ Logged in as anish#3fa1 (profile: default)
ℹ Connected to LAN
ℹ Your IP: 192.168.1.42
ℹ TCP messaging port : 9000
ℹ UDP discovery port : 41234 (range 41234–41238)
Type /help for available commands.
>
On first launch you are prompted for a nickname. Your identity — nickname, stable UUID, TLS cert, and Ed25519 signing keys — is saved to ~/.lnchat/profiles/ and reused automatically on every subsequent run.
When another lnchat instance appears on the network:
● rahul#c2d9 joined the network
| Command | Description |
|---|---|
/list |
Show discovered peers with discriminators, IPs, and latency |
/msg <name[#disc]> [text] |
Send a message; use name#disc when names clash |
/ping <name[#disc]> |
Ping a peer and show round-trip time |
/focus <name[#disc]> |
Enter focused chat — all plain text goes to that peer |
/back |
Exit focused chat and return to the global prompt |
/all <text> |
Broadcast a message to every online peer |
/history [name] |
Show recent messages (all peers, or filtered by peer name) |
/notify |
Toggle desktop notifications on/off |
/clear |
Clear the terminal screen (local only) |
/help |
Show available commands |
/exit |
Quit |
File transfer
| Command | Description |
|---|---|
/share <name> <file> |
Offer a file to a peer (drag the path from Finder/Files into the terminal) |
/share <file> |
Offer to focused peer (focus mode shorthand) |
/share all <file> |
Broadcast a file offer to all online peers (confirmation required) |
/accept [id] |
Accept an incoming file offer (id optional when only one is pending) |
/reject [id] |
Decline a file offer |
/cancel [id] |
Cancel an active transfer (id required if both sides are transferring) |
/pause [id] |
Pause an active transfer |
/resume [id] |
Resume a paused transfer |
/transfers |
List all active, queued, and pending transfers |
/downloads [path] |
Show or change the download directory (default: ~/Downloads) |
> /list
1. Rahul#c2d9 192.168.1.11 8ms
2. Priya#8ab3 192.168.1.55
> /msg Rahul deployment done?
[14:02] You → Rahul#c2d9: deployment done?
> /msg Priya
Messaging Priya — type your message:
> quick question about the PR
[14:03] You → Priya#8ab3: quick question about the PR
When two peers share the same nickname, use the 4-character discriminator:
> /msg Rahul hi
⚠ Multiple peers named "Rahul". Use the discriminator: Rahul#c2d9, Rahul#7f1e
> /msg Rahul#7f1e hi
[14:04] You → Rahul#7f1e: hi
/focus locks the prompt onto one peer so you can have a real back-and-forth without typing /msg on every line:
> /focus Rahul
ℹ Focused on Rahul#c2d9. Type /back to return.
@Rahul#c2d9> hey, you around?
[14:10] You → Rahul#c2d9: hey, you around?
@Rahul#c2d9> what's the status on the deploy?
[14:10] You → Rahul#c2d9: what's the status on the deploy?
@Rahul#c2d9> /back
ℹ Left conversation with Rahul#c2d9.
>
Slash commands still work normally while in focus mode. If the focused peer goes offline, focus exits automatically with a notice.
> /all standup in 5 minutes
[14:15] You → everyone: standup in 5 minutes
Send any file to a peer with /share. The transfer is encrypted over a dedicated TLS data connection.
> /share Rahul ~/Desktop/report.pdf
⏳ Hashing report.pdf…
📎 [8cd5] Offer sent to Rahul#c2d9 — report.pdf (2.3 MB). Waiting for response…
[8cd5] Rahul#c2d9 accepted. Opening data port…
✔ [8cd5] Sent report.pdf to Rahul#c2d9 (2.3 MB)
On Rahul’s side:
📎 [8cd5] anish#3fa1 wants to send report.pdf (2.3 MB). /accept 8cd5 or /reject 8cd5
> /accept 8cd5
✔ [8cd5] Received report.pdf (2.3 MB) → /Users/rahul/Downloads/report.pdf
In focus mode the peer name is implicit:
@Rahul#c2d9> /share ~/Desktop/report.pdf
You can drag a file from Finder or your file manager into the terminal and the shell will paste the path; no need to type it out.
While a transfer is running a progress bar appears above the prompt. Use /pause and /resume to throttle without losing progress, or /cancel to abort. /transfers shows the state of all concurrent transfers.
To broadcast a file to everyone on the network:
> /share all /path/to/slides.pdf
Send slides.pdf (5.1 MB) to 3 peers: Rahul#c2d9, Priya#8ab3, Dev#f12a. Proceed? (y/n): y
📡 Broadcast offer sent to 3 peers. Waiting 15s for responses…
Change where received files are saved (persisted to your profile):
> /downloads ~/Documents/lnchat-files
ℹ Downloads directory set to: /Users/anish/Documents/lnchat-files
While typing in focus mode (or composing a message via /msg), a live indicator appears on the recipient’s terminal:
● Rahul#c2d9 is typing...
It disappears the moment the message arrives or after a few seconds of inactivity.
> /history
[13:45] Rahul#c2d9: good morning
[13:46] You → Rahul: morning!
[14:02] You → Rahul: deployment done?
> /history Priya
[14:03] You → Priya#8ab3: quick question about the PR
lnchat fires a native OS desktop notification for every incoming message — useful when the terminal window is behind other apps or minimised.
osascript; no install needed. Grant notification permission when prompted (System Settings → Notifications → Terminal).notify-send (install with sudo apt install libnotify-bin if missing).\x07) always sounds regardless of notification state.Toggle notifications at runtime:
> /notify
ℹ Desktop notifications off.
> /notify
ℹ Desktop notifications on.
Slash commands are highlighted as you type:
/command → cyanpeername (first argument to /msg, /focus, /ping, /history, /share) → bold yellow| Shortcut | Action |
|---|---|
Esc Esc |
Clear the current input line |
↑ / ↓ |
Navigate command history |
Each profile is an independent identity with its own UUID, nickname, and cryptographic keys.
# Default profile
lnchat
# Named profiles — two instances can run simultaneously
lnchat --profile work
lnchat --profile personal
# Start fresh: re-prompt for nickname, generate new keys
lnchat --new-account
lnchat --profile work --new-account
# List all saved profiles
lnchat --list-profiles
# Remove a profile and all its keys permanently
lnchat --remove-profile work
--space restricts peer discovery to instances that share the same space name. Peers without the flag (or with a different name) are invisible to each other.
# Everyone in this space sees each other; default-space peers are hidden
lnchat --space team-alpha
# Combine with --profile to separate identities too
lnchat --profile alice --space dev
lnchat --profile bob --space dev # sees alice
lnchat --profile carol --space staging # does NOT see alice or bob
When --space is given, lnchat prompts for an optional passphrase:
Passphrase for space "team-alpha" (Enter to skip): ••••••••
The passphrase is never stored. It is combined with the space name using PBKDF2 to derive an opaque token, and that token is what gets broadcast in HELLO packets. Only peers who enter the same space name and the same passphrase derive the same token and can see each other. Pressing Enter skips the passphrase — the plain space name is used, which is the same behavior as before and fully compatible with older versions.
Peers with no passphrase, the wrong passphrase, or an older version of lnchat all land in their own silently-isolated groups. Nobody receives an error — they simply don’t see the protected peers.
The space name (and derived token) is included in the Ed25519-signed HELLO packet, so it cannot be forged or stripped by an attacker.
| Flag | Description |
|---|---|
--profile <name> |
Select a named profile (default: default) |
--new-account |
Ignore saved profile data and create a fresh identity |
--list-profiles |
Print all saved profiles and exit |
--remove-profile <name> |
Delete a profile and all its cryptographic keys, then exit |
--factory-reset |
Delete all lnchat data (~/.lnchat/) — prompts for yes to confirm |
--space <name> |
Restrict peer discovery to instances using the same space name |
--port <n> |
Bind the TCP messaging server to a specific port (default: first free port in 9000–9009) |
--no-notify |
Start with desktop notifications silenced (toggle later with /notify) |
--version / -v |
Print the installed version and exit |
--remove-profile deletes everything about that identity:
device UUID, TLS cert + key, Ed25519 signing keys. On next launch, a fresh UUID and new keys are generated. Other peers will treat you as a brand-new device.
lnchat --factory-reset
Removes ~/.lnchat/ entirely — all profiles, all keys, and the peer trust store. Requires typing exactly yes at the prompt. This cannot be undone.
lnchat is designed for trusted LAN environments. Here is exactly what is and is not protected.
Encryption in transit
All messages travel over TLS using a self-signed RSA-2048 certificate generated once per profile. TCP message traffic cannot be read by plain-text sniffers on the network.
Peer authentication (signed HELLOs)
Every UDP discovery broadcast is signed with an Ed25519 private key unique to your profile. The receiver verifies the signature and rejects unsigned or forged packets. A 30-second timestamp window prevents replay attacks.
Trust On First Use (TOFU)
The first public key seen for a device ID is stored in ~/.lnchat/known_peers.json. If a subsequent HELLO arrives for the same device ID with a different public key, it is rejected and you see:
⚠ Security warning: Rahul (abc12345…) sent a HELLO with a different
public key — possible impersonation. Message rejected.
Space isolation
The --space name is part of the signed payload. An attacker on the network cannot forge or strip the field to inject peers across space boundaries.
~/.lnchat/profiles/. Physical access to your machine is outside lnchat’s threat model.| File | Contents |
|---|---|
~/.lnchat/profiles/<name>.json |
Device UUID and nickname |
~/.lnchat/profiles/<name>-cert.pem |
TLS certificate (public) |
~/.lnchat/profiles/<name>-key.pem |
TLS private key |
~/.lnchat/profiles/<name>-sign-priv.pem |
Ed25519 signing private key |
~/.lnchat/profiles/<name>-sign-pub.pem |
Ed25519 signing public key |
~/.lnchat/known_peers.json |
TOFU store — trusted peer public keys |
Each instance sends a UDP HELLO packet every 5 seconds containing the device ID, nickname, discriminator, TCP port, TLS fingerprint, Ed25519 public key, signature, space name, and a timestamp. Packets are sent to:
127.0.0.1 on ports 41234–41238 — reaches other lnchat instances on the same machine; bypasses the macOS Application Firewall (loopback is always allowed)192.168.1.255) — reaches peers on the same LANFive ports are used so multiple instances on the same machine each bind their own exclusive port without conflict. Peers that stop heartbeating are evicted after 15 seconds.
Each instance runs a TLS server, binding to the first free port in the range 9000–9009 (or a specific port via --port). If all ten are taken it falls back to an OS-assigned port. Messages are short-lived TLS connections directly to the peer’s IP and port; they are newline-delimited JSON objects. The TLS connection verifies the peer’s certificate against the fingerprint announced in the HELLO — a mismatch closes the connection immediately.
A persistent UUID is generated once per profile and stored in ~/.lnchat/profiles/<name>.json. The 4-character discriminator (e.g. #3fa1) is the first 4 hex characters of the UUID — deterministic, stable, and unique enough to distinguish peers with the same nickname.
macOS
Linux
ufw: sudo ufw allow 41234:41238/udp && sudo ufw allow 9000:9009/tcpiptables: iptables -A INPUT -p udp --dport 41234:41238 -j ACCEPT && iptables -A INPUT -p tcp --dport 9000:9009 -j ACCEPT--port <n> to pin a specific port.See CONTRIBUTING.md for development setup, testing guidelines, code style, and how to submit a pull request.
Anish Shekh — @anishhs-gh
Repository — github.com/anishhs-gh/lnchat
lnchat is source-available with a non-compete restriction. You may freely use, modify, study, and contribute to the project. You may not publish the software — or a substantially similar derivative — to npm or any other package registry, nor offer it as a competing hosted service or tool.
See LICENSE for the full terms.