LnChat — Terminal messenger for your LAN

Zero-config terminal messenger for LAN networks — no servers, no cloud, no setup.

View the Project on GitHub

lnchat

Zero-config terminal messenger for devices on the same LAN.
No servers. No cloud. No accounts. No setup. Just run it.

npm CI Node License GitHub


Features


Install

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


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 network

Commands

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

Focused 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 minutes

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

Typing 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 PR

Desktop notifications

lnchat fires a native OS desktop notification for every incoming message — useful when the terminal window is behind other apps or minimised.

Toggle notifications at runtime:

> /notify
  ℹ  Desktop notifications off.

> /notify
  ℹ  Desktop notifications on.

Input highlighting

Slash commands are highlighted as you type:

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 work

Spaces

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

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-reset

Removes ~/.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

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:

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

Linux


Contributing

See CONTRIBUTING.md for development setup, testing guidelines, code style, and how to submit a pull request.


Author

Anish Shekh@anishhs-gh
Repositorygithub.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.