Install it as systemd user services
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
- Install the commands
install.shchecks for:node,npm,git,tmux,cloudflared. A missing one printsmissing: <name>plus the Fedora and Debian package lines, and the run stops withinstall the packages above, then re-run. - Clone the repo into its final home.
installrefuses to run from a linked git worktree:<dir> is a linked git worktree; run this from the main checkout: <path>. - See what it would write first, without touching anything:
bash install.sh render --out /tmp/units. Compare the three files there. - Run
bash install.sh install. - Open
~/.config/v-code/envand fill inTUNNEL_TOKEN,VCODE_URLandVCODE_PEERS. The installer prints this as its last instruction. - Run
bash install.sh updateto re-render and restart with those values. - 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:
wrote /home/you/.config/v-code/env (0600), orenv file exists, left alone: <path>rendered <out>/v-code-tunnel.serviceand one line per other unitskipped v-code-tunnel: TUNNEL_TOKEN is empty in <env file>. Fill it in, then run: install.sh update— only while the token is emptyNext: fill TUNNEL_TOKEN, VCODE_URL and VCODE_PEERS in <env file>, then run: <path>/install.sh update- then the
statusoutput
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
- Linux with
systemd --useronly. There is no macOS or Windows installer. installandupdaterefuse to run from a linked git worktree.renderworks from one, because rendering points at whatever--dirsays.- A value containing
|,&,%, a backslash or whitespace is refused before anything is written:sedbuilds the units and systemd splitsExecStarton whitespace, and%is a systemd specifier that would expand again. - If your agent CLIs are not found, the installer warns
warning: neither claude, codex nor opencode found; the units get the system PATH onlyand continues. It looks atcommand -vafter sourcing nvm, plus~/.opencode/bin/opencodeexplicitly — the opencode installer adds that directory through~/.bashrc, which systemd never reads. install.shdoes not configure Cloudflare itself. See reach-it-from-a-phone.--instance devworks from a clone only. A release box stops with--instance dev is for a git clone; a release box runs the standard instance only. Any name other thandevstops withunknown --instance: <name> (the only one is dev).voiceandinstall --voicerefuse--instance dev:the dev instance has no voice unit; point VCODE_STT_URL in <env file> at a running sidecar instead.uninstallleaves the env file, the state directory, the voice venv and model, and the live engine files, and prints the path of each.
Related
- update-the-install — pulling a new version
- install-on-linux — the same units from a signed release, with no clone
- environment-variables — every variable, and where it comes from
- reach-it-from-a-phone — Tailscale, or the tunnel plus Access
- memory-limits-and-parking — the cgroup caps the unit sets
- logs-and-troubleshooting — journal, restarts, the rules
- install-the-voice-sidecar — the third unit
- machine-name — where
VCODE_NAMEandVCODE_PEERSshow up