Install VCode on Linux with one command
curl -fsSL https://fezu.dev/install | sh
This command installs the newest signed release
of VCode on a Linux machine (x86_64 or ARM64), starts it as a systemd --user service, and
links it to your fezu.dev account. You need no git, node or npm. The command
from Add a workstation on fezu.dev/account adds a join code, so the box links
in one step, with no browser step. On a computer with a desktop session the
command also installs the VCode desktop app (desktop-app).
You use the box through the app at app.fezu.dev. A release box has no page of its own: opening its port in a browser shows a line that points to the app.
Why you would use it
You want VCode on a server or a spare machine and do not plan to work on its code. The release brings its own node and packages, and it keeps itself up to date (update-automatically). To run from a git clone instead, see install-as-systemd-units.
How to use it
The quickest way starts on fezu.dev/account: tap Add a workstation and copy the command it shows (add-a-workstation). It looks like this:
curl -fsSL https://fezu.dev/install | V_CODE_JOIN=<code> sh
Paste it on the machine, logged in as the user VCode should run as. The installer runs as below, but at the end it joins instead of opening the browser: the box links to the account that made the code, and the account page shows it in its list. Then open the box in the app at app.fezu.dev. The code works once, for 15 minutes; after that, make a new command.
Without a join code:
Log in to the machine as the user VCode should run as, over ssh or at the console. Not through
su:systemctl --usermust answer.Run:
curl -fsSL https://fezu.dev/install | shOn a machine with wget and no curl, such as a stock Ubuntu desktop:
wget -qO- https://fezu.dev/install | shThe installer then downloads with wget too.
To pick the port yourself:
curl -fsSL https://fezu.dev/install | sh -s -- --port 3500At the end VCode opens on its login card: in the desktop app the command has just installed, else in Chrome or Chromium, else your default browser. The card reads
Create an account or log in to connect <computer>. We send a code to your email.Type your email and click Continue, then enter the code the email brings. A new email makes a new account with 30 days of the full plan (free-trial); an email you have used logs in to your account. The computer is linked to that account, and VCode comes back signed in with the computer in it. On a server with no browser, open the address it prints on any device, log in there, and paste the code the page shows into the terminal. This isv-code box linkrunning on your terminal (link-this-box).Pair your phone:
v-code box pair(add-a-phone).Later, type
v-codein a terminal to open VCode. On a computer linked to your email it opens the desktop app signed in, or app.fezu.dev in the browser without the app. On a computer not linked it runsv-code box link, and on one still on an older trial account it runsv-code box trial, which gets no new start link now: open VCode on a device signed in to that account.
The installer has no screen of its own. It runs in the terminal only.
What you see
A first install prints a short summary. Each line starts with v-code:, in order:
v-code: installing <version>- on a computer with a desktop session, the desktop app's line:
VCode desktop app: installed <version>, or a reason it was skipped or failed (desktop-app). A failed app step never stops the install v-code: details in ~/.local/share/v-code/install.logv-code: running on port <port>- the install counts notice, unless you set
V_CODE_NO_ANALYTICS=1(install-counts) - with
V_CODE_JOIN, the join:v-code: linked to <email>andv-code: paired with <browser name>, which names the browser that showed the command on fezu.dev, thenOpened the VCode app.when the desktop app opened signed in - otherwise the link. With a terminal,
Opening VCode to connect this computer…andIf it doesn't open, go to <address> on any device, and paste the code it shows here:(on a box with no browser to open, onlyGo to <address> on any device, and paste the code it shows here:), and once connectedLinked to <email>, andPaired with <device>when the site names one. On a computer still on an older trial account, the trial's lines instead (free-trial). With no terminal:v-code: Link this box: v-code box link. On a box already linked:v-code: Already linked to <email>. On one linked to an email,v-code box appruns next:v-code: Opened the VCode app.when the desktop app opened signed in to that account. Without the app, or when the site refuses (v-code: Could not sign the app in: <reason>), the browser opens app.fezu.dev:v-code: If the browser doesn't open automatically, click this link: https://app.fezu.dev(desktop-app) v-code: Use it in the app: https://app.fezu.dev, when the step before gave no link: after a join, with no terminal, or after a link that failed
The steps go to ~/.local/share/v-code/install.log (0600) instead of the
terminal. Each run of install.sh install appends to it under a line
== <UTC time> install.sh install <version>. The steps are:
unpacked <version> into ~/.local/share/v-code/versions/<version>, from the download script, above that headerwrote ~/.config/v-code/env (0600), or on a re-runenv file exists, left alone: ~/.config/v-code/envinstalled the updater: ~/.local/share/v-code/bin/v-code-updatewrote ~/.local/bin/v-codeandwrote ~/.local/bin/portal- one
rendered …line per unit - what
systemctl --userprints while it enables and restarts the units, such asCreated symlink … skipped v-code-tunnel: TUNNEL_TOKEN is empty in <env file>. Fill it in, then run: install.sh install
When a systemctl --user call fails, the install stops with
systemctl --user <arguments> failed; see ~/.local/share/v-code/install.log,
and its message is in that file.
A link that fails prints v-code: The link did not complete. Try again with: v-code box link.
The box runs either way. A free account gets the site's own refusal; linking
needs premium.
A join that fails prints The join did not complete: <reason> and
Make a new command under Add a workstation on fezu.dev/account. The box is installed and runs either
way. The reasons you may see:
that code is wrong, used or expired; make a new command under Add a workstationthis box is linked to another account; unlink it firstthe site's answer does not match this join code
On a box already installed. The same command with V_CODE_JOIN only joins.
It prints v-code: already installed: <version>; joining, then the join lines.
Nothing is downloaded, so it works while dl.fezu.dev is down. No unit is
rendered or restarted, so agents running on the box carry on, even when a newer
release is out; the updater installs that one at its next hourly run. A box
linked to the same account keeps its link.
When the installed release is from before the join, the command installs the
newest release instead and joins at the end of that install. It prints
v-code: the installed release predates the join; installing <version>. That
install restarts the units, so a turn an agent is running on the box stops.
When no newer release is out, it stops with
v-code: this release does not support V_CODE_JOIN.
On a box installed before the rename to VCode. Releases from before
2026-09-26 run as portal-workbench-prod.service and read
~/.config/claude-portal/env. Their updater never moves such a box to a newer
release: it reads latest.json, which stays at the last of them. Run the
install command once. It installs the newest release as it would over any older
one, and that release's install.sh install moves the env file, the state, the
units and their drop-ins to the new names. The threads, the AUTH_TOKEN, the
box's link and its paired devices stay. From then on the box updates itself
again. With V_CODE_JOIN, the command installs the newest release first and
joins at the end of that install, printing
v-code: the installed release predates the rename to VCode; installing <version>.
When no newer release is out, the installed release joins as it is.
When the old updater died part way through a switch, the command stops before
it changes anything:
v-code: an update from before the rename to VCode has not finished: run ~/.local/share/v-code/bin/v-code-update --update first, then this again.
That runs the old updater, which settles its own switch. Then run the install
command again.
Run it from a terminal, not from a thread or the shell panel of the box it
moves: the move stops that unit. Inside it, the command stops before it
downloads anything:
v-code: this is running inside portal-workbench-prod.service, which the move to VCode stops part way through. Nothing was changed. Run the install command from a terminal outside VCode: ssh, or a desktop terminal.
After the move, the release from before the rename is not one to go back to by
hand. Its install.sh writes a fresh env file at the old path with a new
AUTH_TOKEN, and starts the old units on empty state.
Running the command again on a box that runs VCode. From a terminal outside
VCode, the command installs a newer release even while threads work: the
restart resumes the turns it cuts. When VCode answers as the installed version,
the command checks the switch the way the hourly update does. The new version
must report itself running within 30 s and still run 5 s later. A good switch
prints v-code: updated to <version>. A new version that fails goes back to
the version you had. The hourly update does not try it again, but the install
command installs it again when you run it:
v-code: <version> did not report itself running within 30 s; rolled back to <old version>, and <version> will not be tried againv-code: <version> came up but did not stay up; rolled back to <old version>, and <version> will not be tried againv-code: install.sh of <version> stopped (exit <n>); rolled back to <old version>, and <version> will not be tried again
A new version that never started is not held against it. The command switches
back, and you can run it again:
v-code: <old version> is still the version running: <version> was never started; switched back to <old version>; run the install command again to install <version>.
When VCode does not answer, the command installs as it would on a new box.
From a thread on the box itself, the command never restarts VCode: the restart
would stop the command part way. It installs nothing, and the link step and the
desktop app step run as usual:
v-code: <version> is out, but this run is inside v-code.service, whose restart would stop it: nothing was installed. To install it now, run the install command from a terminal outside VCode.
A run there that has to restart VCode stops before it downloads anything. That
is a run with --port, one after an install that did not finish, and one while
an update is still settling:
v-code: this is running inside v-code.service (a VCode thread), which the install would stop part way through. Nothing was changed. Run it from a terminal outside VCode: ssh, or a desktop terminal.
The shell panel runs outside VCode's own unit, so the command behaves there as in any other terminal. VCode restarts, and the shell panel stays open.
Other lines you may see:
warning: tmux not found, so the shell panel is off. Fedora: sudo dnf install tmux, Debian: sudo apt install tmuxnote: ~/.local/bin is not on PATH in this shell; a new login shell usually adds itwarning: Could not turn on linger, so VCode stops when you log out. Run: sudo loginctl enable-linger <user>. See Limits below.v-code: VCode did not answer on http://127.0.0.1:<port> within 60 s. See: journalctl --user -u v-codev-code: already installed: <version> (the newest release is <version>), thenv-code: to run its installer again: ~/.local/share/v-code/current/install.sh install. It then runs the link step again: a computer on an older trial account runsv-code box trial, which gets no new start link now, one with no link opens VCode's login card, and one linked to an email saysv-code: Already linked to <email>, then opens the desktop app signed in to that account (v-code: Opened the VCode app.). Without the app it opens app.fezu.dev in the browser. A re-run never installs an older or equal version. A re-run that installs a newer one keeps the release it replaced and removes any older one, except the one VCode runs; when VCode does not answer it removes none:v-code: could not ask VCode which version it runs, so no old release is removed this run. A re-run after an update that died part way settles that update first, as the updater would.
The checks that stop the install before anything is written:
| Check | Message |
|---|---|
V_CODE_JOIN with --update |
V_CODE_JOIN is for an install; --update never links |
V_CODE_JOIN is 34 letters, digits, - and _ |
V_CODE_JOIN is not a join code; copy the command again |
| Linux or macOS (a Mac takes the path in install-on-macos) | VCode installs on Linux and macOS only; this machine runs <os> |
| x86_64 or aarch64 | VCode installs on x86_64 and aarch64 only; this machine is <arch> |
tar, gzip, ssh-keygen, bash, head, find, readlink, flock |
<tool> is missing; install it and run this again |
curl or wget |
curl and wget are both missing; install either one and run this again |
| GNU tar | VCode needs GNU tar; this tar is: <first line of tar --version> |
| OpenSSH 8.1 or newer | ssh-keygen has no -Y verify: VCode needs OpenSSH 8.1 or newer |
sha256sum or shasum |
sha256sum and shasum are both missing; install coreutils and run this again |
systemctl |
systemctl is missing: VCode runs as systemd --user units |
| a user manager | systemctl --user does not answer: log in as this user over ssh or a console (not su) and run this again |
And the checks on the release itself, which install nothing when they fail:
signature check failed for releases/<version>/manifest.json: <ssh-keygen's message>download does not match the signed manifest: sha256 <got>, signed <signed>download is longer than the signed manifest's <size> bytesstable.json names <a> but its signed manifest is for <b>another install or update is running; try again when it has finished
With V_CODE_JOIN, a release whose install.sh features does not list join
stops the run with v-code: this release does not support V_CODE_JOIN. The
release is left unpacked under versions/, but current does not move and its
installer does not run; run the plain command, or wait for a newer release.
Options and settings
| Option | Default | What it changes |
|---|---|---|
--port N |
first free port from 3445 to 3499, skipping 3446 | VCODE_PORT in a new env file. An existing env file keeps its port |
--base URL |
https://dl.fezu.dev |
Where releases are read from. For tests |
V_CODE_JOIN=<code> |
unset | An environment variable, not an option: the join code from Add a workstation on fezu.dev/account. The installer joins with it instead of opening the browser. It is never written to the env file |
V_CODE_NO_ANALYTICS=1 |
unset | Sends no install counts. See install-counts |
V_CODE_APP=no |
unset | Skips the desktop app (desktop-app) |
V_CODE_APP=only |
unset | Installs only the desktop app, no box; with V_CODE_START=<code> it opens signed in (desktop-app) |
The port pick binds each port on 127.0.0.1 with the release's node. 3446 is
skipped because it belongs to the voice sidecar, running or not. With no port
free the install stops: no free port from 3445 to 3499; pass --port N.
Where everything lives:
~/.local/share/v-code/
versions/<v>/ one unpacked release (at most the current one and the last good one)
current -> versions/<v> the units run from here
bin/v-code-update the updater the hourly timer runs
update-state/ installed, pending, rejected, agent-dirs
app/<v>/, app/current the desktop app, when installed
install.log what each install did, step by step (0600)
~/.local/bin/v-code runs current/node/bin/node current/bin/v-code
~/.local/bin/portal the same, under the name from before the rename
~/.config/v-code/env AUTH_TOKEN, VCODE_PORT and the rest (0600)
~/.local/state/v-code/ threads, the box's key, voice files
Commands on an installed box:
| Command | What it does |
|---|---|
~/.local/share/v-code/current/install.sh install |
Re-renders the units, restarts them, prints the URL and links again. Run it after you install an agent |
~/.local/share/v-code/current/install.sh join |
Waits for VCode, then joins with V_CODE_JOIN from the environment. Renders and restarts nothing. Without the variable: install.sh join needs V_CODE_JOIN: run the command from Add a workstation on fezu.dev/account |
~/.local/share/v-code/current/install.sh link |
The link step alone, as at the end of an install: v-code box link on a computer not linked (or removed from its account), v-code box trial on an older trial account, or v-code: Already linked to <email>. Renders and restarts nothing |
~/.local/share/v-code/current/install.sh features |
Prints what this release supports, one word per line: join, v-code-names, channel and link |
~/.local/share/v-code/current/install.sh update |
Runs v-code-update --update now. See update-automatically |
~/.local/share/v-code/current/install.sh status |
systemctl status over VCode, the tunnel and v-code-update.timer |
~/.local/share/v-code/current/install.sh uninstall |
Disables and removes the units, then removes ~/.local/bin/v-code, ~/.local/bin/portal, current, .lock, bin/, versions/, update-state/, and any .stage.* or current.new.* a killed run left, and the desktop app with its menu and autostart entries. The env file and the state stay; a desktop app's AppArmor rule stays too, and the command prints how to remove it. While an update runs it stops: an update is running; try again when it has finished |
Limits and known gaps
- Linux on x86_64 or ARM64 (aarch64) with
systemd --useronly. On ARM64 the desktop app is not built yet: the install skips it withVCode desktop app: not available on Linux ARM64 yet; skipped. The same command installs on a Mac, with launchd instead of systemd (install-on-macos). On Windows, see install-on-windows: Windows has its own installer. - Linger. Without it the user manager, and VCode with it, stops when you
log out. The installer runs
loginctl enable-linger <user>. logind asks polkit before it lets a normal user change that, so on a minimal server or a container without polkit it is refused. The install carries on and prints thesudo loginctl enable-linger <user>line; run it once. - The installer does not install agents, and does not say whether it found
one. Install one from the app (install-an-agent),
or install
claude,codex,opencodeorcursor-agentyourself, then run~/.local/share/v-code/current/install.sh installso the unit'sPATHfinds it. It looks onPATH, beside nvm's node, in~/.opencode/bin, and in~/.local/bin, where claude's own installer putsclaude. install.shfrom a release refuses to run unlesscurrentpoints at that release:install through get-v-code.sh: current does not point at this release (<dir>).--portapplies only when the env file is new. To move an installed box, editVCODE_PORTin the env file and runinstall.sh install.cloudflaredis needed only when you setTUNNEL_TOKEN.
Related
- update-automatically: how the box keeps itself up to date
- add-a-workstation: the command with the join code
- free-trial: the 30 days a new account gets
- link-this-box: the link the installer runs at the end
- add-a-phone: the next step after linking
- install-as-systemd-units: the same units from a git clone
- install-on-windows: the same on Windows
- install-on-macos: the same command on a Mac, with launchd
- environment-variables: what the env file holds
- logs-and-troubleshooting: the journal
- install-counts: what the installer reports, and how to turn it off
- desktop-app: the window the command installs on a desktop