Self-hosted PWA launcher for Claude Code sessions on a remote dev box
  • Python 78%
  • JavaScript 10.5%
  • CSS 7.9%
  • HTML 1.9%
  • Shell 1.7%
Find a file
Hiten Patel cec431e95c
Some checks failed
CI / lint-and-test (push) Failing after 42s
fix(supervisor): drop -x/-y from tmux new-window; size via resize-window
2026-10-02 08:17:26 +00:00
.github/workflows ci: migrate to arm64 runner (Oracle VM) 2026-04-24 16:37:08 +00:00
backlog docs+chore: supervisor-pivot README/AGENTS rewrite; bump SW cache to v32 2026-09-09 04:46:20 +00:00
config chore: baseline working-tree state (files browser, terminal, providers) before supervisor pivot 2026-09-09 00:42:42 +00:00
docs chore: baseline working-tree state (files browser, terminal, providers) before supervisor pivot 2026-09-09 00:42:42 +00:00
hybrid_agent fix: harden hybrid delegation lifecycle 2026-08-29 14:46:58 +00:00
scripts fix: harden hybrid delegation lifecycle 2026-08-29 14:46:58 +00:00
static feat: FastAPI lifespan, banner parsing, web-push notifications 2026-09-09 05:32:31 +00:00
systemd v0.2.0 foundation: tests, lint, log rotation, CI 2026-04-21 11:20:34 +00:00
tests fix(supervisor): drop -x/-y from tmux new-window; size via resize-window 2026-10-02 08:17:26 +00:00
.gitignore chore: ignore local worktrees 2026-08-29 10:44:06 +00:00
AGENTS.md docs+chore: supervisor-pivot README/AGENTS rewrite; bump SW cache to v32 2026-09-09 04:46:20 +00:00
app.py feat: FastAPI lifespan, banner parsing, web-push notifications 2026-09-09 05:32:31 +00:00
CLAUDE.md chore: baseline working-tree state (files browser, terminal, providers) before supervisor pivot 2026-09-09 00:42:42 +00:00
LICENSE Initial commit: claude-code-pwa 2026-04-21 06:57:34 +00:00
push.py feat: FastAPI lifespan, banner parsing, web-push notifications 2026-09-09 05:32:31 +00:00
pyproject.toml v0.2.0 foundation: tests, lint, log rotation, CI 2026-04-21 11:20:34 +00:00
README.md docs+chore: supervisor-pivot README/AGENTS rewrite; bump SW cache to v32 2026-09-09 04:46:20 +00:00
requirements-dev.txt Add live model picker, interactive terminal, and health checks 2026-07-22 16:12:48 +00:00
requirements.txt feat: FastAPI lifespan, banner parsing, web-push notifications 2026-09-09 05:32:31 +00:00
supervisor.py fix(supervisor): drop -x/-y from tmux new-window; size via resize-window 2026-10-02 08:17:26 +00:00

claude-code-pwa

CI

A self-hosted PWA dashboard that supervises Claude Code remote-control servers on a remote machine, from your phone.

You declare the folders you care about. The backend keeps exactly one healthy claude remote-control server alive per folder — restarting it on crash or reboot, backing off when it keeps failing, and raising an attention banner when a folder needs a human (a /login, a trust prompt). Those servers are what the Claude desktop/mobile app connects to, so starting a new session from your phone is a native Claude-app action; this PWA's job is to make sure there is always something for it to connect to.

  • Supervised folders — declare a folder once; the reconcile loop keeps its anchor server running (claude remote-control --permission-mode bypassPermissions --spawn same-dir --name <name>)
  • Restart / pause / resume / remove per folder, from the phone
  • Attention banner when a folder is needs_attention or unhealthy, with a one-tap terminal into that anchor
  • Adopt an already-running remote-control server discovered in the shared tmux session, turning it into a supervised folder
  • Other sessions — live, non-supervised Claude sessions found in the tmux session, with end / bulk-end
  • Interactive terminal per session or anchor — full xterm.js over WebSocket, attached to the tmux window via a PTY (grouped tmux session, so your desktop client keeps its own view). Includes a mobile key bar (esc / tab / ⇧tab / arrows / ^c / ⏎)
  • Uptime Kuma heartbeat that reports down when any folder is unhealthy or the reconcile loop stalls
  • Installable on iOS/Android home screen as a PWA

Pair with Tailscale or any other private network to reach your dev machine securely from anywhere.

Note: this used to be a session launcher (POST /api/sessions, history, dormant-session resume). Those paths were removed in the supervisor pivot. Sessions the PWA never saw are no longer replayed from history — only what is live in tmux, plus what you declare in supervised.json, is shown.

Architecture

┌─ phone (PWA) ──────────────────┐       ┌─ dev machine ──────────────────────┐
│                                 │       │                                    │
│  Installable PWA                │       │  FastAPI backend (this repo)       │
│  (HTML/CSS/JS, no framework)    │◄─────►│  ├─ /api/supervised* CRUD         │
│                                 │       │  └─ supervisor.py reconcile loop   │
└─────────────────────────────────┘       │     (one asyncio lock, 15 s tick)  │
                                          │                                    │
┌─ Claude mobile / desktop app ──┐        │  tmux session "claude"             │
│  connects to an anchor server  │◄──────►│  ├─ window: sup-ironpulse (anchor) │
│  and starts sessions in it     │        │  ├─ window: sup-nasdocker (anchor) │
└─────────────────────────────────┘       │  └─ window: <other live sessions>  │
                                          │                                    │
                                          │  anchor = claude remote-control    │
                                          │    --permission-mode bypassPermissions
                                          │    --spawn same-dir --name <name>  │
                                          └────────────────────────────────────┘

Requirements

  • Linux with systemd (or adapt for your init of choice)
  • Python 3.10+
  • tmux
  • Claude Code CLI 2.1.263 or newer, logged in — the supervisor uses the claude remote-control subcommand, not the older --remote-control flag
  • Optional: Tailscale or a reverse proxy if you want to reach it from outside your LAN

Install

git clone https://git.hiten-patel.co.uk/hiten/claude-code-pwa.git ~/dev/claude-code-pwa
cd ~/dev/claude-code-pwa
./scripts/install.sh

The install script will:

  1. Create a .venv/ inside the project and install deps
  2. Create ~/.claude-pwa/ for runtime data (config, state, logs)
  3. Generate a random bearer token (stored in ~/.claude-pwa/config.json)
  4. Generate PWA icons
  5. Install + start a systemd user service that binds to 0.0.0.0:8866
  6. Enable loginctl enable-linger so the service survives logout

Print the URL + token at the end. Open it on your phone, enter the token once, and tap Add to home screen in your browser.

Usage

  • Tap + → enter a folder (e.g. ~/dev/my-project), optional name and model → Add
  • The reconcile loop pre-seeds workspace trust, spawns the anchor, and watches its pane until it reports ready. The card goes starting → ok
  • Start sessions in that folder from the Claude app — the anchor is the server they connect to
  • Restart respawns the anchor. Pause stops it and leaves the folder declared. Remove stops it and drops the declaration
  • If a folder shows needs_attention (usually a first-time /login), the banner links straight to a terminal on that anchor — deal with it there once and the loop resumes
  • Other sessions lists live Claude sessions that aren't anchors; Adopt promotes one to a supervised folder

Status model

Status Meaning
starting anchor spawned, pane not yet reporting ready
ok ready marker seen and stable for ready_stable_seconds (default 30)
needs_attention pane wants a human (/login, trust prompt). Respawn is suspended until resolved
unhealthy window died; respawn scheduled under backoff (10s / 30s / 60s / 300s, 5 failures max)
paused enabled: false — the loop leaves it alone

Runtime data

All writable state lives at ~/.claude-pwa/:

~/.claude-pwa/
├── config.json           # bearer token(s), claude binary path, allowed_roots, kuma URL
├── supervised.json       # declared folders: [{id, cwd, name, model, enabled}]
├── supervisor_state.json # runtime per folder: status, window_id, restarts, backoff
├── state.json            # currently active session
├── claude_urls.json      # bridgeSessionId → claude.ai/code URL cache
└── logs/
    └── pwa.log           # rotated at 10 MB × 5 backups

This directory is intentionally outside the repo so the repo stays clean.

Both supervisor files are written atomically (temp file + os.replace). A malformed supervised.json is logged and the last good in-memory state is kept — the supervisor never collapses to an empty list, and never mass-kills anchors because a file got truncated. On first boot with no supervised.json, the supervisor seeds it from anchors already live in the tmux session.

Backup & restore

./scripts/backup.sh ~/backups/claude-pwa     # → ~/backups/claude-pwa/claude-pwa-YYYYMMDD-HHMMSS.tar.gz
./scripts/restore.sh ~/backups/claude-pwa/claude-pwa-20260421-091500.tar.gz [--force]

The backup excludes logs/ and prunes archives older than KEEP_DAYS (default 30). Cron-friendly.

Optional Uptime Kuma heartbeat

Add a kuma_push_url to config.json and the backend will curl it every 60s (configurable via kuma_push_interval). If unset, no heartbeat task runs.

The heartbeat is status-aware: it pushes down when any supervised folder is in a status other than ok/paused, and also when the reconcile loop itself has stalled (no tick for 3 × supervisor_interval), with the offending folder ids in the message. So Kuma alerts on "a folder needs a login" as well as on "the process died".

Security

  • The backend binds to 0.0.0.0 by default. Restrict access via firewall / Tailscale ACLs / reverse proxy — the bearer token is a second line of defense, not the first. For non-LAN deployments, see docs/reverse-proxy.md for Caddy and Tailscale Funnel recipes that terminate HTTPS in front of the PWA.
  • Anchors are spawned with --permission-mode bypassPermissions, and the supervisor pre-seeds hasTrustDialogAccepted in ~/.claude.json for each declared folder. Anyone who gains authenticated access to the PWA — or to the Claude account those anchors are logged into — can run anything on your machine. Treat the token like an SSH key.
  • Multi-token auth is supported: define auth_tokens in ~/.claude-pwa/config.json as a list of {"name": "...", "token": "..."} so each device has its own revocable token. Generate with scripts/add-token.sh <name>.
  • Folder allow-list: POST /api/supervised accepts only paths under the configured allowed_roots (default ["~"]). Paths are symlink-resolved before the check, so escapes outside the roots are rejected.

Hybrid delegation

Claude can remain the main orchestrator for every remote-control session while delegating bounded planner, reviewer, and implementer stages to subscription models or OpenRouter fallbacks. This uses Claude Code hooks and a local CLI; MCP is not required. See docs/hybrid-delegation.md for secure setup, concurrency, and rollback.

Development

See AGENTS.md for repository layout, conventions, and a quick orientation for humans or AI agents picking up the codebase.

Backlog and milestones live on Forgejo: https://git.hiten-patel.co.uk/hiten/claude-code-pwa/issues

License

MIT