Update an installed machine
bash install.sh update pulls the new code, reinstalls dependencies, migrates
the env file, re-renders the units and restarts them. One command per machine.
Why you would use it
A new version landed on the branch this machine tracks, and you want it running without remembering which of the six steps matter this time.
How to use it
- Run
bash install.sh updatefrom the main checkout, not from a worktree and not from inside a VCode thread. - Read the
statusblock it prints at the end. - If a phone has the app installed and the change touched the UI shell, confirm
VERSIONinpublic/sw.jswas bumped in the commit. Without a bump the phone keeps serving its cached shell whatever the server does.
What you see
In order: git pull --ff-only, npm ci --omit=dev, the env file line
(env file exists, left alone: …), a migrated … line only when the file
actually changed, one rendered … line per unit, then systemctl --user daemon-reload, enable --now, restart, and the status output with the
tunnel token redacted.
Extra lines you may see:
skipped v-code-tunnel: TUNNEL_TOKEN is empty in <env file><repo>/.env is no longer read by any unit; it can be removed once the tunnel unit has been restarted from <env file>warning: uv not found, skipped <repo>/voice/requirements.txt. The sidecar keeps the packages it has; run install.sh voice once uv is back
Options and settings
| Option | Default | What it changes |
|---|---|---|
--dir DIR |
the script's directory | Which checkout is pulled and pointed at |
--out DIR |
~/.config/systemd/user |
Where the re-rendered units land |
--env-file FILE |
~/.config/v-code/env |
The file that is migrated and read |
--state-dir DIR |
~/.local/state/v-code |
State and voice paths in the units |
--dry-run |
off | Print the steps, change nothing |
The rename to VCode
update on a clone, like install, first moves a box that still has the names
from before the rename (migrate_names). It runs after git pull and
npm ci, before anything writes the env file or a unit:
| Old | New |
|---|---|
~/.config/claude-portal/env |
~/.config/v-code/env, every WORKBENCH_* and PORTAL_* key under its VCODE_* name |
~/.local/state/claude-portal |
~/.local/state/v-code |
portal-workbench-prod.service, claude-portal-tunnel.service, portal-voice.service |
v-code.service, v-code-tunnel.service, v-code-voice.service; the old units are stopped, disabled and removed, and their drop-ins move to the new names |
~/.portal-shots |
~/.v-code-shots |
The dev instance moves ~/.config/claude-portal/dev.env,
~/.local/state/claude-portal-dev, portal-workbench-dev.service and
portal-tunnel-dev.service the same way. Each step prints one line, such as
moved <old> to <new>, keys under their VCODE_ names. A run with nothing left
to move prints nothing. When both the old and the new path exist, it stops
before changing anything: both <old> and <new> exist, so the rename to VCode stopped and nothing was changed. Keep the one this box uses, move the other out of the way, and run this again. Run it from a terminal outside VCode: it
refuses to run inside the unit it is about to stop.
voice, voice-browser and uninstall do not move anything. On a box that
still needs the move they stop with this box still has names from before the rename to VCode: run <repo>/install.sh install first.
The env file migration
migrate_env runs on every install and every update, and rewrites the env
file in place for a file written before the terminal portal was retired:
| Old | New |
|---|---|
WORKBENCH_URL=<url> |
PORTAL_URL=<url> (the old PORTAL_URL named the retired portal, and is dropped) |
PORTAL_PEERS=name|portalUrl|workbenchUrl |
PORTAL_PEERS=name|workbenchUrl |
PORT=<n> |
removed |
It is idempotent: a file already in the current shape is left byte for byte
alone and nothing is printed. When it does write, it writes through a temp file
at 0600 and a rename, because the file holds both tokens, and prints
migrated <path> to the one-product shape (PORTAL_URL is this server). The
rename step then moves PORTAL_URL and PORTAL_PEERS to VCODE_URL and
VCODE_PEERS.
update also re-runs the voice uv pip install, but only on a machine where
the venv and the model are already there — that is how a changed pin in
voice/requirements.txt reaches the sidecar.
Limits and known gaps
updaterefuses to run from a linked git worktree.git pull --ff-onlyfails on a dirty tree or a diverged branch, andset -estops the run there. Nothing has been rendered or restarted at that point.- Never run
updatefrom inside a VCode thread. The restart kills the agent turn that issued the command. Run it from a terminal or over ssh. - A merge that adds a
<script src>topublic/index.htmlis fatal until the unit restarts: static files are served from the repo directory, so disk serves the newindex.htmlwhile the running process still 404s the new route.updaterestarts, so this only bites when you merge without running it. - An installed phone keeps its cached shell until
VERSIONinpublic/sw.jschanges. The bump is what shows the update button (#update-ready) in the phone's top bar. See update-to-a-new-build. updatedoes not touch the voice model or the live engine files. Those are install-the-voice-sidecar and dictate-live.
Related
- install-as-systemd-units — the first install
- update-automatically — how a box installed from a release updates itself
- logs-and-troubleshooting — what to check when it comes back wrong
- environment-variables — the keys the migration rewrites