Run Claude Code on your desktop from your phone, over a tunnel that costs nothing and a QR code you scan once. No server, no account, no hosting bill — the desktop does the work and the phone is a window onto it.
That screenshot is the real app on a real phone: prompt at the top behind a copper rule, the tool call it made, the answer, and what the turn cost.
Needs Node 23.6+ (the host runs TypeScript directly, no build step) and a working
claude CLI — the same login you already use in your terminal.
cd host
npm install
npm start
That prints a QR code, and writes the same code to ~/.kablo/qr.png for terminals that
mangle block characters. Scan it with the app and you are connected.
Building the app needs JDK 17+ and an Android SDK with platform 37:
cd android
./gradlew :app:assembleRelease # app/build/outputs/apk/release/app-release.apk, 4.1 MB
The APK is signed with the debug key on purpose — it is sideloaded onto your own phone, and a real keystore committed to a public repo would be worse than none.
The QR holds a token and one or more addresses. The phone dials all of them at once and keeps whichever answers first, so you never have to know where you are standing:
| Transport | Address | When it wins |
|---|---|---|
| Cloudflare quick tunnel | wss://<random>.trycloudflare.com |
Anywhere. No account, no card, no DNS. cloudflared is downloaded on first run (~55 MB, into ~/.kablo/bin). |
| Tailscale | ws://100.x.y.z:7420 |
If you already run Tailscale. Stable address, so the QR never goes stale. |
| LAN | ws://192.168.x.x:7420 |
Opt-in with --lan. Fastest at home. |
The header says which one you got in on — live · lan, live · tunnel. Turning WiFi
off mid-session moves you to the tunnel in about twenty seconds without touching
anything.
The token is permanent; only the address rotates. Plain ws:// is accepted only for
100.64/10, 10/8, 172.16/12 and 192.168/16 — a QR pointing the phone at
ws://some-public-host is refused rather than sending the token in the clear.
The agent keeps running when the phone disconnects. Every frame is numbered, the host keeps the last 2000, and a phone that reconnects asks for everything after the last number it saw. Going into a tunnel on the metro does not cancel a refactor.
mic next to the composer dictates into the text box and stops there. It never sends.
The far end runs shell commands without asking, so a misheard word has to stay editable
— dictation appends to whatever you had already typed, and you press send yourself.
Uses the phone's own recogniser in the phone's own language.
kablo runs Claude Code with bypassPermissions. It will not ask before writing a
file, deleting one, or running a shell command. Whoever holds the token has that.
What protects it: a 256-bit token that never crosses the wire in cleartext, every HTTP path answering 404 so a stranger who finds the tunnel URL sees nothing, and a delay that doubles on each wrong token — 250 ms, 500 ms, 1 s, up to 5 s. Deliberately not a lockout: behind a tunnel every connection arrives from 127.0.0.1, so locking an address out would let a stranger lock out your own phone.
What does not protect it: anything, if you photograph the QR and post it. Rotate with
npm start -- --rotate, list paired phones with --who.
--port 7420 listen somewhere else
--model <id> override the model the agent uses
--scan a,b directories to look for git repos in
(default: ~/projects and ~/Desktop)
--lan add the LAN address to the QR
--no-tunnel skip Cloudflare entirely
--rotate mint a new token; every paired phone must scan again
--who list phones that have authenticated
- No diffs. A file edit shows as
src/agent.ts · 12→18, not as changed lines. You can see that something was written, not what. - Text only. Images the model produces, and anything a tool renders visually, do not reach the phone.
- One phone at a time. A second connection displaces the first.
- Sleep kills it. If the desktop suspends, the tunnel dies and the run dies with it. Nothing here keeps the machine awake.
- A quick tunnel URL changes on every launch. Without Tailscale, that means rescanning the QR each time you restart the host. The token survives; only the address is stale.
- The pairing was lost once, on a phone that had taken six sideloaded upgrades in an
hour, and it has not reproduced. The Keystore key that decrypts the stored token can
be dropped by the OS; that path now logs to
kabloin logcat instead of silently showing the scan screen, but the cause is unconfirmed. - macOS downloads nothing.
cloudflaredis fetched automatically for Windows and Linux only; on macOS runbrew install cloudflaredfirst.
On a Galaxy A52 (Android 14) against Windows 11, Node 24.18, JDK 21:
- Pairing by camera, then a prompt typed on the phone that created a file on the desktop
and ran
git status. - The same session over the Cloudflare tunnel from mobile data with WiFi switched off — 415 ms to connect, reconnect and transport switch handled without intervention.
- Wrong tokens, empty tokens, an old protocol number, and a prompt sent before auth: all refused, and the owner's phone still admitted in 4 ms during the attack.
- Host restarted underneath a live phone: it reconnected and reopened the project by itself.
- 20 host tests and 16 Android tests, the latter decoding real frames captured off a running host.
Not tested: Tailscale (not installed here), macOS, tablets, Android below 14, and any turn long enough to exercise compaction.
host/src/protocol.ts and android/.../Wire.kt describe the same wire format twice and
nothing enforces that but tests. After editing either, recapture the fixtures:
cd host && npm start -- --no-tunnel --lan # leave running
node scripts/capture.mjs && node scripts/capture.mjs --fault
cp frames.json ../android/app/src/test/resources/frames.json
cd ../android && ./gradlew :app:testDebugUnitTest
host/scripts/probe.mjs drives a full turn from the command line without the phone,
which is the fastest way to tell whether a problem is in the host or the app.
