{"slug": "show-hn-remotevibe-remote-coding-sessions-on-your-own-vps-from-your-phone", "title": "Show HN: Remotevibe – remote coding sessions on your own VPS from your phone", "summary": "Remotevibe, a new open-source tool by developer Alessandro Diaferia, lets iPhone users start remote coding sessions on their own VPS from a phone, using Claude Code with Remote Control. The tool, which requires a Claude Pro or Max subscription and a GitHub PAT, clones a repo into a Docker container on the user's hardware and registers the session with the Claude app, with no Anthropic API key needed. Sessions are long-lived and survive daemon or container restarts, and the project is available on GitHub.", "body_md": "Start a coding session on your own hardware from your phone.\n\n## remotevibe-demo.mp4\n\nOpen a small web app on your iPhone, pick one of your GitHub repos, tap start. A container on your VPS clones the repo and launches Claude Code with Remote Control enabled. Switch to the Claude app — the session is there, in your repo, on your machine, ready to work.\n\n```\niPhone ──tailnet──▶ remotevibed ──docker run──▶ container\n (PWA)                (Go, VPS)                  ├─ git clone <your repo>\n                                                 └─ tmux: claude --remote-control\n                                                              │\niPhone ◀───────────── Claude app ◀───── registers ────────────┘\n```\n\nNo Anthropic API key: the agent signs in to your Claude subscription the same\nway it does on your laptop. That sign-in is interactive and occasional — see\n[Authentication](#authentication), which is the part of this project worth\nreading carefully.\n\nSessions are long-lived: they survive the daemon restarting, keep their\ncheckout and their conversation across a container restart, and show up as\n*error* with a reason — not a phantom \"running\" — when something goes wrong.\n\n- A machine on your tailnet running Docker and Tailscale — a small VPS is fine\n- A Claude subscription (Pro or Max)\n- A fine-grained GitHub PAT\n\nNothing else: the daemon builds inside Docker, so the host needs no Go toolchain.\n\nEverything below happens on the machine that will run the sessions. Put the\ncheckout wherever you keep services — `/opt/remotevibe`\n\nis a reasonable\ndefault.\n\n**1. Configure**\n\n```\ngit clone https://github.com/alediaferia/remotevibe /opt/remotevibe\ncd /opt/remotevibe && cp .env.example .env && chmod 600 .env\n```\n\nEdit `.env`\n\n: set `RV_GITHUB_TOKEN`\n\n, and set `RV_STATE_DIR`\n\nto a directory you\nown (`/opt/remotevibe/state`\n\n). Leave `RV_AUTH_MODE=seeded`\n\n. Consider\n`RV_SESSION_PREFIX=vps`\n\n, which is what makes sessions read as `vps/my-repo`\n\nin\nthe phone app instead of blurring together with the ones on your laptop, and\n`RV_CPUS`\n\n/ `RV_MEMORY`\n\n, so one session cannot take the box down.\n\nEach `make`\n\ntarget is a one-line wrapper — `make image`\n\nis `docker build`\n\n,\n`make auth`\n\nis `./scripts/bootstrap-auth.sh`\n\n, `make up`\n\nis\n`docker compose up -d --build`\n\n— so a host without `make`\n\ncan run the commands\ndirectly.\n\n**2. Sign in once**\n\n```\nmake image     # the session container\nmake auth      # opens Claude in a container; use /login\nmake verify    # look for the session in the Claude app on your phone\nmake verify    # run it twice — a fresh profile must not ask you to log in\n```\n\nThe second `make verify`\n\nis the one that matters: it starts from a blank\nprofile, exactly like a session your phone kicks off for a repo you have never\nopened. If it reaches the session without prompting, the flow works. See\n[Authentication](#authentication) for why this dance exists.\n\n**3. Start the daemon**\n\n```\nmake up        # docker compose up -d --build\nmake logs      # follow it\n```\n\nYour user needs to be in the `docker`\n\ngroup: the daemon runs in a container\nwith the Docker socket mounted, so it can start sessions as sibling containers.\n`RV_STATE_DIR`\n\nis mounted at the same path inside the container as outside —\nthe daemon passes that path to the host's Docker when seeding a session, so it\nhas to mean the same thing on both sides.\n\n**4. Put it on your tailnet**\n\n```\ntailscale serve --bg 8787\ntailscale serve status     # the https URL to open on your phone\n```\n\nIf that host already serves something on 443, put remotevibe on another HTTPS\nport instead — `tailscale serve --bg --https=8443 8787`\n\n. Tailscale allows 443,\n8443 and 10000. Do not use `--set-path`\n\n: the PWA fetches from absolute paths\nand will not work under a subpath.\n\nOpen that URL in Safari on the iPhone and add it to the home screen — it is a\nPWA, so it gets its own icon and no browser chrome. The daemon publishes only\nto loopback; `tailscale serve`\n\nis what makes it reachable, and only from your\ntailnet.\n\n`make build && make run`\n\nruns the daemon straight from the checkout, which is\nthe convenient shape for hacking on remotevibe itself.\n\nFor a host-native service instead of a container, `deploy/`\n\nhas a systemd unit\nand `deploy/install.sh`\n\n, which installs to `/usr/local/bin`\n\nwith an\n`/etc/remotevibe.env`\n\nand a dedicated service user. Compose is the path that\ngets exercised, so prefer it unless you have a reason not to; with the systemd\nroute, remember that the interactive `make auth`\n\nhas to run as the service user,\nsince it writes the profile that user reads.\n\nOne fine-grained PAT does all three jobs: listing your repos in the UI, cloning\ninside the container, and pushing the agent's commits back. It needs\n**Contents: read & write** and **Metadata: read** on the repositories you care\nabout. There is no OAuth device flow, on purpose — this is a single-user tool.\n\nThe agent needs your Claude credentials inside the container, and Anthropic's\nsign-in is interactive. A session you start from your phone has nobody at a\nkeyboard to answer that prompt, so the whole question is how an\n**already-authenticated profile** reaches a brand new container.\n\nOne thing to know before choosing a mode: a login has two halves. The\ncredentials live in `.credentials.json`\n\n, but the account record — which\nsubscription, which user — lives in a *separate* file, `.claude.json`\n\n, that\nsits outside the credentials directory. Persist only the first and the agent\nasks you to sign in again, credentials or not. The image sets\n`CLAUDE_CONFIG_DIR`\n\nso both halves land in one directory, which is what makes\nany of this survive a restart.\n\n`make auth`\n\nsigns you in once and leaves the profile on the host in\n`$RV_STATE_DIR/agent-home`\n\n. Every new session mounts that directory read-only\nand **copies** it into its own volume on first start.\n\n- one sign-in, ever; new repos start silently, which is what the phone flow needs\n- sessions keep their own credentials, conversation and project state\n- if a token refresh in a session makes the canonical copy stale, re-run\n`make auth`\n\nEvery session mounts that same profile directly instead of copying it.\n\n- nothing to re-seed, and a refresh in one session benefits all of them\n- but concurrent sessions write to one config file and one session history\n\n`claude setup-token`\n\nlooks like the obvious fit — a long-lived credential, no\nfiles to sync — and it was the original default here. It does not work: the\ntoken carries no account record, and a Remote Control session stops and asks\nfor a full browser sign-in with different scopes regardless. Setting\n`RV_AUTH_MODE=token`\n\nnow fails at startup with that explanation rather than\nwasting your time.\n\nOpen the session's **Logs** — the agent pane is included, and it shows exactly\nwhat the session is sitting on. The two answers you are most likely to see:\n\n*a sign-in prompt*— the profile did not reach the container; re-run`make auth`\n\nand check`$RV_STATE_DIR/agent-home`\n\nholds both`.credentials.json`\n\nand`.claude.json`\n\n*a dialog awaiting a keypress*— one the pre-seeded profile did not cover. You can unstick it by hand:\n\n```\ndocker exec -it rv-<owner>-<repo> tmux attach -t agent\n```\n\n`make auth`\n\nruns in the same permission mode your sessions use, so any warning\nyou accept during it is recorded in the profile they are seeded from. Accepting\none inside a throwaway `make verify`\n\ncontainer does **not** carry over — that\ncontainer is deleted, and with it the copy of the profile you just changed.\n\nSessions fail with an auth error in the phone app when the credential lapses,\nand a session that stalls on the sign-in prompt shows up as *error* with that\nreason rather than as a session your phone will never find. Re-run `make auth`\n\n,\nthen stop and start the affected sessions. `GET /healthz`\n\nreports the\nconfigured mode.\n\n**Docker is the registry.** Every session container carries `rv.*`\n\nlabels\n(repo, branch, agent, name, created). The daemon lists sessions by querying\nDocker, so restarting it — or rebooting the VPS — never loses track of\nanything, and there is no state file to drift out of sync.\n\n**Three lifetimes, kept separate.** The *container* is the per-repo sandbox and\nrestarts with `unless-stopped`\n\n. *tmux* is the durable TTY inside it, so the\nagent survives anything happening to the daemon. *Remote Control* is the link\nto your phone, and it is the only one of the three that talks to Anthropic.\n\n**Starting is idempotent.** Session id, container name and volumes all derive\nfrom the repo slug, so tapping a repo twice reattaches instead of spawning a\nduplicate. (The slug flattens punctuation, so `owner/my.repo`\n\nand\n`owner/my-repo`\n\nwould collide — rename one if you own both.)\n\n**Sessions survive restarts.** Each session gets two volumes: the checkout at\n`/workspace`\n\n, and the agent's profile at `CLAUDE_CONFIG_DIR`\n\n— credentials,\naccount record, conversation and all. The second one is what makes a reboot\nresume the session instead of quietly starting a blank one under the same name.\nStopping a session keeps both, so restarting it picks up the same branch, the\nsame uncommitted work and the same context; pass `?purge=1`\n\nto `DELETE`\n\nto\nthrow all of it away.\n\n**A new session never asks you to log in.** On first start the entrypoint\ncopies the canonical profile into the session's own volume, so a repo you have\nnever opened starts already authenticated.\n\n**Readiness is real.** The container's `HEALTHCHECK`\n\nreports healthy only while\nthe agent process is actually alive in tmux, which is what the phone UI shows\nas *starting → running*. It is a liveness check, not a one-time flag: if the\nagent dies, the session goes back to *error* rather than lying about being\nattachable.\n\n**Disk is visible, because keeping volumes is the default.** Every session card\nshows what its checkout and profile occupy. A *Storage* panel adds the totals\nand — the part the session list structurally cannot show — volumes whose\ncontainer is gone, left behind by a stop without a purge. Stopping a session\noffers both shapes: **Stop** keeps the checkout so the session resumes, **Stop\n& delete** reclaims it. Sizes come from one cached `docker system df -v`\n\nthat\nrefreshes off the request path, so a slow scan never stalls the session list.\n\n**A prompt is not progress.** The agent starts in an interactive TTY, and\nanything that stops for a keypress — sign-in, workspace trust, the\nbypass-permissions warning — looks exactly like a healthy process to anything\nwatching from outside. The entrypoint therefore classifies startup from the\npane and records one of three outcomes: registered, waiting for a sign-in, or\nnever confirmed. The last two are reported as errors with the fix in the\nmessage, and the profile is pre-seeded to skip the known dialogs. If a session\nis rescued by hand, the supervisor notices and it goes back to *running*.\n\n**Logs show the part that matters.** The agent runs in a detached tmux pane, so\nnothing it prints reaches `docker logs`\n\n— including the auth error you are\nlooking for. `/api/sessions/{id}/logs`\n\nreturns the container startup output\n*and* a capture of the agent pane.\n\n**The agent layer is pluggable.** A driver contributes environment variable\nnames and one command line; the entrypoint runs whatever `RV_AGENT_CMD`\n\nit is\nhanded. See [docs/codex.md](/alediaferia/remotevibe/blob/main/docs/codex.md) for what a Codex driver would need,\nand why one is not shipped.\n\nThe default is `--permission-mode bypassPermissions`\n\n. You cannot tap \"approve\"\non every tool call from a phone, so the container is the security boundary\ninstead of the prompt: the agent can run anything it likes inside its own\ncontainer, with a GitHub token that can push to your repositories.\n\nThat is the actual trade. If you would rather keep approvals, set\n`RV_PERMISSION_MODE=acceptEdits`\n\nand answer prompts in the app. Scope the PAT\nto the repositories you actually hack on from your phone, not to everything.\n\nThe container runs as an unprivileged user by default, but that user has\npasswordless sudo — installing a missing build dependency from your phone is\notherwise impossible — so with `bypassPermissions`\n\nthe agent can become root\n*inside its own container* whenever it wants. What it does not get is the\nDocker socket, `--privileged`\n\n, or anything of the host: the boundary is the\ncontainer, not the user inside it. CPU and memory caps come from `RV_CPUS`\n\nand\n`RV_MEMORY`\n\n.\n\n**The tailnet is the perimeter.** The daemon binds to localhost and is published by`tailscale serve`\n\n. There is no login screen because a single-user, tailnet-only service gains nothing from one — do not expose the port publicly.**Secrets travel by name.** The daemon forwards`RV_GITHUB_TOKEN`\n\nwith`docker run -e NAME`\n\n, inheriting the value from its own environment, so it never appears in the process table. It*is*visible in`docker inspect`\n\noutput for the running container, as environment variables always are. The Claude credential never travels as an environment variable at all — it is a file in the session's profile volume.**Nothing is baked into the image.** Tokens live in`.env`\n\n(gitignored) or`/etc/remotevibe.env`\n\n(0600).\n\n| Method | Path | |\n|---|---|---|\n`GET` |\n`/api/repos?q=&refresh=1` |\nrepos visible to the token, recent first |\n`GET` |\n`/api/agents` |\ndrivers and whether each is usable |\n`GET` |\n`/api/sessions` |\nlive sessions, derived from Docker |\n`POST` |\n`/api/sessions` |\n`{\"repo\":\"owner/name\",\"branch\":\"main\",\"agent\":\"claude\"}` |\n`DELETE` |\n`/api/sessions/{id}?purge=1` |\nstop; `purge` also drops the volumes |\n`GET` |\n`/api/storage` |\ntotal and reclaimable bytes, plus orphaned volumes |\n`DELETE` |\n`/api/volumes/{name}` |\ndelete one orphaned `rv-ws-*` / `rv-home-*` volume |\n`GET` |\n`/api/sessions/{id}/logs?tail=200` |\nstartup log + agent tmux pane, text/plain |\n`GET` |\n`/healthz` |\ndocker reachable, image present, auth mode |\n\nThe PWA is a launcher, not a terminal — the real work happens in the Claude app. When you do need the box itself:\n\n```\ndocker exec -it rv-owner-repo tmux attach -t agent   # watch or drive the agent\ndocker exec -it rv-owner-repo bash                   # a plain shell\ndocker logs -f rv-owner-repo                         # clone + startup output\n```\n\nIf the agent exits, its tmux pane drops back to a shell and the container stays up, so you can relaunch it by hand instead of losing the checkout.\n\n```\ncmd/remotevibed      daemon entrypoint\ninternal/config      environment-driven configuration\ninternal/dockerx     container lifecycle via the docker CLI\ninternal/ghclient    repository listing\ninternal/agent       agent drivers (claude, codex stub)\ninternal/httpapi     JSON API + static serving\nweb/                 the PWA, embedded into the binary\nimage/               session container: Dockerfile, entrypoint, healthcheck\ndeploy/              systemd unit + installer\nscripts/             auth bootstrap, remote-control smoke test\n```\n\nThe container image is meant to be forked: add your toolchains to\n`image/Dockerfile`\n\nso the repos you clone are actually buildable from a phone.\n\nMIT", "url": "https://wpnews.pro/news/show-hn-remotevibe-remote-coding-sessions-on-your-own-vps-from-your-phone", "canonical_source": "https://github.com/alediaferia/remotevibe", "published_at": "2026-08-27 11:16:53+00:00", "updated_at": "2026-08-27 11:48:52.641064+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-agents"], "entities": ["Remotevibe", "Alessandro Diaferia", "Claude Code", "Anthropic", "GitHub", "Docker", "Tailscale", "iPhone"], "alternates": {"html": "https://wpnews.pro/news/show-hn-remotevibe-remote-coding-sessions-on-your-own-vps-from-your-phone", "markdown": "https://wpnews.pro/news/show-hn-remotevibe-remote-coding-sessions-on-your-own-vps-from-your-phone.md", "text": "https://wpnews.pro/news/show-hn-remotevibe-remote-coding-sessions-on-your-own-vps-from-your-phone.txt", "jsonld": "https://wpnews.pro/news/show-hn-remotevibe-remote-coding-sessions-on-your-own-vps-from-your-phone.jsonld"}}