VCode home

Install it as systemd user services

Agents: claude, codex, opencode · On: desktop

bash install.sh install puts VCode on a machine as a systemd --user service, writes its env file with a fresh login token, and starts it. A second unit runs the Cloudflare tunnel.

Why you would use it

You want VCode up after a reboot, and reachable from your phone. Running npm start in a terminal ends when the terminal does, and it holds no token.

How to use it

  1. Install the commands install.sh checks for: node, npm, git, tmux, cloudflared. A missing one prints missing: <name> plus the Fedora and Debian package lines, and the run stops with install the packages above, then re-run.
  2. Clone the repo into its final home. install refuses to run from a linked git worktree: <dir> is a linked git worktree; run this from the main checkout: <path>.
  3. See what it would write first, without touching anything: bash install.sh render --out /tmp/units. Compare the three files there.
  4. Run bash install.sh install.
  5. Open ~/.config/v-code/env and fill in TUNNEL_TOKEN, VCODE_URL and VCODE_PEERS. The installer prints this as its last instruction.
  6. Run bash install.sh update to re-render and restart with those values.
  7. Check it: bash install.sh status.

Add --dry-run to any command to print the steps without running them.

What you see

install prints a line per step, in this order:

status runs systemctl --user --no-pager status over the units and pipes it through a redaction, so the cloudflared command line reads --token *** and never the real value.

Options and settings

Option Default What it changes
install.sh install — deps, npm ci --omit=dev, the move from the names before the rename (update-the-install), env file, units, enable --now, linger
install.sh update — see update-the-install
install.sh render — render only. Requires --out, side-effect free
install.sh status — systemctl status over the installed units, token redacted
install.sh uninstall — disable and remove the units; env file, state and voice files stay
install.sh voice / voice-browser — dictation: install-the-voice-sidecar and dictate-live
--dir DIR the script's own directory Which checkout the units point at
--out DIR ~/.config/systemd/user Where the unit files are written. render has no default and demands it
--env-file FILE ~/.config/v-code/env The file every unit reads through EnvironmentFile=
--state-dir DIR ~/.local/state/v-code Where thread state and voice files live
--node BIN resolved Absolute path to the node binary baked into ExecStart
--voice off Provision the dictation sidecar during install
--threads N 4 CPU threads the voice sidecar decodes on
--instance dev the standard instance Install this clone as a second VCode beside the standard one. See A dev instance beside the standard one
--dry-run off Print each step instead of running it

What the generated env file holds

Written only when absent, at mode 0600, because it carries both tokens.

AUTH_TOKEN=<openssl rand -hex 32>
TUNNEL_TOKEN=
VCODE_PORT=3445
VCODE_NAME=<hostname -s>
VCODE_URL=
VCODE_PEERS=

An existing file is left alone but forced back to 0600. A machine upgrading from an older layout keeps what it had: AUTH_TOKEN, TUNNEL_TOKEN, the server's URL and the peer list are copied from <repo>/.env, or the token alone from ~/.claude-portal-workbench-prod/env, rather than minting a new token every phone would have to be re-paired with.

The three units

Unit Template When it runs
v-code.service systemd/v-code.service.in Always
v-code-tunnel.service systemd/v-code-tunnel.service.in Only once TUNNEL_TOKEN has a value
v-code-voice.service systemd/v-code-voice.service.in Only once install.sh voice has built the venv and the model

All three are rendered every time. Only the active ones are enabled and started. The tunnel is held back because cloudflared with an empty token fails instantly, and Restart=always plus StartLimitIntervalSec=0 would retry that forever.

A dev instance beside the standard one

Add --instance dev to install, update, status, render, uninstall or voice-browser to run this clone as a second VCode on a machine that already has one, such as a release installed with curl -fsSL https://v-code.dev/install | sh.

Standard --instance dev
VCode unit v-code.service v-code-dev.service
Tunnel unit v-code-tunnel.service v-code-tunnel-dev.service
Voice and update units yes none
Env file ~/.config/v-code/env ~/.config/v-code/dev.env
State directory ~/.local/state/v-code ~/.local/state/v-code-dev
Shell panel tmux session v-code-shell-prod v-code-shell-dev

The first dev install writes dev.env with a fresh AUTH_TOKEN and the first free port from 3445, so it does not collide with the standard instance. Every systemctl call names only the dev units, and the printed hints read install.sh update --instance dev. --env-file and --state-dir still override the dev defaults.

The dev instance has no voice unit. For dictation, set VCODE_STT_URL in dev.env to the standard instance's sidecar, http://127.0.0.1:3446.

Limits and known gaps