TL;DR — cordless manages many remote terminal / coding-agent (Claude Code, Codex) sessions like browser tabs, with sessions that survive disconnects. v0.5 adds a hardened Electron desktop app (Windows
.exe, macOS.dmg, Linux.AppImage/.deb), a proper>_<brand logo made with gpt-image-2, and a security course-correction: QR pairing stays the only default way in, and the desktop's one-click "Connect to this computer" uses a loopback-only credential that a Tailscale or LAN address is rejected for. As always — designed with GPT-5.6 Sol, driven with GitHub Copilot CLI.
I shipped cordless, then made it livable in v0.4 — seamless resume, an in-app QR scanner, no more truncation. v0.5 is about the two things a "real" tool needs that a phone app alone can't give you: a proper desktop experience, and an identity. And it's where a security instinct paid off.
A face for the project: the >_< logo
cordless needed a mark. I generated one with gpt-image-2 on my Azure AI Foundry deployment — a small Python script (tooling/gen_logo.py) that POSTs a prompt and writes back the base64 PNG. After iterating on a few concepts, the winner was almost too obvious: >_< — a shell prompt >_ that doubles as a happy little face — set in a blue→violet gradient inside a terminal-window frame.
One gotcha worth remembering: gpt-image-2 won't render a transparent background (you get an HTTP 400 if you ask). So I generated the mark on a dark background and luminance-keyed it transparent with Pillow (tooling/apply_logo.py) — the same script then resizes it into every size the project needs: PWA icons (icon-192/512, a maskable variant, apple-touch, favicons), the landing-page hero, and the full set of Android launcher / adaptive-icon / splash assets.
A subtle bug fell out of this: the repo's .gitignore had a blanket *.png rule (to keep Playwright screenshots out), and it was silently swallowing the new app icons. The APK and PWA would have shipped with missing icons. Two lines fixed it — !client/public/*.png, !desktop/build/*.png — a good reminder to always check git status after adding binary assets.
The desktop app — a hardened Electron shell
The headline feature. When you're at your dev box (or remoted into it), a phone is the wrong tool — you want a real window with a real keyboard. So v0.5 ships a desktop app for Windows, macOS, and Linux.
I designed the architecture in my running conversation with Sol, and we landed on the pragmatic choice: the Electron window loads the daemon's own served page at http://127.0.0.1:<port>. Because it's same-origin to the daemon, there are zero CORS/CSP changes — it's the exact same UI, sessions, tabs, and replay you already have, just in a window.
The interesting part is the security hardening, straight from Sol's review:
-
contextIsolation: true,nodeIntegration: false,sandbox: true,webSecurity: true. - Navigation is pinned to the trusted loopback origin —
will-navigateandsetWindowOpenHandlerreject anything else; all new windows,<webview>s, and permission requests (camera, mic, geolocation…) are denied. - The renderer gets a tiny preload bridge and nothing else:
{ platform, getLocalCredential, startDaemon, retry }. Nofs, no shell, noopenExternal. - Every IPC call validates that the sender is one of our own trusted pages.
startDaemonaccepts no path, command, or argument from the page — it resolves the installed CLI itself. - If the daemon isn't running, the app shows a Start daemon / Retry screen instead of a blank window.
- The port is resolved with a strict precedence — the credential's
server, then the daemon config, then7443— and every candidate is validated to be loopbackhttp(nohttps, no userinfo, no path, nolocalhostname ambiguity).
That last bit of parsing is security-critical, so I pulled it into a pure module (desktop/lib/resolve.js) and wrote 24 unit tests for it. It runs in CI on every push.
QR-first security — the course-correction that mattered
Here's the moment I'm most glad about. While wiring the desktop app, I built a convenient "auto-connect on localhost" path. It worked. It was also wrong — it quietly bypassed pairing. The rule for cordless is: QR / pairing-code is the one true way in.
So I tore the auto-connect out and rebuilt it as an explicit, opt-in button. The daemon now writes a desktop-credential.json (mode 0600; it stores only the token's hash) tied to a device with scope: "loopback". The authenticate() path accepts that token only when the socket peer is 127.0.0.1 / ::1 — checked against the real socket address, never a spoofable header. A Tailscale 100.x or LAN address is rejected outright. In the UI, the 🖥️ Connect to this computer button only appears when the Electron bridge actually hands over a credential; open the same page in a plain browser and you see QR/code only.
I verified it with a test that asserts exactly this: loopback IPs authenticate, and 100.64.x.x (Tailscale's range) does not. It's a local convenience that is structurally incapable of being a remote bypass.
ok ip=127.0.0.1 authed=true (expected true)
ok ip=::1 authed=true (expected true)
ok ip=203.0.113.5 authed=false (expected false)
ok ip=100.64.1.2 authed=false (expected false) <- Tailscale, still rejected
=== LOOPBACK-SCOPE ENFORCEMENT PASS ===
Prerequisites & Tailscale, documented properly
A fair complaint about the earlier docs: the prerequisites and Tailscale setup weren't written down clearly. Fixed. The README and landing page now spell out, with Sol's help:
-
Prerequisites — Node.js 22, the platform build tools
node-ptyneeds, Tailscale, and (on Windows) PowerShell 7, with download links. -
Tailscale setup —
tailscale up/status/ip -4, an optional device tag, a copy-pasteable tailnet ACL that locks TCP7443to your own identity, and a Windows Firewall rule that only opens7443on the Tailscale adapter (100.64.0.0/10). -
Resume your sessions —
cordless installonce, then open the desktop app orhttp://localhost:7443; your tabs reattach, and after a reboot the daemon reopens them from its restore manifest.
How it was built
Same loop as the whole project: me on GitHub Copilot CLI, Sol as the design partner and reviewer — and this time I kept the conversation stateful, so Sol remembered every prior decision instead of re-litigating them. The desktop architecture, the Electron hardening checklist, and the loopback-credential rules all came out of that back-and-forth. CI does the heavy lifting: pushing a v* tag builds the Android APK and the three desktop installers on native GitHub runners and attaches them to the release. And npm test is now a self-contained harness that boots an isolated daemon and runs every suite — protocol E2E, security headers, the new desktop-credential + loopback-scope checks, and session-restore across a daemon restart. Seven suites, green.
The good
v0.5 is the version where cordless stopped feeling like a phone experiment and started feeling like a product: it has a name and a face, it runs as a real desktop app, and — the part I care about most — the convenience features were built without weakening the security model. The one-click local connect is genuinely handy, and it cannot be turned into a remote hole. That's the bar.
Try it
- ▶️ Live / install the PWA: naveenneog.github.io/cordless
- 🖥️ Desktop app (Win/macOS/Linux): github.com/naveenneog/cordless/releases/latest
- 📦 Android APK: github.com/naveenneog/cordless/releases/latest
- 💻 Source: github.com/naveenneog/cordless
Part of the #AI4Good series. Built one day at a time. — @naveenneog

Top comments (0)