Updating & Uninstalling
Updating
Update to the latest version with a single command:
subnaut update
This pulls the latest code from main, updates dependencies, and prompts you to configure any new options that were added since your last update.
subnaut update automatically detects new configuration options and prompts you to add them. If you skipped that prompt, you can manually run subnaut config check to see missing options, then subnaut config migrate to interactively add them.
Passive update notices
Pinned or noninteractive installations can disable passive CLI version and banner update checks:
subnaut config set updates.check false
This suppresses both cached update notices and passive update-check network requests. The default is true. Explicit subnaut update --check and subnaut update still work; this setting does not control the Desktop application's updater.
What happens during an update
When you run subnaut update, the following steps occur:
- Pre-update snapshot — a lightweight state snapshot is saved by default (covers pairing data, cron jobs,
config.yaml,.env,auth.json, and other state files that get modified at runtime; individual files over 1 GiB are skipped so a large sessions DB never slows the update down). Because the code swap and gateway restarts touch every profile, the same snapshot is taken for every profile on the install — each into its ownstate-snapshots/directory — and the post-update cron-jobs safety net checks each profile against its own snapshot. Controlled byupdates.pre_update_backup(quickby default,fullfor a zip of all ofSUBNAUT_HOME,offto disable). Recoverable via the snapshot restore flow described under Snapshots and rollback. Quick snapshots are file-loss recovery, not code-rollback insurance — for a coherent point-in-time rollback use--backup(full mode). - Git pull — pulls the latest code from the
mainbranch and updates submodules - Post-pull syntax validation + auto-rollback — after the pull, Subnaut compiles the nine critical files every
subnautinvocation imports at startup. If any fails to parse (e.g. an orphan merge-conflict marker, an accidentally truncated file), Subnaut runsgit reset --hard <pre-pull-sha>to roll the install back so your shell stays bootable. Re-runsubnaut updateonce the upstream fix lands. - Dependency install — runs
uv pip install -e ".[all]"to pick up new or changed dependencies - Config migration — detects new config options added since your version and prompts you to set them
- Desktop rebuild (stage-and-swap) — if the Subnaut Desktop app was built from this checkout, it is rebuilt so the GUI matches the new code. The rebuild packs into a temporary staging directory next to
apps/desktop/release/, verifies the staged app, and only then renames it over the previous build. A rebuild that fails at any point — corrupt Electron download, missing dependency, disk full — leaves the previous app untouched and launchable; the update reports⚠ Update partially completeandsubnaut desktopretries the rebuild. - Gateway auto-restart — running gateways are refreshed after the update completes so the new code takes effect immediately. Service-managed gateways (systemd on Linux, launchd on macOS) are restarted through the service manager. Manual gateways are relaunched automatically when Subnaut can map the running PID back to a profile. Manually-launched
subnaut servebackends (for example a network-bound serve powering a remote Desktop) are handled the same way: each backend records its bind address in the install's spawn ledger at startup, so the update stops it before the code swap and relaunches it afterward on the same host and port — a remote Desktop pointed at that endpoint reconnects instead of stranding. Backends owned by a running Desktop app are left to the app's own respawn.
Updating from a release that had the browser dashboard
The browser dashboard was removed; Subnaut Desktop is the graphical interface. The first subnaut update from a release that still had it runs that release's updater, which asks npm for the removed web workspace. That one run reports an npm error and a partially complete update. Run subnaut update once more: the current updater repairs the Node dependencies.
Missing Windows updater files
If the maintained updater script is missing (for example after antivirus quarantine), the legacy update forwarder fails instead of reporting a successful hand-off. Repair the installation and review the security software's quarantine report before retrying; do not disable antivirus protection. Before reporting success, the maintained updater checks the CLI import, Windows executable header, ASAR header and packaged main entry, readable renderer HTML with a local module entry, initial module files, and current build stamp. These are minimum artifact checks, not a full dependency audit or an application/backend launch test. Missing Python is reported before waiting for Desktop shutdown; dependency repair is still allowed to run as part of the update. Electron checks maintained handoff prerequisites before stopping backends when that layout is present; genuine legacy-flat updater layouts remain supported, so not every missing updater file is detected before backend shutdown.
On Windows, a Desktop reopened during packaging is stopped again immediately before the staged build is promoted. This cleanup is restricted to executables inside that checkout's Desktop release tree; unrelated installations are not stopped. A remaining lock still makes staged promotion fail rather than bypassing the rename error.
Updating against a non-default branch: --branch
By default subnaut update tracks origin/main. Pass --branch <name> to update against a different branch — useful for QA channels, feature branches, or release-candidate testing:
subnaut update --branch release-candidate
subnaut update --check --branch experimental # preview behindness only
If your local checkout is on a different branch, Subnaut auto-stashes any uncommitted work, switches HEAD to the target branch, and then pulls. Branches that don't exist locally are auto-tracked from origin/<name> (git checkout -B <name> origin/<name>). Branches that don't exist anywhere fail cleanly — your stashed changes are restored before exit so you're never stranded in a weird state. The main-only fork-upstream sync logic is automatically skipped on non-main branches.
Checkout parked on a feature branch
If the source checkout was left sitting on a feature branch (by tooling, a worktree experiment, or a manual checkout), subnaut update switches it back to the update target automatically whenever the working tree is clean:
- Branch fully merged (every commit already contained in
origin/main—git cherryreports nothing unmerged): the update says so —Checkout was parked on '<branch>' (fully merged) — switched back to main— and stays onmainafterwards. - Branch has unmerged commits but the tree is clean: the update still switches to
mainso the update can proceed — this is what non-interactive callers (the desktop update button, gateway/update, cron) rely on, since they have no way to resolve a skip. Your commits are untouched:git checkoutnever discards committed work, and the update prints a loud notice naming the branch and commit count, plus thegit checkout <branch>command to pick the work back up later.
If you deliberately run a custom branch (local patches maintained on top of main), set updates.parked_branch_strategy: update_in_place in config.yaml. The update then merges origin/main into your branch instead of switching away from it — the checkout never moves, your commits survive, and the running code advances. Fast-forward when possible; on divergence a true merge behind a pre-update-<stamp> safety tag, stopping cleanly (nothing changed) on conflict. subnaut update --switch-branch overrides back to the switch path for one run — useful on a deep feature branch that must not accumulate update-driven merge commits.
When the parked branch has uncommitted changes (dirty tree), Subnaut does not touch it. The code update is marked SKIPPED with a loud warning naming the branch, how far behind origin/main it is, and the exact commands to resolve — instead of pretending the update succeeded. The completion line always shows the actual branch and HEAD (✓ Update complete! [main @ 30fcf9580]) so drift is visible at a glance. Set updates.auto_switch_parked_branch: false in config.yaml to disable the auto-switch entirely (the skip warning still fires).
Local changes on non-interactive updates
When you run subnaut update in a terminal, Subnaut stashes any uncommitted source-tree changes, pulls, then asks whether to restore them — exactly as it always has. Nothing changes for interactive updates.
When the update runs without a terminal — from the desktop/chat app's "Update" button or a gateway-triggered update — there's no prompt to answer. The updates.non_interactive_local_changes setting decides what happens to your stashed changes:
# ~/.subnaut/config.yaml
updates:
non_interactive_local_changes: stash # default: keep + auto-restore
# non_interactive_local_changes: discard # throw local source edits away
stash(default) — auto-stash, pull, then auto-restore your changes on top of the updated code. Nothing is lost; if a restore hits conflicts they're preserved in a git stash for manual recovery.discard— auto-stash and drop the stash after the pull, so the update always lands on a clean tree. Use this only on machines where you never intend to keep local edits to the Subnaut source. It stash-drops (notgit reset --hard+git clean -fd), so ignored paths likenode_modules,venv, and build outputs are never touched.
In the desktop app this is Settings → Advanced → In-App Update Local Changes.
Desktop updates never auto-restore. The desktop updater invokes subnaut update --keep-stash: local source edits are still stashed so the update can proceed, but they are not re-applied afterward — they stay parked in git stash and the update log prints the exact git stash apply <ref> command to bring them back. This prevents local edits from silently riding along across desktop updates and breaking the freshly updated install. (non_interactive_local_changes: discard still wins if you've opted into discarding.) To restore parked changes manually:
cd ~/.subnaut/subnaut-agent # or your install root
git stash list --format='%gd %H %s' # find the subnaut-update-autostash entry
git stash apply stash@{0}
You can pass --keep-stash to a terminal subnaut update too if you want the same never-reapply behavior interactively.
Preview-only: subnaut update --check
Want to know if an update is available before pulling? Run subnaut update --check — it fetches and compares commits against origin/main. No files are modified, no gateway is restarted. Useful in scripts and cron jobs that gate on "is there an update".
Fleet preview: subnaut update --plan
Before updating a machine that runs several profiles or services, subnaut update --plan prints the full update plan without changing anything: the install kind (git checkout, Docker image, Nix/apt managed), every running Subnaut service across all profiles with its supervisor (systemd, launchd, manual) and the code version it is actually serving, and the restart mechanism each one will get. Manually-launched subnaut serve backends appear too (from the spawn ledger), with their recorded bind endpoint and a "stop before code swap, relaunch with recorded launch args" restart mechanism. On image- or package-managed installs the plan reports that the install is not updatable in place and names the right update command instead. Read-only and safe on a live fleet.
The same inventory is embedded in every real update's receipt (~/.subnaut/logs/update_receipts/), so after an update you can compare what the updater saw against what it did.
Update receipts and the fleet version check
Every subnaut update run writes a machine-readable receipt to ~/.subnaut/logs/update_receipts/ (last 20 kept, latest.json always points at the most recent): the pre-update fleet plan, each step taken, anything skipped and why, the gateway restart outcome, and the final fleet version matrix. After the restart phase the updater compares each live gateway's running code against the freshly updated checkout and prints a per-profile matrix — a gateway still serving pre-update code is reported loudly with the exact restart command, and the update exits non-zero so automation never treats a mixed-version fleet as healthy. Both --plan and the fleet check ask each running gateway directly over its local control socket (gateway.sock in the profile's data directory, a named pipe on Windows) when available, so version and supervisor information comes from the gateway itself; gateways from older versions are still discovered through their state files as before.
Interrupted gateway restarts
If an earlier update pulled code but did not finish restarting the fleet, the next
subnaut update retries even when the checkout is already current. An empty process
scan does not prove recovery: failed systemd units and installed launchd jobs may
have no live PID. The pending restart marker is retained if supervisor discovery
fails, a restart fails, or a requested service cannot be verified active. The update
exits nonzero and reports the affected services; recover them with the printed
commands and retry subnaut update.
A failed historical receipt does not by itself prove that gateways are still stale. Startup and gateway-status warnings, as well as update catch-up, check the live fleet before acting on receipt-only restart obligations. Every recorded gateway profile must have a live successor on the current checkout; an unrelated current gateway cannot stand in for a missing, down, unknown-version, or non-gateway runtime. A manual gateway restart can therefore settle the warning without rewriting a failed update as successful. A separate pending marker remains authoritative because it can belong to a newer interrupted update whose inventory never reached the receipt.
Full pre-update backup: --backup
For high-value profiles (production gateways, shared team installs) you can opt into a full pre-pull backup of SUBNAUT_HOME (config, auth, sessions, skills, pairing):
subnaut update --backup
Or make it the default for every run:
# ~/.subnaut/config.yaml
updates:
pre_update_backup: full
updates.pre_update_backup is a single knob with three modes: quick (default — the lightweight state snapshot described above), full (the quick snapshot plus a complete SUBNAUT_HOME zip; can add minutes on large homes), and off (no pre-update backup at all — --no-backup does the same for a single run). Legacy boolean values still work: true means full, false means off.
Update backups protect an in-place update. If you're migrating your whole setup to different hardware, use subnaut backup + subnaut import instead — see Exporting Subnaut to another machine and subnaut backup vs subnaut profile export.
Windows: another subnaut.exe is running
On Windows, subnaut update will refuse to run if it detects another subnaut.exe process holding the venv's entry-point executable open — most commonly the Subnaut Desktop app's spawned backend, an open subnaut REPL in another terminal, or a running gateway:
$ subnaut update
✗ Another subnaut.exe is running:
PID 12345 subnaut.exe
Updating now would fail to overwrite ...\venv\Scripts\subnaut.exe because
Windows blocks REPLACE on a running executable.
Close Subnaut Desktop, exit any open `subnaut` REPLs, and
stop the gateway (`subnaut gateway stop`) before retrying.
Override with `subnaut update --force` if you've already
confirmed those processes will not write to the venv.
Close the listed processes and re-run. If you're sure the concurrent process won't interfere (rare — usually only useful when an antivirus shim is mis-attributed), pass --force to skip the check. In that case the updater will still retry the .exe rename with exponential backoff and, on stubborn locks, schedule the replacement for next reboot via MoveFileEx(MOVEFILE_DELAY_UNTIL_REBOOT) so the update can complete.
A second, separate guard refuses to touch the venv while any process is running from its Python interpreter (the Desktop app's backend, a gateway, a Python REPL). Those processes keep native extension files (.pyd) locked, and a dependency sync that dies partway on an access-denied error strands the install between versions. This guard is not bypassed by --force; if you're certain the detected holders are false positives, use the explicit subnaut update --force-venv.
Windows venv recreation is transactional
When the Windows installer must recreate an existing venv, it first moves the old directory to a unique venv.stale.* name, then creates and verifies the replacement. The old tree is deleted only after the dependency install completes and the baseline imports pass in the new tree — until then it is the rollback source (recorded in venv.pending-backup).
If the move cannot be completed, the installer stops and leaves the live venv untouched. If uv fails or reports success without creating the interpreter, any partial replacement is moved to venv.failed.* and the previous venv is restored. This keeps the health and blocker checks usable after a failed install.
A venv.stale.* or venv.failed.* directory can remain when another process still owns a file handle. Close Subnaut Desktop, gateways, and Python processes using the install, then retry the install/update; parked directories are cleaned up best-effort after a successful recreation.
Expected output looks like:
$ subnaut update
Updating Subnaut Agent...
📥 Pulling latest code...
Already up to date. (or: Updating abc1234..def5678)
📦 Updating dependencies...
✅ Dependencies updated
🔍 Checking for new config options...
✅ Config is up to date (or: Found 2 new options — running migration...)
🔄 Restarting gateways...
✅ Gateway restarted
✅ Subnaut Agent updated successfully!
Recommended Post-Update Validation
subnaut update handles the main update path, but a quick validation confirms everything landed cleanly:
git status --short— if the tree is unexpectedly dirty, inspect before continuingsubnaut doctor— checks config, dependencies, and service healthsubnaut --version— confirm the version bumped as expected- If you use the gateway:
subnaut gateway status - If
doctorreports npm audit issues: runnpm audit fixin the flagged directory
If git status --short shows unexpected changes after subnaut update, stop and inspect them before continuing. This usually means local modifications were reapplied on top of the updated code, or a dependency step refreshed lockfiles.
If your terminal disconnects mid-update
subnaut update protects itself against accidental terminal loss:
- The update ignores
SIGHUP, so closing your SSH session or terminal window no longer kills it mid-install.pipandgitchild processes inherit this protection, so the Python environment cannot be left half-installed by a dropped connection. - All output is mirrored to
~/.subnaut/logs/update.logwhile the update runs. If your terminal disappears, reconnect and inspect the log to see whether the update finished and whether the gateway restart succeeded:
tail -f ~/.subnaut/logs/update.log
Ctrl-C(SIGINT) and system shutdown (SIGTERM) are still honored — those are deliberate cancellations, not accidents.
You no longer need to wrap subnaut update in screen or tmux to survive a terminal drop.
Checking your current version
subnaut --version
Compare against the latest release at the GitHub releases page.
Updating from Messaging Platforms
You can also update directly from Telegram, Discord, Slack, WhatsApp, or Teams by sending:
/update
This pulls the latest code, updates dependencies, and restarts running gateways. The bot will briefly go offline during the restart (typically 5–15 seconds) and then resume.
Manual Update
If you installed manually (not via the quick installer):
cd /path/to/subnaut-agent
# Activate the venv you created during install (outside the source tree)
export VIRTUAL_ENV="$HOME/.subnaut/venvs/subnaut-dev"
export PATH="$VIRTUAL_ENV/bin:$PATH"
# Pull latest code
git pull origin main
# Reinstall (picks up new dependencies)
uv pip install -e ".[all]"
# Check for new config options
subnaut config check
subnaut config migrate # Interactively add any missing options
Rollback instructions
If an update introduces a problem, you can roll back to a previous version:
cd /path/to/subnaut-agent
# List recent versions
git log --oneline -10
# Roll back to a specific commit
git checkout <commit-hash>
uv pip install -e ".[all]"
# Restart the gateway if running
subnaut gateway restart
To roll back to a specific release tag (substitute your previous tag — e.g. a recent release like v2026.5.16, or any earlier tag from git tag --sort=-version:refname):
git checkout vX.Y.Z
uv pip install -e ".[all]"
Rolling back may cause config incompatibilities if new options were added. Run subnaut config check after rolling back and remove any unrecognized options from config.yaml if you encounter errors.
Image-managed installs (Docker): the provenance marker
Published Docker images bake a small read-only marker (/etc/subnaut/image-provenance.json) that authoritatively identifies the filesystem as image-managed. subnaut update, subnaut update --check, and the backend's update API all consult it before touching anything: on an image-managed install they refuse cleanly (exit code 2), print the actual update command (docker pull ghcr.io/moy-boy/subnaut:latest), and write a refused receipt so fleet tooling can see the attempt happened. The marker wins even when a source checkout is bind-mounted into the container — the refusal is based on what the running filesystem is, not what it looks like. A damaged marker still refuses (fail-closed). Nix- and apt-managed installs refuse through the same gate using the existing detection.
Note for Nix users
Nix is no longer an explicitly supported install path (best-effort only) — see Nix Setup. If you installed via Nix flake, updates are managed through the Nix package manager:
# Update the flake input
nix flake update subnaut-agent
# Or rebuild with the latest
nix profile upgrade subnaut-agent
Nix installations are immutable — rollback is handled by Nix's generation system:
nix profile rollback
See Nix Setup for more details.
Uninstalling
subnaut uninstall
The uninstaller gives you the option to keep your configuration files (~/.subnaut/) for a future reinstall.
Take your setup with you before removing anything: subnaut backup captures the entire ~/.subnaut directory including credentials, while subnaut profile export packs a single profile with credentials excluded by design (so an export alone is not a full backup). See subnaut backup vs subnaut profile export.
Manual Uninstall
rm -f ~/.local/bin/subnaut
rm -rf /path/to/subnaut-agent
rm -rf ~/.subnaut # Optional — keep if you plan to reinstall
If you installed the gateway as a system service, stop and disable it first:
subnaut gateway stop
# Linux: systemctl --user disable subnaut-gateway
# macOS: launchctl remove ai.subnaut.gateway