Unreleased
Changes on main since v0.8.89, one line each. The version in Cargo.toml is already 0.8.90, but nothing is tagged yet: these ship when the owner decides to release, and no date is promised here.
- One interface on every device. The app is three sections — Home, Devices, Settings — on desktops and phones alike. Home is the old Overview and History as one page: the connection status said once as a plain line, then everything synced, newest first, one row per item. The Latest Synced / Latest File / Group Clipboard cards, the filter pills and the second online counter are gone.
- A server is a computer. A device is a Phone, a Tablet or a Computer — one rule, three icons; the separate «Servers» group and the Server / Terminal / WSL labels are gone.
- Copy is Copy: one act per item, and it copies the item itself. «Copy» on a picture puts the picture on the clipboard, «Copy file» puts the file; the path is a separate, secondary Path act. Each act is shown only where the daemon says this device’s clipboard can hold it.
- Phones copy a picture as the picture, everywhere. On Android and iOS — in the app, in the arrival offer and on the notification — a picture lands on the clipboard as an image an app can paste, instead of a path that pasted as text and was useless there.
- A group of phones only works, and says what it cannot do. With no computer in the group there is no Path act (a path pastes nowhere), a file is copied as a file, and a share counts as delivered when a device that can actually use it has the bytes.
- The iPhone notification shows the picture. The notice carries a small end-to-end encrypted thumbnail, so the expanded card shows what arrived. Copy there puts the picture when its bytes are already on the phone; when they are not, the card waits briefly and then opens Clipshot, which applies the picture. It never puts a path on a phone.
- «Waiting for <sender>». In a group with no computer, a phone that learns an item exists before its bytes arrive says so on Home — «Waiting for Pixel — open Clipshot there» — instead of showing nothing.
- A long text can be copied straight from the iPhone notice: above the plain 2.6 KB limit a text of up to 64 KB travels compressed and still end-to-end encrypted, and the card copies it with the app closed.
- macOS: Cmd+B survives another app’s secure keyboard entry. While an app holds Secure Event Input (a password field in Chrome, say), Clipshot registers the paste-path and sync-toggle combos the other way around and a one-line banner says who holds it and until when. Before, Cmd+B silently did nothing.
- Pairing keeps the established group — proven against the live portal, both directions: a fresh device joins the existing group instead of pulling an established device into its own.
- iOS: a suspended app is no longer reported as a stall. A tick stage that spanned a suspension is logged as exactly that, so a real stall while the app is awake stands out.
- One received item is one row, and a picture sent as a file is shown as a picture, with a thumbnail and a working Copy.
- A clipboard text that is just a group path is not re-sent as new content (the Universal Clipboard echo), and neither is a text that is the previous text re-encoded.
- Activity ids stay distinct past the 100-event cap — after a day of use, History had been collapsing every received text into one row.
- No white screen when returning to the app on Android, and the first frame is already the rendered app.
v0.8.89 — 2026-09-29
- A device back from downtime gets what it is owed within seconds. Catch-up now reaches a device that outbound pushes had put in quarantine, and a delivered catch-up lifts that quarantine — before, an owed screenshot waited for the next unrelated copy (measured: six minutes).
- Reconnect never dials a third party’s relay. Only the target’s own relay can reach it, so the step that borrowed another device’s relay (and credited it when iroh connected on its own) is gone.
v0.8.88 — 2026-09-28
- The iPhone never hangs on «Connecting to the local daemon…» after a background. iOS reclaims a suspended app’s listening socket, which killed the local HTTP server silently; it is now rebound, and a probe on return to front heals a listener that died without an error.
- The local API watches itself on every platform — it asks itself
GET /healthevery 30 s and rebinds after two unanswered probes, so a silent listener death is caught on Android and desktops too, not only on an iOS return to front. - A dead daemon ends the process instead of answering from stale caches (iOS and Android), so the next open or push starts clean; Android’s service starts it again.
- A share from the share sheet reaches devices that were away. The desktop that accepts an extension’s item now owes it to peers on their next reconnect, in the author’s name — before, a laptop asleep during an iPhone share never got the screenshot.
clipshot updateon a GUI build fetches the GUI asset, and refuses a non-GUI one even if the manifest offers it — a Linux GUI service had been updating itself into a headless binary and then failing to start.- The macOS auto-update ships the Developer ID-signed app. Every update before this installed an ad-hoc-signed build, which quietly dropped the app’s privacy grants.
v0.8.87 — 2026-09-27
- A share from the iOS share sheet now wakes the phones that are away. The Share Extension has no hub connection of its own, and only an item’s origin ever announced a new item — so a share straight from the sheet notified nobody (measured 2026-09-27: six iPhone shares, zero notices on the suspended iPad). Now the desktop that accepts the item announces it on the origin’s behalf when the item is new to it, and the notice names the device that made it («from iPhone»), not the one that relayed it. The origin itself is never woken about its own share.
- A copy that happened says so. Applying an offer on a phone now shows a confirmation — «Path copied — image from iPhone» with the group path, «Text copied», «Image copied» on Android — and the offer bar hides itself once the item is on the pasteboard. Bringing Clipshot to the front has no tap of its own, so before this a successful copy and nothing at all looked exactly the same.
- A duplicate delivery never resets a completed offer. When the same item (same kind, size, author and origin timestamp) arrives a second time — a catch-up re-offer during a reconnect burst, for example — the offer keeps the file it already has, instead of dropping back to «nothing was saved for this text» when you tap Copy.
- The phone’s local bridge secret is one per install, not one per launch. A reader that had cached the previous launch’s value held a dead secret, and the app’s own page sat on «Connecting to the local daemon… (bridge auth required)» while the share itself landed fine. Regenerating it bought no security — the file is app-private either way.
- Android: a cold start waits for the daemon. The WebView shows a local «Starting Clipshot…» page and loads the app the moment the daemon answers, instead of opening a URL nothing answers yet and rendering Chromium’s connection-refused error page.
- Android release APKs are signed with the release key when a keystore is configured — they were debug-signed whatever the environment said.
v0.8.86 — 2026-09-27
- iOS: a pasteboard write is confirmed by the system, not by our own readback. The Share Extension reads pasteboardd’s change counter: a readback that matches while the counter has not moved is a refused write, and the share sheet says so instead of reporting success.
- A refused or empty pasteboard write is retried on a fixed 150 / 400 / 1000 ms schedule — never when something else wrote after us, which stays the other writer’s item.
- One rule decides the outcome of a pasteboard write (written / refused / kept other / empty), and the share report carries how many attempts the group-path write needed.
- The Share Extension’s diagnostics are drained after logging starts, so its lines actually reach the log — five cold launches had shown none of them.
- iOS hardware testing runs over a Tailscale tunnel (no USB, no shared LAN): the owner’s screenshot → Share → paste flow measured 8/8.
- Consistency work after v0.8.85: the notification card’s sync-name rule mirrors the daemon’s exactly, and file names stay identical across nodes.
v0.8.85 — 2026-09-21
- The notification carries the item itself. A text of up to 2.6 KB travels end-to-end encrypted inside the push payload, and an image or file travels as its group path, so Copy on the expanded notification card works with Clipshot terminated — no app launch, no network, no wake (measured 10/10 on the simulator and on an iPhone 15).
- File names are consistent across nodes: the sender’s
YYYYMMDD_<name>sync name travels on the wire and receivers keep it, and minting it is idempotent so a forward or a retry never adds a second date prefix. - iOS notices are passive — a row in Notification Center for the moment you paste, with no banner, no sound, and no reliance on background delivery.
- A Universal Clipboard item syncs but wakes no phone: content the Mac received from another Apple device of yours is still shared with the group, but the phone that copied it is no longer notified about its own clipboard.
- Linux: a received item is no longer reverted by the clipboard manager — an X11 write hands the selection over to the new owner instead of leaving it ownerless for a moment (KDE’s Klipper used to re-serve the previous item over it).
- A big share resumes after the receiver restarts: an iPad Share Extension continued a 45 MB item from the host’s offset mid-transfer, with identical checksums on both hosts.
- iOS simulator lane: notification scenarios now skip with a stated reason when a live fleet item interferes, instead of failing falsely.
v0.8.84 — 2026-09-21
- A device is never
node_NNNN. iPhone and iPad now write their real device name (then the model) at startup, Android already wroteBuild.MODEL, and the fallback for a device without a name readsunnamed-device-NNNN— the owner’s iPhone had been sitting in the fleet asnode_7120beside dead test nodes, and a cleanup pass removed it from the group. - Pairing keeps the group that already exists. A token is adopted only from a device that HAS a group: a joiner with an established group keeps it against a fresh generator’s provisional token, and the fresh generator joins the joiner’s group instead.
- iOS: the expanded notification’s Copy button copies exactly the item the notice is about. The notice carries the item’s identity (kind, size, origin timestamp), so a card whose item has not landed yet reads «Arriving…» and waits for that item instead of copying a newer one. Before the first unlock after a reboot the card says «Unlock to copy» and copies nothing.
- A group path is a reference, not content. A clipboard text that is exactly a
~/.clipshot/sync/<name>path — for example the one Apple Universal Clipboard carries from a phone to a Mac — is no longer broadcast as a new text item, so the group stops filling with.txtfiles whose body is a path. - Presence on iPhone/iPad is «resumed AND focused», and it is sent by the hub connection itself rather than queued on the daemon tick: a phone that leaves the foreground is no longer counted as present, so the hub sends it the notice it was missing. A visible-but-unfocused window (iPad Windowed Apps / Split View) counts as not present.
- Peers:
connection_typeis the path the last health probe travelled (direct/relay), and is absent until a probe has reported one. It used to be read off the address card, so every hub-known peer showedrelaywhile the node held direct QUIC connections; measured after the fix onmain: 5 of 6 peers direct, the LTE phone relay. - The tick never dials a peer inline. A peer named by the hub or by gossip is registered and handed to the reconnect scheduler, which dials one peer per cycle under its own cap — one offline peer used to freeze the tick for ~30 s on every hub (re)connect, i.e. on every phone wake. Every tick stage is timed now, and a stage over 2 s logs a warning naming itself.
- A removal the hub has already forgotten stops being re-sent. A device that an authoritative peer snapshot no longer lists is treated as confirmed removed, instead of re-sending
RemoveDeviceonce a minute for up to 7 days. - One backoff owner for relay dials —
connect_via_relayis a single dial, not a second retry loop beside the reconnect scheduler. - Cmd+B pastes the path it decided, instead of falling back to an older file from the sync directory.
- Onboarding is one card, not a four-slide tour — three lines for the host you are on (desktop: copy here / Ctrl+B pastes the path / History; phone: Share → Clipshot / what arrives is offered, tap Copy / History) and it no longer covers the arrival-offer bar.
v0.8.83 — 2026-09-17
- Phones: an arrival is an offer. Nothing that arrives on a phone or tablet writes the pasteboard by itself. It changes only by your own act: your own send, a tap on the offer («Image (1.2 MB) from main — Copy path» in the app, Copy / Copy path on the Android notification), or bringing Clipshot to the front — which applies the newest offer only if it arrived after the last thing you put there yourself. The 60 s “own send owns the pasteboard” window from v0.8.82 is replaced by this rule.
- Offers survive an app relaunch — a screenshot that arrived while the app was closed is still one tap away after reopening.
- A phone’s own clipboard is never mirrored; the only way in from a phone is Share → Clipshot. The
mirror_mobile_clipboardsetting is removed (it had no UI and decided something the platform decides). - A phone is not a host. A share from a phone counts as delivered when a desktop or server has it; the phone’s pasteboard gets the group path at that moment. If only another phone took the bytes, the share sheet says so and holds the item instead of reporting success.
- Big items resume after a drop. From 8 MiB up an item travels in 1 MiB chunks; the receiver keeps a partial and any sender of the same item — the original or a forwarding device — continues from the receiver’s offset instead of restarting. Up to three resumes within the item’s own time budget; an unreachable device is a dead link (one quick retry, then failed), not an endless resume.
- The iOS share sheet resumes big items too. A share of 8 MiB or more from iPhone/iPad continues from the desktop’s offset after a drop, within the time the sheet gives that device; a busy or unreachable device means the next one is tried. A share the sheet could not finish is continued by the next send of the same item.
- One transfer row per item on both sides, kept in progress across a drop; the byte count never goes backwards, and the receiving row names the device that finished the item.
- Every clipboard push goes through the broadcast queue — Retry from History and a browser-mode copy no longer push from the daemon’s own tick, so a slow peer cannot stall clipboard polling. A retry while sync is paused now answers «Sync is paused».
- Big items fan out two uploads at a time, hosts first, so one slow phone does not hold every desktop’s copy.
- A sleeping phone reads as “Asleep”, not as a degraded peer, in Peers, in
/api/peers(data_plane_state: "asleep") and inclipshot doctor. - No Welcome Screen flash on a paired device: the first-run screen is shown only after a successful settings read says there is no token; a failed or pending read shows a spinner. The daemon answers a settings load failure with an error instead of defaults.
- A slow in-flight copy of an item no longer blocks a second copy — a faster sender takes it over, once per item.
- No spurious catch-up offer every 5 minutes: a peer’s periodic self-announcement is no longer read as a reconnect, so connected peers stop receiving a redundant “newest item” offer on the clock.
- Dependencies: rustls 0.23.45 (TLS 1.3 handshake advisory);
validator0.20 drops the lastidnaadvisory from the lock.
v0.8.82 — 2026-09-14
- iOS Share: a raw image is tried in the extension first and handed to the app if that attempt was killed; the path lands on the pasteboard only after confirmed delivery; an own send owned the pasteboard for 60 s (superseded on
main, see Unreleased). Large-file fan-in: size-scaled per-candidate timeout, one in-flight copy per item. A mesh forward never echoes to the origin; received files stay off the desktop clipboard; History Copy path works on phones; abandoned transfer rows fail instead of spinning.
v0.8.81 — 2026-09-08
- Relay health: a node never dials its own address as a peer relay; one canonical spelling of peer-relay URLs in log, health and dial.
v0.8.80 — 2026-09-07
- Relay health: a cancelled peer-relay attempt counts as a failure, so the 24 h quarantine is reachable from the sub-second reconnect stage.
v0.8.79 — 2026-09-07
- Relay health: one canonical key so the 24 h quarantine actually fires (measured on 0.8.78: 87 dials/h of an unreachable peer relay, 0 quarantines).
v0.8.78 — 2026-09-07
- Relay fault isolation: a dead relay is remembered for 24 h, peer-relay URLs are never synthesized from IP addresses, and a frozen network actor is detected and the endpoint rebuilt.
v0.8.77 — 2026-09-07
- Removal notices wait for their host instead of dialing absent peers; a stale re-send can no longer evict a fresh clipboard frame.
v0.8.76 — 2026-09-06
- Removal retries are bounded so clipboard and health survive a stale-peer backlog; newcomer installer and Android sharing fixes.
v0.8.75 — 2026-09-05
- Phone share 404 fixed; any file can be shared from a phone; a received image raises a notification; Ctrl+B falls back to the newest file on disk; dead peers are evicted.
v0.8.74 — 2026-09-03
Bug Fixes
- Headless builds trust the OS certificate store again — the fix existed, but the feature carrying it was absent from
headless, the profile the Linux release binary is built with, so a node behind a corporate TLS proxy rejected the relay certificate asUnknownIssuerand sat inconnectingfor eight days while still reportinghub_connected: true. - A coalesced clipboard frame is acknowledged instead of answered “busy” — the frame that coalesces is the one that gets stored (it wins the latest-wins slot), so the sender was told to retry work that had already been accepted. A healthy mesh reported itself
backpressuredcontinuously as a result.NotEnqueued— where nothing was stored — still answers busy. auto_accept_filesandreceived_files_dirare no longer frozen at boot — a settings change read back correctly and did nothing, so a file could land on a device whose accept setting was off.
Features
- Mobile clipboard bridge — platform-neutral
POST /api/clipboard/bridge,POST /api/clipboard/bridge/imageandGET /api/clipboard/bridge/pending, sharing one mailbox with the existing/api/clipboard/androidspellings the shipped Android APK calls, plusPOST /api/clipboard/captureandGET /api/clipboard/capture/statusfor platforms that cannot read the clipboard unattended. See API reference. - One ingest rule for both phones — the share-sheet path is stricter than the clipboard path, and the rule lives in Rust so both front ends get it.
- A waiting file is visible on every page in the web UI, instead of History only.
Architecture
- The OS trust store is a target property, not a per-profile feature — patching one profile had left a third one broken, with a manifest-parsing test reporting green on it.
Security
- Dependency advisories with a patch available: 8 → 1. The remaining one (
idnaviavalidator) has no reachable call path — noemail/urlvalidators exist in the codebase.
Infrastructure
- Build images verify what they execute —
rustup-initand the bun archive are pinned and checked against upstream digests with negative tests, and the Android toolchain is pinned, so the images are reproducible rather than merely authentic. - CI rebuilds the Android builder image when its recipe changes, not only when the image is missing; the lane had been green about images it never ran.
v0.8.73 — 2026-08-22
Bug Fixes
- Persistent node ID is written atomically — the node ID file was written with a truncating
fs::write; a reader racing that window saw an empty file and minted a different ID, forking the host’s stable identity.
Security
- The updater signing key and its passphrase are no longer in the repository —
keys/clipshot.keyandkeys/.env(which held the passphrase for exactly that key) were committed together, which defeats the encryption entirely. The committed key was already superseded, not live — it derives a different public key than the one pinned intauri.conf.json, and the live key exists only on the build host — so signed updates are unaffected..gitignorenow blocks the shape (keys/*.key,keys/*.pass,keys/.env,*.p12,*.p8) so it cannot recur.
Infrastructure
- The release pipeline heals itself instead of stranding work — all three paths that commit into the shared working copy (
release.sh,release.yml,docs.yml) now rebase-and-retry against the CI remote, up to three attempts, re-fetching each time. Previously a commit landing between the rebase and the push rejected as non-fast-forward and was swallowed as advisory, leaving the tree both ahead of and behind the remote — the state that had to be unblocked by hand before v0.8.71 and v0.8.72. - Only the CI remote counts as a successful push — success was judged by whichever remote happened to work, which hid the one failure that matters, since every release re-reads the CI remote.
- The GitHub mirror is removed from the release path — nothing depended on it (
install.shdownloads fromclipshot.cc/dist, CI runs on Forgejo, and no artifacts were ever published there), so it was a write-only copy of the source on a third-party host. - Service install test asserts what
ExecStartneeds — it checked that the running test binary still exists on disk, which Cargo can invalidate mid-run; it now asserts the absolute path that a service manager actually requires.
v0.8.72 — 2026-08-21
Bug Fixes
- Updates no longer wipe granted macOS permissions — reinstalling a signed build erased an already-granted Accessibility / Input Monitoring permission and re-prompted for it. The
tccutil resetthat caused this was correct only under ad-hoc signing, where a grant is bound to the binary’s cdhash; releases are Developer ID signed and notarized now, so a new build satisfies the existing grant on its own. The reset now fires only for a grant that is dead rather than merely absent — the binary changed and that service still reads denied. A denied service is also reset on its own, so one rotted grant no longer takes a healthy neighbour down with it. - Permission fingerprint is written even while a permission is denied — previously a single update was re-reported as “binary changed” on every subsequent launch.
Architecture
- TCC reset policy moved out of the GUI — the decision, the binary fingerprint, and the service names now live in
src/permissions/tcc_reset.rsinstead ofsrc/gui/tauri_app.rs; the bundle ID and the three spellings of each TCC service name (kTCCServiceListenEvent,ListenEvent, “Input Monitoring”) are single-sourced through aTccServiceenum. The decision is pure code, so it stays under test on Linux and Windows CI too.
Infrastructure
- CI image recipes committed — the
clipshot-docs-builderimage had no recipe in the repository and existed only as a local artifact on one host, which failed the release preflight. Its Dockerfile is now committed alongsiderebuild-all-ci-images.sh, which restores the exact image set the preflight checks and reports free disk against the release gate. - IPv4 precedence in the docs-builder image —
gem installhung because the resolver kept choosing AAAA records on a host with no IPv6 egress; the same one-broken-family-must-not-starve-a-healthy-one rule the daemon applies to its own DNS.
v0.8.71 — 2026-08-21
Bug Fixes
- Atomic node ID write — the persistent node ID was written with a truncating
fs::write; a reader racing that window saw an empty file and minted a different ID, forking the host’s stable identity. The write is now atomic. paste_pathnames files after their content type — Cmd+B /POST /api/paste_pathalways used theimg_YYYYMMDD_XXXX.pnghelper, so clipboard text was saved under an image name while every other node in the mesh wrotetext_….txtfor the same bytes.- Health probe proves application liveness — the probe no longer accepts a bare QUIC path as proof that a peer is alive.
- Relay candidates filtered by address family — relay candidates (including the primary relay URL) are filtered to the address families actually available locally, so a v4-only host stops dialing IPv6 relays.
- Reconnect stage no longer starves or livelocks on portal candidates over high-latency links.
Architecture
- Multistream sync handler — a persistent bounded connection supervisor keeps shared QUIC connections for peer answers, with isolated and bounded inbound file streams.
Infrastructure
- Developer ID signed macOS builds — signing and notarization lane added, with credentials sourced from outside the repository.
v0.8.70 — 2026-08-07
Bug Fixes
- Handshake success no longer wipes reconnect escalation state — a bare QUIC handshake was treated as “reconnected” and deleted the peer’s
ReconnectState, so a peer could sit atconnected_peers=0/5while every status surface stayed green. Root cause of a 57-minute silent outage. - Windows pastes a received file as a file —
CF_HDROPis implemented, so Ctrl+V in Explorer no longer pastes just the file name. - Windows clipboard CPU storm — polling now uses the clipboard change counter: 14.5s → 1.8s CPU per 30s.
- Path traversal via peer-supplied filenames — a peer could write outside the sync directory through the content filename; both the direct and chunked receive paths now share one guarded-read primitive.
Security
- Cross-origin and cross-host HTTP callers rejected — CORS matched origins by prefix with no delimiter check and reflected the match back, so
http://localhost.evil.comwas allowed to read responses. Host matching is now exact, and a DNS-rebinding gate covers every route including/healthand preflight. - The group token is no longer served over the API —
GET /api/settingsreturned the full group join credential. The API projection now returns[redacted:<8 hex of SHA-256(token)>]; redaction lives in the projection, not on the field, because the same struct is the on-disk TOML format. - History content reads confined to the sync and received directories.
- Bounded receives — the chunked receive, the number of concurrent chunked receives, and the pending-incoming queue are all bounded, and partials are reaped; a hostile mDNS TXT record can no longer panic the daemon.
- Received-file retention judges a file by when it arrived, not by the mtime it claims.
Infrastructure
- Tick lock-poison recovery completed for the remaining lock set, with a panic budget for the GUI tick driver.
- Log-noise sweep — caller-triggerable command failures are throttled across all seven paths, and several per-poll clipboard log sites were demoted to debug.
v0.8.69 — 2026-08-03
Bug Fixes
- Windows hotkeys — removed a duplicate
track_modifier_stateleft behind by the hotkey listener decomposition. - Honest degraded warnings — the outbound data-plane WARN now distinguishes entered degraded, a throttled echo with no new failures, and worsening (failure count grew), instead of printing the same line in all three cases.
Features
- Diagnosis telemetry switch — one reusable opt-in,
diagnostic_telemetry(defaultfalse), gates Clipshot’s own investigation-aid logging. Enable it viasettings.toml, a native GUI full-settings save, orCLIPSHOT_DEBUG_TELEMETRY=1; it is deliberately not settable overPOST /api/settings. - Push attempts exposed —
/api/peersnow reportspush_attempts_since_quarantine, which separates a genuinely failing transport from a peer that is quarantined but never actually pushed to. - Doctor states the contradiction —
clipshot doctorspells out “quarantined but still receiving data” and says whether any outbound push was attempted, instead of leaving it to journal archaeology.
Infrastructure
- Log-noise sweep — the
noqandnoq_protoQUIC crates are error-capped (they were ~95% of the journal), the iroh probe-success INFO is throttled per canonical peer id (184 lines/hour from a single unreachable peer → ~12), and “Enumerated N network interfaces” dropped to debug. - Hotkey listener, daemon-startup, and test-hook route tables were decomposed; the CI complexity gate no longer miscounts braces inside string and char literals.
v0.8.68 — 2026-08-02
Bug Fixes
- Per-family libc DNS resolve — libc lookups are issued separately for IPv4 and IPv6, each under its own timeout, so one broken address family no longer discards the healthy one. This removes the cause of the corp-network DoH request storm (see rule 53).
- Peer card honesty — the peer UI no longer renders “reachable but not connected” and “dead” identically.
- Tick lock-poison recovery — a caught tick panic now clears lock poison in both tick drivers, so one panic no longer guarantees a cascade of follow-up panics.
Architecture
- Hub / setup decomposition — the hub run loop and Tauri
setup_appwiring were split into smaller units (pure extraction, no behavior change). - Bare no-iroh build rejected explicitly —
cargo build --no-default-featureswithoutirohnow fails with a clearcompile_error!instead of a wall of confusing type errors.
Infrastructure
- Log records are tagged with their originating crate, and noisy dependency crates (including
hickory_net) are capped through one shared list. - CI complexity and formatting gates are green tree-wide.
v0.8.67 — 2026-07-28
Features
- Always-on crash diagnostics — release binaries keep symbols so backtraces are readable, fatal signal handlers (plus a Windows exception filter) capture crashes, a two-tier signal-safe breadcrumb ring records recent activity, and a heartbeat snapshot acts as a SIGKILL witness.
clipshot doctorsurfaces the collected crash evidence.
v0.8.66 — 2026-07-25
Bug Fixes
- Outbound data-plane liveness — outbound clipboard-push failures are tracked separately from health RTT, quarantined peers are evicted and re-seeded for reconnect, and
/api/peersexposesdata_plane_stateandconsecutive_push_failures. Health can no longer look green while pushes silently wedge. - Receiver stuck-inbound liveness — inbound drain stalls are detected and surfaced as
consecutive_inbound_stalls;accept_biand payload reads are bounded, with a latest-wins bounded hand-off and ACK-after-enqueue. - Relay-hint reconciliation — relay candidates now union
relay_urlwith the verified relay, dial multiple relays, and fall back on the data-plane push path. - Identity domain split — a two-layer canonicalizer replaced the synthetic mesh-u64 identity mint, fixing GUI share-URI silent peer loss.
Features
- Refresh all — force-reconnect every dropped peer from the UI.
v0.8.65 — 2026-07-19
Bug Fixes
- Portal
RemoveDeviceretried until hub-confirmed, so device removal converges even if the hub is temporarily down. - Gossip re-adoption honors tombstones, so a removed device is no longer re-adopted through gossip.
v0.8.64 — 2026-07-19
Features
- Device removal converges across the fleet — new
PeerRemovedV2wire message (append-only, dual-send), split last-writer-wins on receive (gossip vs authoritative), local tombstone store, remove-intent retry set, snapshot removed-list ingest, a data-plane inbound gate for clipboard and file traffic (with kill-switch), victim-side UX, and a doctor check.
v0.8.63 — 2026-07-18
Bug Fixes
- Peer-remove mesh-stall fix pack — endpoint rebuild now respawns all workers, the expire-vs-backoff invariant is enforced, removal propagates without the hub (with a
source_idauth gate), removal purges every reconnect source, a projection-contradiction guard was added,node_idpersists as a hex string (values abovei64::MAXno longer break), and a control-frame flush bug was fixed. - CLI
clipshot setupnotifies a running daemon of the newly adopted token.
Features
- Tauri auto-updater — signed update artifacts and an update manifest are produced by the release lane.
Infrastructure
- New test-only route
GET /api/test/reconnect_statefor reconnect forensics. - Releases auto-mirror to GitHub.
v0.8.62 — 2026-07-15
Infrastructure
- macOS GUI DMG built in CI — the
.appbundle + DMG installer (Clipshot_VERSION_aarch64.dmg) is now produced by the release pipeline in a headless KVM macOS VM (macos-vm.sh build-gui); warn-only for the first release, then gating. - Repo history purged of committed binaries — 218 MB → 17 MB pack;
dist/*no longer tracked in git/LFS. - install.sh aligned with the DMG lane — macOS default install downloads the DMG, mounts it, copies the
.appto/Applications, and clears quarantine (xattr -cr).
v0.8.61 — 2026-07-15
Bug Fixes
- Single token-write lock — all group-token writes go through one leaf lock, closing the C4 class of racy token overwrites.
- CLI
clipshot setuptoken adoption fixed (B4) — setup now uses the store-level token-replacement path instead of the disk-preserve settings merge that silently kept the old token.
Infrastructure
- Docs annotator fails loudly + stable
data-testidselectors for screenshot automation. - Dispatch-only release pipeline —
workflow_dispatchis the single release trigger.
v0.8.60 — 2026-07-15
Infrastructure
- Docs screenshots pipeline — version badge rendered in browser mode (no more permanent
v?), declarative annotated callouts viaannotations.json, and readiness-waited screenshots to eliminate half-loaded captures.
v0.8.59 — 2026-07-15
Features
- Browser sign-in in the GUI — the Welcome screen and Settings page can open a browser setup session (
clipshot setupflow): sign in with email/password, Google, or GitHub, pick a group when you belong to more than one, and the app adopts the group token without a restart. replace_group_tokenprimitive — intentional token replacement (Tauri command +POST /api/settings/replace_group_token) that bypasses the disk-preserve settings merge and reconnects the hub on the new token (fixes B4/B5/B10, including re-pair returning a node to its old group after restart).
Security
- Popup/tabnabbing hardening — browser sign-in opens the window synchronously in the user-gesture stack with
noopener,noreferrer, with a copyable fallback link when popups are blocked.
v0.8.58 — 2026-07-14
Bug Fixes
- macOS post-sleep mDNS spin fixed — after sleep/wake, mDNS sockets returning
ENOTCONNcaused a hot spin: 100% of one core, log storm, thousands of leaked file descriptors →EMFILE→ DNS failures → the node dropped off the hub (“loses connection once a day”). Fixed by upgradingmdns-sd0.11 → 0.20 (two sockets total, no fd bomb) plus a pure sleep-gap watchdog (src/network/sleep_gap.rs) that detects wall/monotonic clock gaps > 30 s and recreates the mDNS registration after wake.
v0.8.57 — 2026-07-07
Bug Fixes
- System-nameserver DNS rung — the external-DNS fallback layer now queries the nameservers from
/etc/resolv.confdirectly over UDP/53 before DoH. Covers hosts where the libc/NSS layer is broken but the host’s DNS infrastructure is healthy, and where corp MITM proxies kill HTTP/2 DoH (closes the gene corp-DNS incident). Only nameserver IPs are taken — neversearch/ndotsoptions.
v0.8.56 — 2026-07-07
Features
- External DNS policy — corp-network-resilient resolution for external endpoints (hub WebSocket, pair portal): libc first (1.5 s, respects
/etc/hosts), then hickory with DoH fallback (8 s), then a verified-IP cache (~/.clipshot/dns_cache.json, TTL 7 days). Layer selection escalates on connection failures, not just resolve failures. - Doctor: external DNS health check — verdict matrix over libc/DoH-fallback/IP-cache probes with kill-switch-aware wording.
- Kill-switch —
external_dns_fallback = falseinsettings.tomldisables the DoH layer (for compliance-locked corp devices); not settable via HTTP.
v0.8.55 — 2026-06-23
Infrastructure
- Test-suite stability — eliminated parallel global-state races in
cargo test --lib(60/60 clean parallel runs); more Docker E2E lanes (pair-v2, mesh-heal, file-transfer, mutation pilots, iroh-restart) promoted from advisory to gating. - One-command release script + repo cleanup.
Bug Fixes
- Faster, non-dead-end reconnect for isolated/relay-less peers.
v0.8.54 — 2026-06-20
Infrastructure
- Full CI/CD pipeline on self-hosted Forgejo — unified
release.ymlwith 5 chained jobs: test → build → docs → deploy → verify. All builds run in Docker containers on spex (no external CI). - Windows headless binary added to release artifacts (
clipshot-windows-x64.exevia Docker mingw cross-compile). - Docs auto-audit pipeline — AI-powered docs audit → fix → screenshot refresh → Jekyll build → deploy to docs.clipshot.cc, all containerized.
- Self-hosted downloads — all download URLs point to
clipshot.cc/dist, zero GitHub dependency. - Push-to-main CI trigger — Forgejo Actions now auto-runs tests on every push to main.
v0.8.53 — 2026-06-01
Bug Fixes
- Copying a file/image no longer hijacks the clipboard — previously, copying a file in Finder could replace the clipboard with a text sync path, so pasting into a chat app inserted a path instead of the file/image. Now the clipboard is never auto-overwritten: Cmd/Ctrl+V pastes the original content, and Cmd+B / Ctrl+B writes the sync path on demand. Behavior is uniform across macOS/Linux/Windows — no Finder-specific special case.
- Headless
paste_path(Ctrl+B) now correctly persists the path write — a missing clipboarddetach()previously let the spawnedxclipdie, losing the write.
Architecture (SOLID/DRY/KISS)
- Removed the entire auto-overwrite decision layer:
sync::clipboard_paste_policymodule,Clipboardtrait methodsis_finder_copy/supports_multi_type_write/set_image_with_path_text,ContentKind/ContentType::kind(), and thepaste_sync_path_on_filesetting. Net −535 lines. Clipboard write path is now one uniform rule on every OS.
Tests
- New unit tests pinning the contract:
sync_contentnever overwrites the clipboard for image or file content, path always cached for Cmd+B. - E2E (Docker/xclip):
clipboard-toggle-test.shrewritten to the current contract. - E2E (macOS/NSPasteboard): new
macos-finder-copy-test.sh— Finder file copies stay as files, images stay as images, Cmd+B writes the path on demand.
v0.8.52 — 2026-05-29
Bug Fixes
- Multi-file copy no longer breaks the clipboard — copying 2+ files in Finder/file manager previously read only the first file (rest silently lost) and then overwrote the clipboard with a single sync path, destroying the whole selection. Now a multi-file copy is left completely untouched: no sync, no clipboard rewrite. Single-file copy behavior unchanged. To send several files, archive them into one.
Architecture (SOLID/DRY/KISS)
FileClipboardOutcomevalue type —content_from_filesnow returnsEmpty | Single | Multipleinstead ofOption. One pure decision point (SRP), reused identically by all four platform clipboards (macOS/Wayland/X11/Windows) (DRY).Multipleshort-circuitsget_content()toNoImagewithout falling through to the image probe (avoids syncing Finder’s icon-preview PNG).- Multi-file judged by input length, not readability — a broken file in a 2-file selection cannot collapse into a single-file sync.
Tests
- 7 new unit tests for the file-outcome policy + a Linux/X11 docker E2E verifying multi-file leaves clipboard + sync dir untouched while single-file syncs.
v0.8.51 — 2026-05-25
Architecture (SOLID/DRY/KISS)
- Relay hostname preserved — removed DNS pre-resolve that broke TLS/SNI (
relay.clipshot.cc→ raw IP). NewRelayConfigvalue object. - Runtime defaults centralized —
runtime_defaults.rsfor P2P/HTTP/test-hooks ports. Zero hardcoded ports in runtime code. - DaemonClient IPC —
ipc/daemon_info.rs+ipc/client.rs. Daemon writes JSON at startup, CLI uses it for pair/status/peers. - install.sh delegates to binary —
clipshot service installrenders systemd/launchd. One source of truth. - peer_registry SRP split — 2375 → 1153 impl lines across
mod.rs(553),addr.rs(467),metadata.rs(133). - iroh_addr module — pure address helpers extracted from PeerRegistry.
- Unified connection check —
is_peer_alive()replaces duplicatedmanager && monitorpattern.
Bug Fixes
- aws-gene connectivity restored — relay domain preserved for TLS/SNI.
- Clipboard: Cmd+V pastes content, not path — only Finder Cmd+C overwrites clipboard with sync path. Screenshot/Telegram/Slack images stay as images. Detection via
com.apple.finder.noderef. - CLI
clipshot pairpolls for joiner — shows code, waits 5 min with progress dots, auto-confirms. - install.sh pairs AFTER daemon starts — was attempting pair before service installed.
- Flaky tests fixed —
serial(test_hooks_env)for CLIPSHOT_TEST_HOOKS env races.
UI
- PairDialog: OS install picker — Linux 🐧 / macOS 🍎 / Windows 🪟 tabs with platform-specific install commands.
- History: copyable path field — read-only input with select-all and copy button under each transfer.
- Device badge overflow — long node names truncate with tooltip, latency stays inside bounds.
Testing
- 1872 Rust unit tests (was 1849), 299 Vitest, 27 E2E tests passed.
- 3/3 parallel cargo test runs clean (0 flaky).
v0.8.50 — 2026-05-25
Architecture: DaemonClient IPC
- New
src/ipc/module:DaemonClientconnects CLI commands to the running daemon via localhost HTTP API DaemonInfo(pid, version, http_port) written to~/.config/clipshot/daemon.jsonat daemon start- CLI
pair,status,peers,settingscommands now use DaemonClient instead of control socket src/runtime_defaults.rscentralises port constants: P2P_PORT (19231), HTTP_PORT (15282), TEST_HOOKS_PORT (18181)
Clipboard paste policy (KISS)
- Never auto-overwrite clipboard with sync path — except Finder file copies
- Finder Cmd+C detected via
com.apple.finder.noderefpasteboard type - All other sources (screenshots, app copies, images) keep original clipboard content
- Cmd+B still gives the sync path on demand
- New module:
src/sync/clipboard_paste_policy.rs(pure policy, fully tested)
CLI pair now polls for joiner
clipshot pair(generate) now polls for up to 5 minutes, showing progress dots- Displays confirmation digits when joiner arrives
- No longer fire-and-forget — mirrors the GUI PairDialog UX
- install.sh pairs AFTER daemon starts (daemon must be up for DaemonClient IPC)
PairDialog OS install picker
- After generating a pair code, the dialog shows install commands for Linux, macOS, and Windows
- Platform tabs with one-click copy:
curl | bash --code=...(Linux),brew install ... && clipshot pair(macOS),irm | iex(Windows)
Refactoring
peer_registrysplit into SRP modules:mod.rs,addr.rs,metadata.rs,tests.rsiroh_addr.rsextracted from PeerRegistry: pure stateless iroh URL utilitiesrelay_config.rspreserves configured relay hostname for TLS/SNI- Unified connection alive check (DRY)
- Daemon auto-enables HTTP API on port 15282
Fixes
- Image clipboard keeps PNG, not path (Cmd+V pastes image again)
- Device badge overflow — truncate name, keep latency inside bounds
- Path field overflow in UI
- Clippy clean, flaky test fixes (serial test_hooks_env)
v0.8.49 — 2026-05-25
File size limits removed
- No hard file size limits on file transfer (Pro) — any size works
- Clipboard sync
max_file_size_mbsetting default unchanged (10 MB), configurable up to 10 GB - Removed stale 1 MB / 1 GiB size limit references from docs and types
v0.8.48 — 2026-05-24
New: File Transfer (Pro)
Targeted file transfer to specific peers via drag-and-drop UI.
- Send files of any size to selected peer(s) from sidebar “Send File” dialog
- Chunked streaming for files ≥50MB — streams from disk in 1MB chunks, no full-file memory load
- Resume on disconnect — receiver saves partial file, sender continues from offset on retry
- Sequential delivery — one peer at a time, reorder priority in History page
- Auto-accept (default) or manual approval mode via Settings
- System notifications on file sent/received/pending
- Pro gate — sending requires Pro subscription; receiving works for all
- Hash-verified — FNV-1a hash checked after transfer, mismatch = retry
File Transfer API
- 7 new Tauri commands:
send_file,get_file_transfers,reorder_file_transfer,cancel_file_transfer,accept_incoming_file,decline_incoming_file,get_pending_incoming_files - 7 new HTTP endpoints:
/api/file/{send,transfers,pending,reorder,cancel,accept,decline} - Wire protocol:
MSG_FILE_TRANSFER(0x03) on existing QUIC ALPN - 2 new settings:
auto_accept_files,received_files_dir
Fixes
- Clear stale connection state after iroh endpoint rebuild (production spex crash-loop fix)
- Self-relay detection — skip custom relay when it resolves to local interface
- BroadcastWorker writes
sync_sentactivity events (history was empty for sent items) - Partial file truncation on fresh transfer (no append to stale data)
- ACK resilience for large transfers — treat connection-lost-after-all-bytes-sent as success
v0.8.46 — 2026-05-22
Critical: broadcast delivery fix
v0.8.42→v0.8.45 cleanup introduced address URI pollution that caused all broadcasts to timeout (15s × 4 peers = 60s stuck in Sending).
Root cause: Two URI encoding forms (comma addrs=A,B,C vs ampersand addrs=A&addrs=B&addrs=C) coexisted in peer_registry.addresses[]. best_iroh_addr() picked the longest (most polluted) form → iroh Endpoint::connect() timed out on malformed addresses.
Fixes:
canonicalize_iroh_url()— deterministic sorted comma form for all iroh URIs. Parses, deduplicates, sortsSocketAddr, re-encodes.best_iroh_addr()— semantic scoring (relay > addr count > shorter) instead ofmax_by_key(len)which always selected the worst form.merge_address_by_node_id()— canonical comparison prevents Form A/B duplicates from accumulating.add_peer()— canonicalize all addresses on ingest including theoriginaladdress frompeers.toml.replace_addresses_by_node_id()— canonicalize hub-authoritative addresses so sorted-vs-unsorted forms collapse.build_connect_addr_for_peer()— rewritten to usebest_iroh_addr+ relay enrichment (old code re-extracted addrs from all entries, which was the source of Form B pollution).
Transport & architecture cleanup (55 commits)
- Replace
IrohAddrTrackerwith nativeEndpoint::watch_addr()(−599 LOC) - Drop dead Ping/Pong sender path → use
Connection::rtt()(−97 LOC) - Set QUIC
keep_alive_interval=10s+max_idle_timeout=45s LivenessSupervisordynamic membership — per-peerIrohPeerChannelregistered on every hub peer-add path- Respawn supervisor on hub reconfigure
rebuild_clipshot_router()— decouple Router from Endpoint lifecycleiroh::Endpoint::metrics()exposed via/api/test/iroh_metricsConnection::paths()exposed via/api/test/connection_paths- Wire
MemoryLookupAddressLookup populated from hub events - 5s read-through settings cache for hot-path loaders
- Fold
ContentTypeKindinto canonicalContentKind - Collapse
PasteboardSourceenum intofile_takes_priority(bool) - Clipboard cache populated on auto-overwrite (Cmd+B toggle-back fix)
- Serialise
ensure_group_tokenwithPAIR_V2_SETTINGS_LOCK
Diagnostics
/api/test/peer_registry_dump— address pollution diagnostics/api/test/iroh_metrics— fullEndpointMetricsvia iroh registry/api/test/connection_paths— per-peer connection path snapshots
Docker E2E
docker-compose.regression-v846.yml— 3-node mesh (1 GUI + 2 headless)- 6 Playwright tests: mesh formation, address dedup invariant, broadcast delivery, clipboard delivery, sustained traffic dedup, no duplicate names
v0.8.42 — 2026-05-21
macOS clipboard priority fix
Copying a file in Finder (Cmd+C) now syncs the real file instead of the file’s icon preview. Previously, Finder ships file URL + PNG icon in the pasteboard simultaneously, and MacOSClipboard::get_content picked the image (always present) before the file URL — resulting in a 153 KB icon being synced instead of the 1.2 MB JSON the user actually selected.
Fixed by extracting the priority into a pure-policy module src/clipboard/macos_priority.rs:
v0.8.41: Image > Files > Text ❌
v0.8.42: Files > Image > Text ✓
SOLID / TDD: 6 RED-first unit tests for the priority function cover the exact regression case (finder_file_copy_picks_files_not_image_or_text) plus the five neighbouring cases (screenshot, plain text, image+alt, empty, files+text).
Transport resilience unified — single LivenessSupervisor
Internal refactor (no breaking changes u2014 patch bump): replaces six independent watchdog mechanisms (hub-WS idle timeout, F1 connect timeout, tick-stall watchdog, iroh_listener_dead flag + accept-loop detection, HealthWorker poll loop, ad-hoc reconnect commands) with one primitive — see plans/transport-resilience-unified.md.
What’s new
src/p2p/v2/transport_health/:Heartbeat+Restartabletraits (async_trait, object-safe)Policystruct (singletick_interval+probe_timeout+consecutive_failures_before_restart+cool_down_after_restart, matched to v0.8.24 production tuning by default)LivenessSupervisor+SupervisorMetrics(OnceLockglobal)- 3 concrete adapters:
HubWsChannel— observesHubClient::last_recv+ signals reconnect viaHubClient::request_reconnectIrohRelayChannel— absorbsiroh_listener_deadflag into the unified APIIrohPeerChannel— naturalisesHealthState::failures(one channel per peer; escalates only at chronic failure count)
GET /api/test/transport_health— single unified diagnostic surface (HTTP API + Mac Tauri test-hooks at port 18181). Replaces 4 legacy endpoints (/api/status:hub_connected,/api/peers:latency_ms,iroh_listener_deadflag, journalctl scraping).
What got removed
HubClient::run_loopinlineidle_timeout = 120 scheck (now owned by the supervisor’sPolicy)- F1’s standalone
CONNECT_TIMEOUTconstant is now scoped to the connect call only (no longer a public knob)
Tests
cargo test --lib: 1517 → 1548 (+31 new tests)- New
tests/e2e/scripts/transport-chaos/Docker chaos suite:- C1 udp-blackhole-peer
- C2 sleep-wake
- C3 relay-restart
- C4 hub-blackhole
- C5 multipath-confusion
- C6 portal-restart
- C7 24-h passive soak (scheduled CI weekly)
- New
.github/workflows/transport-chaos.ymlruns C1-C6 fast (≤10 min) on every push touching transport code; C7 runs Sundays 02:00 UTC.
SOLID/DRY/KISS pay-off
- SRP: probe / classify / decide-restart are now three separate concerns in three different layers (
HealthState/HubClient→ adapters →LivenessSupervisor). - DRY: 86 grep hits on watchdog/idle_timeout/CONNECT_TIMEOUT/ last_alive across
src/p2p/v2/in v0.8.24, down to ≤ 12 hits in v0.9.0 — all insidetransport_health/. - KISS: adding a new transport (future BLE, WebTransport, …) requires implementing only
Heartbeat + Restartable— no edits to the supervisor, no new global constants, no new timeout knobs. - TDD-strict: every T-step (T0-T7) was a discrete commit, each leaving the tree green. RED scenarios committed at T1 before any supervisor existed; each subsequent T-step turned a defined subset green.
Carry-over from v0.8.24
- F4 regression test rewritten as
test_request_reconnect_breaks_inner_loop(supervisor-level behaviour moved to chaos C4). - F5 test-hooks endpoints (
/api/test/status,/api/test/peers) kept.
v0.8.24 — 2026-05-20
Hub WS regression coverage + Mac production debug
Completes the F-series begun in v0.8.23. The earlier release shipped the actual self-healing fixes (F1 connect timeout, F2 TCP keepalive, F6 boot-time peer-registry resync); v0.8.24 closes the remaining two items in the plan: a regression test for the production failure mode, and an on-Mac debug endpoint so the next incident is diagnosable without restarting the daemon with --browser.
-
F4 — post-handshake silence integration test. New
test_post_handshake_silence_triggers_reconnectinsrc/p2p/v2/hub/connection.rs::tests. Spawns a mock WS server that accepts the upgrade, reads Hello, then goes silent forever (exactly the it.local 2026-05-20 failure mode). Withstart_paused = true, the test advances tokio’s virtual clock past the 120 s idle timeout and asserts the daemon emits bothConnected(handshake worked) andDisconnected(silence detected, reconnect loop will retry).Required one surgical source change to make the integration testable:
connection.rsnow importstokio::time::Instantinstead ofstd::time::Instant. The two are bit-identical in production (both are monotonic-clock samples) but only the tokio variant advances withtokio::time::pause()/advance(). No behavioural change at runtime; substantial test-coverage win for the entire heartbeat / idle / reconnect path. -
F5 — Mac Tauri test-hooks for
/api/status+/api/peers. New routes in the existingCLIPSHOT_TEST_HOOKSserver insrc/gui/tauri_app.rs:GET /api/test/status→ cachedDaemonStatusFullJSONGET /api/test/peers→ cachedVec<FullPeerInfo>JSON Both read fromcrate::p2p::control::cache— the same source the production/api/statusand/api/peersHTTP routes serve. Mac Tauri-GUI mode does not bindtiny_httpon 15282 unlessuse_browser_ui = true; withCLIPSHOT_TEST_HOOKS=1(default port 18181) operators can nowcurl http://127.0.0.1:18181/api/test/statuson it.local to diagnose hub / peer state without restarting the app or enabling the full browser-mode HTTP API. Tests assert GET-only and route-string distinctness.
Notes
- 1517 lib tests (1514 → 1517, +3 new for F5 route predicates plus the F4 integration test).
- No new runtime dependencies.
v0.8.23 — 2026-05-20
Hub WebSocket self-healing (production incident 2026-05-20)
it.local Mac daemon stalled for 4+ hours with a single ESTABLISHED TCP socket to the portal and zero TLS handshakes completing. The reconnect loop was blocked inside tokio_tungstenite::connect_async_tls_with_config because the call had no timeout and inherited the OS-default TCP timeout (effectively unbounded on macOS without keepalive).
Three-layer fix:
-
F1 — connect timeout (15 s). Wraps the WS handshake in
tokio::time::timeoutso a stalled handshake aborts within seconds and the existing exponential backoff retries from scratch. Tests:test_connect_timeout_is_bounded_for_half_open_resilience,test_stalled_tls_handshake_does_not_hang_forever. -
F2 — TCP keepalive (30/10/3). New
src/p2p/v2/hub/keepalive.rsmodule opens TCP viasocket2, armsSO_KEEPALIVE+TCP_KEEPALIVE*, then hands the socket to tungstenite. Worst-case half-open detection on Linux: 60 s. macOS lacksTCP_KEEPINTVL/TCP_KEEPCNTso it falls back to the application-level 120 s idle timeout already inrun_loop. New dep:socket2 = "0.5". Tests: 6 inkeepalive::tests. -
F6 — boot-time peer-registry resync. New
AsyncDaemon::apply_authoritative_iroh_addrcollapsespeer_registry.addresses[]to the single hub-authoritative iroh addr on everyProcessPeerList/ProcessPeerJoined/PeerNodeAddrChangedevent. Eliminates the address accumulation that previously left daemons trying dead UDP ports on the first reconnect attempt. Tests: 4 inapply_authoritative_iroh_addr_*.
Other
- Pinned
socket2 = "0.5"with featureallfor cross-platform TCP keepalive accessors.
Net change: 1513 lib tests (1497 → 1513, +16 new). All pass.
v0.8.22 — 2026-05-17
Peer relay connectivity (6 layers fixed)
Connection through embedded peer relays now works end-to-end. Previously, behind-NAT nodes (main, gene) could not reach each other even when a publicly-routable peer (spex) was online with relay-server enabled — the relay address never made it from the publisher to the consumers.
- L1 — Relay DNS pre-resolved at startup: relay URL is resolved once on boot, so subsequent broadcasts never block on DNS even on Tailscale/VPN paths where the resolver is unreachable.
- L2 —
headlesscargo feature includesrelay-server: every headless daemon (CLI / server) can now act as a peer relay. Previously only GUI builds shipped the relay, which broke server-as-relay topologies. - L3 —
persist_hub_peerstoresrelay_addresses: relay addresses advertised by other nodes via the hub are now persisted to the local peer registry instead of being dropped after the WS frame is decoded. - L4 —
persist_hub_peermerges addresses when a peer changes ports / IPs, so we don’t lose the relay entry on the next hub broadcast. - L5 —
ping_peer_by_id(no DNS, no relay lookup): HealthWorker now pings peers byEndpointIdand reuses the iroh path cache, fixing health timeouts on Tailscale where DNS forrelay.clipshot.ccis blocked. - L6 — Portal persists + broadcasts
relay_addresses(KEY FIX): the hub previously hardcodedrelay_addresses: vec![]in everyPeerList/PeerJoinedmessage, so consumers never learned about the publisher’s relay even though the publisher reported it correctly. Migration 018 stores it; the WS layer now reads and broadcasts it.
End result: spex ✓ direct, main ✓ via spex relay, gene ✓ via spex relay.
HealthWorker adaptive backoff
Offline peers (e.g. Windows VM turned off) used to be pinged every 30 s forever — wasting CPU, flooding logs, and making the UI flap between ✓ and timeout. Health probes now back off after sustained failure:
| consecutive failures | next-probe wait |
|---|---|
| 0..3 | 30 s (base) |
| 3..10 | 5 min |
| 10+ | 15 min |
A successful probe — or a hub peer_joined event for that node — resets the counter, so a returning peer is rediscovered within one cycle (≤ 30 s) instead of waiting out the long backoff window. The 1–2-failure “flaky peer” guard is preserved by regression test.
Other
- macOS clipboard: received files now write through
NSFilenamesPboardTypeso Finder paste works for transfers, not just text/images.
v0.8.21 — 2026-05-13
Unified Device Removal
- PeerRemoved P2P gossip — remove a peer from any node, all connected peers learn via native QUIC
- Hub RemoveDevice — removal propagates through portal to all group members
- PeerRemovedFromGroup — distinct hub message for explicit removal (vs soft disconnect)
- No gossip loop — receivers remove locally without re-broadcasting
- Portal dashboard delete — “Remove” button deletes node + broadcasts to all peers
- device_id canonical identity — portal deduplicates by device_id, not node_id
Security
- SQL injection fix — portal remove_device uses parameterized queries (was format!)
Pair Flow Fixes
- pair_confirm hub reconnect — generator connects to hub after writing provisional token
- reconnect_hub reads settings — fills missing hub_url/token from settings.toml
UI
- Tray menu “Manage Account” — replaces “Upgrade to Pro”, checks is_pro from hub cache
- Onboarding overlay — 4-slide guide after first pair (Cmd+B comparison table)
Documentation
- Cmd+B / Ctrl+B paste path hotkey fully documented
- macOS permissions — Input Monitoring + Accessibility requirements
- Registration flow — setup, OAuth, dashboard, link account, Free vs Pro
- Quick Start Guide — first section of USER_GUIDE
Tests
- Rust: 1844, Vitest: 299, Portal: 35
- E2E: 23 scenarios (pair, sync, remove, portal, collisions, dedup)
v0.8.2 — 2026-05-12
Group Token Flow
- group_token in PairOffer: joiner adopts generator’s token automatically
- Deferred token write: pair_generate holds token in memory, persists only on pair_confirm (prevents UI flash)
- group_switch warning: PairDialog shows yellow banner when joining a different group
- mDNS carries group_token in TXT record for LAN pairing
- Hub without registration: any non-empty token gets hub + relay access (anonymous group auto-created)
Portal
- link-group updates anonymous groups: claims ownership on account registration
- link-group creates group row: dashboard shows group immediately after link
- Setup page accepts existing token: pre-fills from URL param
- Dashboard token card: prominent “YOUR GROUP TOKEN” with copy button after login
UI Improvements
- Loading spinner in AppContent before settings resolve (no more WelcomeScreen flash)
- Copy button for group_token in Settings page
- TransferIndicator in header: shows active transfer progress (↑ img.png 439KB/1.2MB)
- connection_type detection: relay vs direct based on address pattern
- device_type inference: server/desktop heuristic from peer name
- Transport labels: “Relay · QUIC” / “Direct · QUIC” in peer details
- Latency display: shows “—” instead of “Measuring…” when unavailable
Infrastructure
- install.sh: –code optional, –token flag for direct token setup, v0.8.2 banner
- Windows NSIS installer: 5.1MB installer with Start Menu shortcuts + uninstaller
- broadcast_queue_size: now settable via HTTP API (validation 1-10)
- RTT consolidation: ConnectionMonitor is single source of truth, 47 lines dead code removed
Tests
- Rust: 1616 → 1634 (+18)
- Vitest: 247 → 262 (+15)
- Portal: 35
Changelog
[0.8.1] - 2026-05-12
🐛 Relay Reconnect Resilience
- Central relay (
relay.clipshot.cc) is no longer permanently blacklisted after temporary downtime - Relay connect attempts increased from 3 to 5 with exponential backoff (2s→4s→8s→16s = 30s window)
- Periodic health reset every 5 minutes ensures central relay is always retried
- Peer relays still blacklisted after repeated failures (prevents hot-loop on dead peer relays)
✅ Verification
- 1612 Rust tests pass, clippy clean
- RED→GREEN TDD:
test_reset_failures_clears_counter,test_connect_via_relay_does_not_blacklist_central_relay
[0.8.0] - 2026-05-11
🚀 Unified Pairing (pair-v2) — Breaking Change
Complete rewrite of the pairing subsystem. One numeric 6-digit code replaces all previous pairing mechanisms (portal WORD-WORD-NN codes, LOCAL_WORD_NN LAN codes, tabbed UI). Pairing no longer requires a Portal account — group_token is generated locally at first pair and accounts can be linked later.
New pairing API surface
POST /api/pair/generate→{code, digits, expires_in_secs}POST /api/pair/join→{digits, peer_addr}POST /api/pair/abort→ best-effort cancellation after confirmation mismatch- Tauri commands:
pair_generate,pair_join,pair_abort - CLI:
clipshot pair/clipshot pair 482917(6-digit numeric)
New pairing architecture
src/pair_v2/module tree:code.rs,digits.rs,offer.rs,handshake.rs,broker.rs,runtime.rsPairBrokerraces multiple discovery transports (mDNS + Portal/api/pair/v2)PairDiscoverytrait withInMemoryDiscovery,MdnsDiscovery,PortalDiscoveryimplementations- X25519 keypair + DH shared secret → 4-digit confirmation digits for MITM detection
ensure_group_token()generates local UUID at pair time (account-decoupled onboarding)
Removed legacy pairing
- Deleted
src/pair.rsandsrc/http/handlers_local_pair.rs - Removed Tauri commands:
pair_generate_code,pair_join_code,pair_generate_local,pair_join_local - Removed HTTP routes:
/api/local-pair/generate,/api/local-pair/join - Removed tabbed PairDialog UI (Pair Code / Local Pair / Scan LAN tabs)
- Removed
joinOnly,defaultTabprops from PairDialog/AddPeerDialog - Removed discover tab from AddPeerForm
UI changes
- PairDialog: 2-button menu (Generate code / Enter code) → generate/join/verify flow
- WelcomeScreen: primary CTA is “Connect your first 2 devices” (no create-account)
- SettingsPage: “Sign in to link this group” action when group_token exists
- AddPeerForm: flat layout with pair code + advanced section (paste URI / manual)
Portal side (clipshot_portal)
POST /api/pair/v2+GET /api/pair/v2/:code(publish/resolve pair offers)POST /api/account/link-group+GET /api/account/groups(deferred account linking)- Legacy
/api/pairpreserved for backward compatibility (pending removal window)
🐛 Resilience fixes
- Iroh listener auto-restart: detect socket closure reactively via
endpoint.is_closed()instead of waiting for the 120s watchdog stall. - Cmd+B echo loop:
paste_file_pathno longer triggers a duplicate broadcast of the same image. - mDNS self-discovery pollution: address validator rejects loopback, unspecified, x.x.x.0 and port 0.
- Peer registry one-time GC: invalid legacy entries dropped at
PeerStore::loadtime. - Peer-relay blacklist:
find_peer_relay_urlskips known-dead relays.
✅ Verification
- Rust:
cargo test --lib— 1603 passed,cargo clippy --all-targets -- -D warningsclean. - Frontend: 247 vitest tests passed,
tsc --noEmitclean. - E2E:
pair-v2-portal-test.shpasses (code exchange + digit parity + joiner connectivity). - E2E:
pair-v2-lan-test.shSKIP (Docker mDNS multicast limitation).
[0.7.6] - 2026-05-10
🚀 Connectivity Reliability
- Central
relay_urlis now stored separately from direct addresses and is never auto-cleared. - Bare
iroh://<node>addresses are rebuilt into full relay-aware connect URLs at connection time. - Connection-refused cleanup now removes only stale direct addresses, preserving the central relay path.
- Removed the “hub down → strip relay” behavior that made relay peers disappear after transient hub outages.
⚡ Broadcast & UX
- Detached
BroadcastWorkersends clipboard updates outside the tick loop, so slow peers no longer block local clipboard polling. - Tray/Dock Sending state appears immediately on copy and returns to Idle after the first confirmed peer delivery.
- Broadcast uses live peer cache, including hub-discovered/transient peers.
- Large screenshots and files continue sending even when one relay peer is slow or temporarily unavailable.
🐛 Fixes
- Fixed spex/main/gene visibility loss caused by stale bare Iroh addresses.
- Fixed idle UI flicker where sync-proven devices disappeared after TTL despite the next relay sync succeeding.
- Fixed stuck Sending indicator when a slow relay peer never completed.
- Fixed reconnect state honesty: Online reflects real sync delivery instead of handshake-only success.
- Extended stale connection expiry to reduce idle UI flapping.
✅ Verification
- 1527 Rust lib tests passing.
- Clippy clean with
-D warnings. - Production test: macOS → gene/main/spex all confirmed via native QUIC relay/direct broadcast.
[0.7.5] - 2026-05-09
🚀 Install Without Pair Code
curl -fsSL https://clipshot.cc/install.sh | bash— works without--code- First-time install opens browser for account creation (
clipshot setup) - After install shows unique pair code for adding second device
- Portal: Setup Sessions API (POST/GET /api/setup/sessions)
🔐 Pair Code Dictionary Expanded
- 64 → 256 adjectives, 64 → 256 nouns, 90 → 990 numbers
- 64.8M unique codes (was 368K) — near-zero collision risk
- Zero overlap between adjective and noun word lists
- Retry on collision (PRIMARY KEY constraint)
🐛 Bug Fixes
- Relay fixed: container must be on
bundle_spexDocker network - Reconnect hot loop:
on_connected()called afterconnect_to_addr() - Canonical + full addr both registered in ConnectionMonitor
- Removed
WORD-WORD-00placeholder from all user-facing docs
📚 Documentation
- Compact screenshots (1000×700), fresh annotations
- Deploy skill updated: Windows on main, dual landing paths, relay instructions
- install.sh shows
<YOUR-CODE>notWORD-WORD-00
[0.7.4] - 2026-05-09
🚀 Non-Blocking Clipboard Broadcast
- Clipboard stage no longer blocks on network I/O (was 1-15s per broadcast)
- New
BroadcastQueue(SRP): bounded FIFO send buffer, configurable 1-10 items - New
BroadcastPendingtick stage sends queued items in network phase - 4 rapid screenshots → all 4 saved locally + all 4 delivered (zero loss)
🔧 Consistent Filenames Across Nodes
- Sender includes real filename (
img_YYYYMMDD_HASH.png) in wire header - Receiver uses sender’s filename directly (was generating different name)
- Image content type preserved on receive (was converting Image → File)
🔐 Post-Quantum Key Exchange
cargo build --features pqenables ML-KEM + X25519 hybrid via aws-lc-rs- Default build uses ring (lighter, better cross-compile)
⚙️ New Setting: broadcast_queue_size
- Default: 1 (only latest clipboard item, previous dropped)
- Max: 10 (keep recent items in queue)
- Available in GUI Settings → Sync section
🐛 Bug Fixes
- Fixed reconnect hot loop (1700+ reconnects/sec blocking clipboard)
- Fixed ConnectionMonitor max_peers=3 blocking all outbound on gene
- Fixed gene visibility (was filtered as transient peer)
📚 Documentation
- Full CLAUDE.md audit: test counts, features, modules, settings, architecture decisions
- Screenshots retaken: settings (broadcast_queue_size), peers (QUIC), pair dialog (3 tabs)
- Token redacted from all screenshots
[0.7.3] - 2026-05-08
🚀 Major: iroh 1.0 Migration + Native QUIC
Complete rewrite of the P2P transport layer. Replaced legacy chunked protocol with native QUIC streams via iroh 1.0.0-rc.0.
Highlights
- iroh 1.0.0-rc.0 — latest P2P networking library
- Native QUIC sync —
write_all/read_to_endreplaces 6000+ LOC of chunked transfer code - -10,000+ LOC removed — deleted legacy transport, chunked transfer, PeerManager, AsyncPeer
- 20MB byte-exact proven through 3-hop mesh chain (Docker) and production relay
- Parallel broadcast — all peers notified concurrently instead of sequentially
- ConnectionTracker — lightweight replacement for PeerManager (298 LOC vs 1856)
Transport
- Native QUIC streams via
clipshot/sync/1ALPN protocol wire.rs— postcard binary header format (ContentHeader)sync_protocol.rs— send_content/receive_content/push_content_to_peersync_handler.rs— iroh ProtocolHandler for Router- Connection drop race fix in sync_handler (proper stream lifetime management)
- Broadcast timeout scales with content size (15s base + 5s per MB)
- Skip recently failed peers (5min cooldown) to avoid timeout waste
Security & Limits
EndpointHooks— device limit enforcement on outbound connections via iroh hooksClipshotHookswithbefore_connectpolicy (12 TDD tests)
Relay
- iroh-relay upgraded from 0.96 to 1.0.0-rc.0
- Caddy h1-only ALPN for relay.clipshot.cc WebSocket upgrade
Deleted (legacy)
iroh_transport.rs,iroh_factory.rs,handshake.rs,codec.rs(protocol/)chunked.rs(transfer/)content_handler.rs,content_send.rs,content_recv.rs,content_chunked.rs(peer/)failover.rs,probe.rs(relay/)connection_coordinator.rs(daemon/)manager.rs,broadcast_engine.rs,peer_index.rs,receive_loop.rs(manager/)AsyncPeer,PeerSender,PeerReceiver,PeerHealthtraitsconverter.rs,dedup.rs(peer/)role.rs,factory.rs(protocol/)transport/module (DataTransport, mock)transfer/module (BoundedQueue)test_helpers.rs- Legacy integration tests (
iroh_integration.rs,iroh_stress_test.rs) - Dead Message variants: ChunkStart, ChunkProgress, ChunkComplete, ChunkCompleteAck, ContentWithMeta
process_accepted_peers,AcceptedPeer, legacy health check
GUI Fixes
- Latency measurement — RTT from native QUIC broadcast time (not legacy Ping/Pong)
- Peer names resolved — iroh hex → friendly name via registry fallback search
- “Unknown device” eliminated from Overview, History, Peers
- Tray icon stable — no more NoPeers↔Idle flapping
- “Last sync: just now” — timestamp updated after each native sync
- Device type — removed misleading “Laptop” label when unknown
- Transport badge — shows “QUIC” instead of misleading “Direct”
- Legacy settings hidden — Transfer timeouts, Retry Attempts, Timeout (ms)
- Addresses populated — primary address shown when alternate list empty
- Parallel broadcast — 3 peers in 15s max (was 45s sequential)
- Sync state — uses native_connected_peers count (was always “no_peers”)
Infrastructure
- iroh-relay 1.0 Docker image on spex
- systemd user service on main and gene
- Root daemon cleanup on main
- Stale peer cleanup across all nodes
- 20MB mesh forwarding: container (no internet) → main → gene byte-exact
Tests
native_sync_test.rs— 3 integration tests (text, 1MB, bidirectional)hooks.rs— 12 TDD tests for EndpointHooksconnection_tracker.rs— 7 TDD testswire.rs— 5 ControlMessage serde tests- Docker E2E: 3-node text sync + 20MB byte-exact verified
[0.7.2] - 2026-05-05
- Native QUIC sync protocol (wire.rs, sync_protocol.rs, sync_handler.rs)
- Router + ProtocolHandler integration
- ConnectionCoordinator for duplicate prevention
- Streaming chunks (removed per-window ACK)
- Relay retry 3x with 2s delay
- Caddy h1-only ALPN fix for relay
[0.7.1] - 2026-04-29
- device_id persistent identity
- effective_relay_url auto-derives from hub_url
- Portal device_id in PeerInfo broadcasts
- Zombie dedup in portal DB
- max_file_size sync on subscription
- Chunked transfer explicit failure (hash mismatch → ChunkComplete{hash=0})