lnchat
v2.0.0
Published
Zero-config terminal messenger for LAN networks
Maintainers
Readme
lnchat
Zero-config terminal messenger for devices on the same LAN.
No servers. No cloud. No accounts. No setup. Just run it.
Features
- Zero config — peers discover each other automatically over UDP broadcast; no IP addresses, no pairing codes
- Encrypted in transit — all messages travel over TLS with self-signed certificates per identity
- Authenticated peers — every discovery packet is signed with Ed25519; forged or replayed HELLOs are rejected
- Trust On First Use (TOFU) — the first public key seen for a device is trusted; a changed key triggers a security warning
- File transfer — send any file peer-to-peer with
/share; receiver accepts or rejects; pause, resume, and cancel supported at any point - Focused chat —
/focuslocks onto one peer so you can type freely without prefixing every message - Broadcast —
/allsends a message to every online peer in one command - Typing indicators — a live "● Alice is typing..." line appears and disappears in real time
- Message history —
/historyshows recent messages, optionally filtered by peer - Desktop notifications — native OS notification on every incoming message; toggleable with
/notifyor--no-notify - Spaces —
--space <name>isolates a group of peers so only same-space instances discover each other - Multiple profiles — run different identities simultaneously with
--profile - Input syntax highlighting — slash commands are coloured cyan, peer names bold-yellow, as you type; gracefully handles long paths that exceed the terminal width
- No runtime dependencies — ships as a single self-contained JS file (~70 kB)
Install
# Global install (recommended)
npm install -g lnchat
lnchat
# One-off with npx (no install needed)
npx lnchat
# Check version
lnchat --versionRequirements: Node.js 18+. Works on macOS, Linux, and Windows.
Quick start
$ 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 networkCommands
| 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) |
Sending messages
> /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 PRWhen 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: hiFocused chat
/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.
Broadcast
> /all standup in 5 minutes
[14:15] You → everyone: standup in 5 minutesFile transfer
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.pdfIn focus mode the peer name is implicit:
@Rahul#c2d9> /share ~/Desktop/report.pdfYou 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-filesTyping indicators
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.
Message history
> /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 PRDesktop notifications
lnchat fires a native OS desktop notification for every incoming message — useful when the terminal window is behind other apps or minimised.
- macOS — uses
osascript; no install needed. Grant notification permission when prompted (System Settings → Notifications → Terminal). - Linux — uses
notify-send(install withsudo apt install libnotify-binif missing). - The terminal bell (
\x07) always sounds regardless of notification state.
Toggle notifications at runtime:
> /notify
ℹ Desktop notifications off.
> /notify
ℹ Desktop notifications on.Input highlighting
Slash commands are highlighted as you type:
/command→ cyanpeername(first argument to/msg,/focus,/ping,/history,/share) → bold yellow- rest of the text → normal
Keyboard shortcuts
| Shortcut | Action |
|---|---|
| Esc Esc | Clear the current input line |
| ↑ / ↓ | Navigate command history |
Profiles and CLI flags
Multiple profiles
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 workSpaces
--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 bobWhen --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.
Full flag reference
| 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 |
Removing a profile
--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.
Factory reset
lnchat --factory-resetRemoves ~/.lnchat/ entirely — all profiles, all keys, and the peer trust store. Requires typing exactly yes at the prompt. This cannot be undone.
Security
lnchat is designed for trusted LAN environments. Here is exactly what is and is not protected.
What's 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.
What's not protected
- UDP discovery packets are visible to anyone on the network (they are signed, but the content — nickname, IP, port — is readable).
- TOFU first contact — if an attacker impersonates a peer before you ever see the real peer, they become trusted. Subsequent key changes will be flagged.
- Local storage — profile data, TLS keys, and signing keys are stored unencrypted in
~/.lnchat/profiles/. Physical access to your machine is outside lnchat's threat model. - Network scope — lnchat is LAN-only by design and is not hardened for use over the internet.
Identity file layout
| 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 |
How it works
Discovery — UDP broadcast
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.1on ports 41234–41238 — reaches other lnchat instances on the same machine; bypasses the macOS Application Firewall (loopback is always allowed)- The subnet broadcast address of every active network interface (e.g.
192.168.1.255) — reaches peers on the same LAN
Five 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.
Messaging — TCP + TLS
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.
Identity
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.
Firewall notes
macOS
- Same-machine discovery uses loopback — always bypasses the Application Firewall.
- Cross-machine discovery uses subnet broadcast. The macOS firewall may show "Do you want the application node to accept incoming network connections?" on first run — click Allow.
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- lnchat tries ports 9000–9009 in order; the actual bound port is shown at startup. Use
--port <n>to pin a specific port.
Contributing
See CONTRIBUTING.md for development setup, testing guidelines, code style, and how to submit a pull request.
Author
Anish Shekh — @anishhs-gh
Repository — github.com/anishhs-gh/lnchat
License
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.
