Security is the reason awaykit exists. This document is the living threat model. If a design decision here is wrong, please open an issue.
- An attacker who compromises the relay learns nothing (ciphertext only).
- An attacker who steals the phone cannot escalate to full laptop access (session-scoped permissions, optional biometric gate for approvals).
- An attacker on the network cannot read or inject messages (E2E encryption with mutual authentication).
- No third party — including us — ever holds keys or plaintext.
The sections below (Pairing, Transport, Authorization scopes) describe the
target design. Here is what the code actually does right now, and its
honest limits.
What's built:
- Shared-key pairing via QR. On first run the daemon mints a random 256-bit
key
K, persisted at~/.awaykit/key(mode600). The pairing QR encodeshttp://<lan-ip>:<port>/#k=<key>— the key rides in the URL fragment, which browsers never send to the server, soKis not transmitted over the (plain-HTTP) network during pairing. Re-pair anytime withnpm start -- --pair. - Forward secrecy via per-session ephemeral keys.
Konly authenticates the handshake; it never encrypts channel data. On each connection, phone and daemon run an X25519 exchange (each side's ephemeral public key is sealed underK, so an attacker withoutKcan't inject their own) and derive a throwaway session key. Recording ciphertext and later stealingKdoes not decrypt past sessions — the ephemeral secrets are already gone. - Authenticated encryption on every message. Each phone⇄daemon message is
sealed with NaCl
secretbox(XSalsa20-Poly1305) under the session key, with a random nonce. The SSE stream uses a single opaque event type, so even the kind of message is hidden. The same primitives run in the browser via a vendoredtweetnacl(WebCrypto'ssubtleis unavailable over plain HTTP, so we bundle a pure-JS lib; onlycrypto.getRandomValues, which works on HTTP, is used). - Session gate.
/eventsand/respondrequire a session cookie that is only issued after the phone proves it holdsK(a sealed, time-fresh proof carrying its ephemeral public key, toPOST /session). - Loopback-only
/hook. The hook endpoint rejects non-loopback connections, so a device on the LAN cannot inject fake tool prompts. - Connection is the switch. With no paired phone connected and no push subscription registered, the daemon does not intercept — Claude Code uses its normal on-laptop permission flow.
- Append-only audit log. Every decision (approve / deny / aborted) is
recorded as a JSON line at
~/.awaykit/audit.log— a local record of what you approved while away. Read recent entries over loopback viaGET /audit. - Zero-knowledge relay (optional,
AWAYKIT_RELAY). For remote access with no VPN: the daemon holds an outbound connection to a self-hosted relay (no inbound ports), and the phone reaches the relay from anywhere. Rooms are keyed by an irreversible hash ofK; every payload is an opaque sealed blob using the same forward-secret handshake as LAN mode. The relay learns timing, direction, and size — never keys, never plaintext. Remote sessions ping every 25 s and expire after 90 s of silence, so "connection is the switch" stays truthful remotely. - Push notifications, zero-knowledge (v0.6). The daemon owns a VAPID keypair
(
~/.awaykit/vapid.json) and sends each wake-up outbound itself to the browser's push endpoint. The payload is encrypted per RFC 8291 to the subscription's own keys, so the push service — and any relay — forward only ciphertext; only the device's service worker can read it. Subscriptions arrive over the already-encrypted channel and are stored locally (~/.awaykit/push-subs.json). Push needs a secure context (HTTPS), so it runs over the relay or an HTTPS tunnel, not plain-HTTP LAN. A registered subscription extends "connection is the switch": awaykit intercepts and wakes a closed app, and falls back to the on-laptop prompt only when neither a live phone nor a subscription exists. - Optional self-signed HTTPS on LAN (v0.7,
AWAYKIT_TLS=1). The daemon mints a persistent self-signed cert (~/.awaykit, SANs = the LAN/VPN IPs) and serves HTTPS; the banner prints a SHA-256 fingerprint to verify the first time your phone warns. This gives the LAN app shell + channel TLS encryption and tamper-evidence, and — if you trust the cert — a secure context that unlocks LAN push without the relay. Loopback callers (hook shim, control CLI) find the daemon via an advertised~/.awaykit/endpoint.jsonand trust the self-signed cert only on loopback (where identity is meaningless — same machine).
What v0.1 defends against: a passive Wi-Fi sniffer (sees only ciphertext);
an unauthorized device on the same network (no K ⇒ can't read events, can't
forge an approval, can't pass the session gate); tampered ciphertext (Poly1305
auth tag rejects it).
Residual risks — NOT yet covered (tracked for later milestones):
- App shell integrity on plain-HTTP LAN (the default). With plain HTTP the
HTML/JS is delivered unencrypted, so an active on-path attacker (ARP spoof /
rogue AP) could tamper with the client code before any key is used. Any HTTPS
transport closes this: the zero-knowledge relay and a VPN both serve the
shell over TLS, and
AWAYKIT_TLS=1serves it over self-signed HTTPS on the LAN. Note self-signed is tamper-evident, not tamper-proof — a browser can't pin the cert, so verify the printed SHA-256 fingerprint on first accept; installing the cert as trusted, or using the relay/VPN, is strongest. Plain-HTTP crypto stops passive attackers; add TLS for active-MITM resistance. - Key at rest on the phone lives in
localStorage. A device-scoped biometric gate is future work. - Relay app-shell trust. In relay mode the phone loads the app shell from the relay host — host the relay behind HTTPS and treat that host as part of your trust base (it can serve code, but it still never sees keys or plaintext messages, which live in the URL fragment / on-device).
- First-run: daemon displays a QR code containing its public key + one-time token.
- Phone scans it, sends its public key back over the ephemeral channel.
- Both sides derive a shared secret (X25519), verified with a short auth string shown on both screens.
- Result: mutual TOFU key pinning. No passwords, no accounts.
Two supported modes:
| Mode | How | Trade-off |
|---|---|---|
| VPN | WireGuard / Tailscale between phone & laptop | simplest, no relay needed |
| Relay | WebRTC data channel; self-hostable relay for signaling + push wake-ups | works anywhere, relay sees ciphertext only |
All payloads are additionally encrypted at the application layer
(libsodium crypto_secretstream) — the transport is not trusted.
The phone client is scoped to the agent session protocol, not a shell:
session.read— view output streamsession.respond— answer agent prompts (approve/deny/text)session.prompt— send new instructionssession.kill— terminate the sessionshell.full— ❌ off by default; explicit opt-in with a warning, per-device
Every action is logged locally to an append-only audit file.
Chat mode lets the phone start and drive agent sessions, not just answer
prompts. Honest framing: a paired phone can already make the agent run anything
(it approves Bash), so chat removes friction from an existing capability
rather than granting a new one. It ships hardened regardless:
- Off by default. Requires
AWAYKIT_CHAT=1and a non-emptyAWAYKIT_PROJECTSallow-list. Without both,/chatreturns 403 and no session manager exists. - Allow-listed dirs only. A session can start only in a directory on the
allow-list; arbitrary paths are rejected server-side (
session.promptcan't escape intocwdof your choosing). - No permission bypass. Managed sessions run
--permission-mode defaultand never--dangerously-skip-permissions; the daemon injects its own hook config so every tool call still crosses your phone as an approval card.shell.fullremains off. - Fully audited. Every
start/send/interrupt/kill(with text) is written to the append-only audit log. - In-memory transcripts. Conversation history lives in memory (bounded ring); the durable record is your repo + Claude's own session files.
Residual: the injected hook settings file (~/.awaykit/chat-hook-settings.json)
points at hook.js; treat ~/.awaykit with the same trust as the daemon itself.
- Protecting against a fully compromised laptop or phone OS
- Multi-user / team access control (single-owner assumption)
Please open a private security advisory on GitHub rather than a public issue.