Under tailscaled --tun=userspace-networking (no TUN device — the default
in unprivileged containers), processes cannot dial tailnet addresses at
all: inbound connections are proxied to loopback by tailscaled, but
outbound 100.64.0.0/10 dials have no route. omarchy-send could receive
on such boxes but never send, probe, or discover over the tailnet.
New internal/tsproxy auto-detects this: when no local interface carries
a tailnet address but a SOCKS5 proxy answers at localhost:1055 (the
conventional --socks5-server address), tailnet-bound connections route
through it. LAN traffic is never proxied; explicit HTTPS_PROXY/
HTTP_PROXY/NO_PROXY win over detection, restoring the env-var support
the custom transports had silently dropped.
Also guard NotePeer against the loopback trap the same proxying causes:
inbound registers all appear to come from 127.0.0.1, and recording that
over a peer's routable address made a TUI "send" loop back to our own
receiver while looking delivered.
Live-verified from an omaterm container (userspace tailscaled): headless
send discovered and messaged a tailnet peer with no env vars set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
runHeadlessSend only ran multicast discovery, so `--to <alias> --message`
could not reach a peer that is only routable over the tailnet (or another
subnet), even though the TUI and quick-send paths could via watchRemotes.
Start the same watcher in the headless path: known peers from config plus
online Tailscale peers are unicast-probed, the probe handshake records the
peer, and FindPeer picks it up like any multicast discovery.
Verified live against a tailnet-only peer (different subnet, DERP-relayed):
discovered and message delivered. TestFindPeerViaProbe covers the
composition Probe -> NotePeer -> PeerFound -> FindPeer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
So any AI agent on the machine knows what omarchy-send is and — crucially —
where files land when other devices send them here. The installer writes a
canonical ~/.config/omarchy-send/AGENTS.md (with a CLAUDE.md symlink) and
appends a managed, idempotent section to ~/.claude/CLAUDE.md (markers prevent
duplicates on re-run; the receive dir is read from config when present).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A piped `curl | bash` defaults to local mode and previously said nothing about
the transfer port being internet-exposed. Now the installer detects a routable
public IPv4 (excluding RFC1918, loopback, link-local and CGNAT/Tailscale) and,
in local mode, prints the exact ufw/nftables commands to lock 53317 to the
tailnet — plus a container-on-host note, a --pin tip, and an app-layer verify
command (raw TCP/nc lie behind providers that SYN-ACK every port). It never
changes the firewall outside remote mode. README gains a "Public-IP boxes"
section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Multicast only finds peers on the same LAN. Reach off-LAN boxes by probing
them directly over unicast (works over any routable address; Tailscale is the
easy, secure choice):
- discovery.Probe: unicast POST /register (https->http fallback) with a two-way
handshake, so send and receive both work; offline peers age out.
- internal/tailscale: Peers() shells `tailscale status --json` for online peers.
- config.KnownPeers: persisted manual remotes.
- main.go: watchRemotes goroutine probes knownPeers ∪ tailscale peers every 10s,
in both the normal TUI path and quick-send.
- TUI: `+` on Devices opens an add-remote modal (host/IP/Tailscale name).
- install.sh: interactive local/remote install prompt; remote mode locks port
53317 to the Tailscale interface (ufw), with container/userspace-networking
detection. README documents remote devices.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Send picker: replace the directory browser with a recursive fuzzy finder.
Type to match files/folders anywhere under $HOME (sahilm/fuzzy, now a direct
dep, compiled in), stage with enter, ctrl+d folders-only, ctrl+s send,
ctrl+u re-root up. Folder send is preserved (staging a dir sends it whole).
Quick-send: `omarchy-send <paths>` opens the TUI on the device list with the
paths pre-staged; selecting a device sends immediately and the window closes
itself when the transfer completes. Runs server-less so it coexists with an
already-running instance (controller Set* methods nil-guard the server).
Nautilus right-click "Send via Omarchy-Send" (desktop-only): a nautilus-python
extension that launches quick-send in a floating terminal with the selection
pre-staged. Resolves omarchy-send to an absolute path since ~/.local/bin is
not on Nautilus's session PATH. install.sh ships it only where Nautilus is
present, so headless boxes skip it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two related additions for using omarchy-send beyond the focused TUI:
Headless send (no TUI / no TTY) — for scripts, cron, SSH sessions:
omarchy-send --to "<alias>" --message "<text>" [--send-pin N] [--wait 15s]
Resolves the peer by alias over multicast (discovery only, not the HTTP
receiver, so it co-exists with a running instance), sends, prints a
one-line result, and exits non-zero on not-found / failure. New
discovery.FindPeer/Snapshot and client.SendMessageSync (returns the error
directly, incl. ErrPinRequired).
Bugfix surfaced by the above: the official LocalSend client answers a
message prepare-upload with HTTP 204 No Content (the text rides in the
preview field, nothing to upload). prepareUpload only accepted 200, so
message sends to official peers failed with "prepare-upload status 204"
in BOTH the new headless path and the existing TUI. Now treats 204 as
success (empty response).
Desktop notifications: a running receiver raises a notify-send
notification on an incoming message or file offer, so mako shows it on
Omarchy/Hyprland even when the TUI isn't focused. New internal/notify
(best-effort; self-disabling on headless boxes with no notify-send /
session bus). Off-switch: --no-notify flag, cfg.NoNotify, and a Settings
'n' toggle wired to a live atomic gate via Controller.SetNotify so it
takes effect without a restart.
Tests: discovery/find_test, client/message_sync_test,
client/prepare_204_test, app/events_test. Race-clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Messages to a PIN-protected peer now prompt for the PIN and retry, like
file sends (was a silent failure). SendMessage takes a pin argument and the
TUI tracks whether a pending send is a message or files.
- Copy a received message to the clipboard with `y` (in the Messages tab or
while reading one).
- Send the clipboard as a message with `v` on a device: it opens the compose
box pre-filled with the clipboard text to review before sending.
- New internal/clipboard package shells out to wl-clipboard / xclip / xsel,
and falls back to tmux's paste buffer when running inside tmux on a headless
box (tmux load-buffer -w also reaches the outer clipboard via set-clipboard,
which omaterm enables by default). No system tool -> "clipboard unavailable".
- Route the standard logger to the debug log (or discard) at startup so stray
stdlib logging — e.g. net/http's "unsolicited response on idle channel"
notice after a peer sends a late response — can't paint over the TUI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds LocalSend-compatible text messaging alongside file transfer.
Wire format (matches the LocalSend app): a message is a single "file" with
fileType "text/plain" whose content rides in the prepare-upload `preview`
field. The receiver returns an empty token set, so nothing is uploaded — the
text is read straight from the preview.
- client: SendMessage builds that single-file prepare-upload (no body upload).
- server: detect a message (one text file with non-empty preview), surface it
on a new Messages() channel instead of saving a file, and respond with an
empty file set. Messages bypass the accept prompt (auto-received); the PIN
gate still applies.
- app: bridge the server's messages channel to the TUI as MessageMsg.
- tui: a new Messages tab lists received messages (enter to read full, d to
delete); press `m` on a device to compose and send one. Incoming messages
show a footer notice.
Tests: end-to-end send→receive (text intact, sender preserved, nothing written
to disk) and unit coverage of the message-detection rule.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the receiver cancelled a transfer partway through, the sender kept
uploading the remaining files of that batch (the loop continued past the
failure), and a newly started transfer never stopped the old goroutine —
so the old, cancelled transfer appeared to "carry on".
Fixes:
- Each send now runs under a cancellable context. A new transfer to a peer
supersedes (cancels) any still-running send to that same peer.
- A network/session error mid-batch aborts the rest of the batch, emitting
a clean Cancel for the un-sent files instead of pushing them anyway. A
local file-open error still skips only that file.
- Bounded connection timeouts (dial 10s, TLS 10s, response-header 30s) so a
vanished peer fails fast; the overall timeout stays off for large files.
Test: a receiver whose session is gone (returns 403) causes the sender to
make exactly one upload attempt and then abort, reporting the rest cancelled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sender only handled regular files: a staged directory would fail at
upload time, so users could only send individual files.
Now a staged directory is walked recursively and each file is advertised
with a name relative to the folder's parent (e.g. "Trip/day1/img.jpg"),
which is the LocalSend-compatible way to carry structure. The receiver
recreates those subdirectories under the receive dir, creating parents as
needed. The path-traversal guard is preserved: names are cleaned against a
leading "/" to collapse "..", and a containment check ensures the result
stays within the receive dir.
In the TUI send picker, "a" stages the folder currently being browsed;
staged folders are tagged in the panel and the help text is updated.
Tests: directory expansion produces relative names; an end-to-end folder
send recreates the structure on the receiver; destPath preserves subdirs,
rejects traversal, and de-duplicates within subfolders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the advertised device name defaulted to the machine hostname.
Since the alias is broadcast in plaintext over multicast to the whole
subnet every few seconds while the TUI is open, that leaks the hostname
to anyone on the network — a privacy problem on a laptop joining
untrusted Wi-Fi (the reason LocalSend randomises its aliases).
Now first run generates a random "Colour Object" name from public-domain
word lists (e.g. "Crimson Quasar"): 56 colours x 44 celestial objects =
2464 combinations. It is generated once and persisted, so a device keeps
the same name across restarts. The hostname is still carried in
DeviceModel, and the alias remains overridable via --alias or Settings.
Word lists are original and use only generic colours and public-domain
astronomy terms, so there is no copyright or trademark exposure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>