`hermes update` and the Desktop update button on an official source checkout now move between published vX.Y.Z releases instead of tracking main commit by commit, and `hermes update --set-channel stable` works. - stable resolves through R2 when a record exists; until one is published it falls back to the latest published GitHub release (non-draft, non-prerelease, strict vX.Y.Z, origin tag verified). It used to fail with "Channel object not found: releases/channels/stable.json". - default_channel(): an official-origin source checkout defaults to stable; forks, mirrors and checkout-less trees keep main. Package installs (MSIX, Store, bundled, electron-updater) are untouched. - An install that never chose a channel only moves forward: a checkout already newer than the latest release (every install that tracked main) stays put until a newer release ships. --set-channel stable is the explicit consent to land exactly on the release commit. - Same rule in --check, the apply path and the passive source_check the Desktop and banner use.
50 KiB
sidebar_position, title, description
| sidebar_position | title | description |
|---|---|---|
| 3 | Updating & Uninstalling | How to update Hermes Agent to the latest version or uninstall it |
Updating & Uninstalling
Updating
Choose the update method for the installation that is running:
| Installation | Update method |
|---|---|
| Managed source checkout | hermes update, or the source-built desktop's update handoff. |
| Windows sideload MSIX | The desktop Update control and Windows App Installer. |
| Microsoft Store package | Microsoft Store updates. |
| macOS bundled app | The desktop Update control, through electron-updater. |
| Docker image | Pull the chosen image and recreate the container with the same data mount. |
| Nix | Update the flake/profile and rebuild. |
| Termux APT | pkg update, then pkg upgrade hermes-agent. |
hermes update does not rewrite package-owned application files. PM manages
dependencies, not application distribution: hermes pm update is a maintainer
pin-update command, not an alternative application updater.
For a managed source installation:
hermes update
A source checkout of the official repository tracks the stable channel: the
latest published vX.Y.Z GitHub release (never a draft, prerelease or canary tag),
checked out at its exact commit. hermes update and the Desktop update button move
only from one release to the next, never commit by commit. Forks and mirrors keep
tracking their main branch. The update prepares dependencies through PM and
reports configuration changes and process-restart results.
hermes update --set-channel main # follow every commit on main instead
hermes update --set-channel stable # back to releases only
hermes update --channel main # one run against main; nothing is saved
The choice is stored per install under update.installs in config.yaml. An
install that never chose a channel only moves forward: a checkout already newer
than the latest release (for example one that tracked main before this default)
stays where it is until a newer release ships. Running --set-channel stable
pins the checkout to the exact release commit, even when that is older.
Windows Store, MSIX and bundled desktop installs are unaffected; their channel
is baked into the package (see below).
Bundled desktop updates
On Windows, the sideload app checks the registered App Installer source. Apply
downloads the .appinstaller descriptor before stopping app-owned backends,
opens that local file, and quits for Windows to replace the package.
A detached waiter attempts automatic relaunch after the package version changes.
A waiter-registration failure produces a manual-reopen warning.
On macOS, apply downloads the ZIP update and waits for Squirrel.Mac to accept the signed app before stopping backends. Only that apply flow requests install and relaunch. An ordinary quit does not install a pending download. Download or verification failures leave backends running.
Store packages use the Store instead of either sideload feed. A broken or unavailable update check must not be interpreted as "already up to date." The application and its bundled base runtime update together; user data stays outside the package. See Desktop installation.
Stable and canary desktop bundles are separate applications. Installing canary
does not replace stable, including on Windows with MSIX. Each application keeps
its own desktop state and updates within the channel baked into its build;
changing channels means installing the other application, not changing a setting.
The canary icon has a yellow background (dark yellow in dark mode), and its CLI
command is hermes-canary.
One-off commit bundles are separate from both release channels and from other
commit bundles. Their red icons show the short build SHA, also used in the
hermes-<short-sha> CLI command. They do not check for or install updates,
including through hermes update or hermes update --check. They explain:
This build doesn't get updates. Ask the developer who gave it to you for a new build.
Separate applications still share Hermes profiles, configuration, and sessions
under the same Hermes home. Running different builds against one profile is not
schema isolation: newer builds can change stored data that an older build cannot
read. Back up shared data before testing. The desktop and standalone CLI warn
when another live installation uses the same profile; this is advisory, not a
lock. Desktop post-update notices are scoped to the application, so launching
canary cannot consume stable's pending notice. The hermes:// URL scheme remains
shared; the application that most recently registered it handles links.
:::tip
hermes update automatically detects new configuration options and prompts you to add them. If you skipped that prompt, you can manually run hermes config check to see missing options, then hermes config migrate to interactively add them.
:::
Passive update notices
Pinned or noninteractive installations can disable passive CLI version and banner update checks:
hermes config set updates.check false
This suppresses both cached update notices and passive update-check network requests. The default is true. Explicit hermes update --check and hermes update still work; this setting does not control the Desktop application's updater.
What happens during an update
For an admitted source checkout, hermes update runs these phases:
- Pre-update snapshot — Hermes saves selected state files for every profile in that profile's
state-snapshots/directory. These include pairing data, cron jobs,config.yaml,.env, andauth.json. Automatic quick snapshots skip individual files larger than 1 GiB.updates.pre_update_backupselectsquick,full, oroff. Full archives use the backup exclusions. Recovery uses Snapshots and rollback. Quick snapshots recover state files, not application code. The snapshot is best-effort: if it fails, the update prints a⚠ Pre-update snapshot FAILEDwarning and continues, and the receipt recordspre_update_backupas a failed step (a deliberateoff/--no-backuplands in the receipt's skips with its reason instead). - Code update — applies the configured source branch or stable release tag and updates submodules. On Linux and macOS every gateway running from this install is paused immediately before the first change to the checkout (see Gateway downtime during an update); an update that has nothing to apply, or that fails before it changes the checkout, stops nothing.
- Post-pull syntax validation + auto-rollback — after the pull, Hermes compiles the nine critical files every
hermesinvocation imports at startup. If any fails to parse (e.g. an orphan merge-conflict marker, an accidentally truncated file), Hermes rolls the install back so your shell stays bootable: it resets the checkout to the commit it ran before the pull (git reset --hard <pre-pull-sha>), or, when the update switched away from a detached checkout or a parked feature branch, returns there withgit checkout --detach <sha>/git checkout <branch>(a branch it cannot check out again is restored detached at the same commit). Re-runhermes updateonce the upstream fix lands. - Dependency preparation — PM provisions required tools and prepares a complete Python environment from the new lock, existing extras, and enabled plugin requirements. It validates that environment before publishing its selection. A plugin never fails the update. A plugin that no longer fits the new core is added to
plugins.disabledin every profile that enables it (a memory provider hasmemory.providercleared). That covers arequires-pythonthat excludes Hermes's Python, amanifest_versionnewer than this Hermes supports, and dependencies that the resolver proves can't resolve alongside core and earlier plugins in config order, or that fail their own build. A download or network failure gets one retry and is disabled if it fails again. The update prints⚠ Disabled plugin '<name>' in <home>: <reason>, records it in the receipt's warnings, and continues. Re-enable it withhermes plugins enable <name>once the plugin ships a compatible release, or once the network is back. Arequires_hermesrange the running version misses never disables a plugin, because a source checkout without release tags can read as an older release. That plugin sits out instead (⚠ Left plugin '<name>' … out of this update), stays enabled, and rejoins once Hermes reports a version it accepts. A secondary profile whose config cannot be read sits out the same way until the config is fixed. Only a core that cannot build on its own fails this step. - Config migration — detects new config options added since your version and prompts you to set them
- Desktop rebuild (stage-and-swap) — if the Hermes 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 (on Windows a real-time scanner briefly holdingrelease/win-unpackedis ridden out with a few short retries). A rebuild that fails at any point — corrupt Electron download, missing dependency, disk full — leaves the previous app untouched and launchable; the update prints a⚠line, still finishes the remaining steps (config migration, gateway restart) and exits 0 with abuildfollow-up in its receipt, and the next launch,hermes updateorhermes desktop --build-only --force-buildretries the rebuild. On macOS the rebuilt bundle is then copied (withditto, signature intact) over a stale/Applications/Hermes.appor~/Applications/Hermes.app, so the copy Finder and the Dock launch matches the backend; an installed copy that is currently running is left alone and the update tells you to quit it and runhermes updateagain. If a copy an earlier update installed has since been removed (including dragged to the Trash), the update rebuilds it if needed, puts it back and says so, so Finder, the Dock and Spotlight find Hermes again;hermes uninstallremoves the app for good. On Windows, when the update finishes from inside the Desktop app it would rebuild (the app completing an interrupted update at launch), the rebuild is skipped with a notice instead: Windows locks a running app's files, and stopping the app would end the update with it. The rest of the update completes; quit Hermes Desktop and runhermes desktopfrom a terminal, or use Update now in Settings → About, to rebuild and reopen it. - Gateway auto-restart: on Linux and macOS, gateways paused for the update are started again as soon as the dependencies are prepared and the launchers refreshed, before the product builds; on Windows they restart after the builds. Any other running gateway is refreshed after the update completes. Service-managed gateways (systemd on Linux, launchd on macOS) restart through the service manager. Manual gateways are relaunched when Hermes can map their PID to a profile. Manually launched
hermes serve/hermes dashboardbackends are different: the updater leaves them running and asks their owner to restart them. See Manual backend restart reminders. Backends owned by a running Desktop app remain the app's responsibility. - Multiplex migration (multi-profile installs) — once the fleet is verified on the new code, an install with two or more profiles that still run one gateway per profile is folded into a single multiplexed default gateway when nothing blocks it (same as
hermes gateway migrate --multiplex --yes); if a blocker exists (a bot token shared by two profiles, a secondary profile binding a port with no/p/<profile>/ingress) the update prints the blockers with their fixes and changes nothing. Single-profile installs are never touched. See Migrating from per-profile gateways.
Gateway downtime during an update
On Linux and macOS, hermes update stops every gateway running from this install right before it first changes the checkout, and starts exactly that set again on the new code once the dependencies are prepared (before the web UI, TUI and Desktop builds, which can take minutes). Your bots are offline for that window: typically the git update plus the dependency sync. Before this, a gateway kept running old code while git rewrote the files under it, and any module it imported lazily in that window came from the new code.
- The update prints one line naming what it paused (
⏸ Paused for the update: hermes-gateway.service) and one when it is back (▶ Restarted paused gateway(s): …). - systemd units and launchd agents are stopped and started through systemd/launchd, so the service manager cannot restart them on old code mid-update. A gateway you started by hand (
hermes gateway run) is drained and then started again in the background from the same command line and Hermes home; its output goes tologs/gateway-update-resume.login that home instead of your terminal. /updatefrom chat survives the pause: when the update runs inside the gateway's own systemd unit, it first moves itself into a scope of its own. A prompt asked while the gateways are paused takes its default immediately, because no gateway can relay it.- The paused set is recorded in the Hermes home before the first stop. If the update is killed, the next
hermescommand (orhermes update) starts exactly those gateways again. A failed update that never changed the checkout restarts them on the old code. If the dependency sync fails after the checkout moved, they restart once a launch has synced the dependencies. - A gateway the updater cannot stop through its supervisor (an s6 or custom systemd unit,
--external-supervisor, a launchd job that started this update) is left running and refreshed after the update, as before. --no-gateway-restartstops nothing; you manage the gateways.- The pause never blocks an update. If it cannot list, record or stop the gateways, it restarts anything it stopped, says so, and the update continues the way it did before the pause existed: gateways run through the swap and are restarted afterwards.
Windows already paused its gateways before the update (they lock files the dependency sync replaces) and restarts them once the update completes, after the builds.
Why the gateway restart can take a while
The restart is drain-first: the running gateway refuses new turns, then waits for in-flight work (chat turns, cron jobs, API runs) to finish before exiting, capped by agent.restart_after_turn_timeout (30 minutes by default) so a long-running job is never cut off mid-run. While that wait is in progress the updater prints, every 30 seconds, what the gateway is still holding for — for example:
→ hermes-gateway: draining (up to 1875s)...
⏳ still draining — 1560s left before the forced restart
waiting on 1 active work unit(s):
• cron job 6ba19dab68df (nightly-scout) in external worker pid 573597, running 6m40s
finish or kill the work above to release the drain now; agent.restart_after_turn_timeout in config.yaml caps this wait
Chat turns show their session key, model and current tool; cron jobs show the job id, name and the process running them (an external restart-safe worker on systemd installs, otherwise the gateway itself). hermes gateway status lists the same units while the gateway is draining. To stop waiting, finish or kill the listed work, or lower agent.restart_after_turn_timeout in config.yaml (0 enters the forced drain immediately).
Wedged work does not hold the restart: a chat turn idle past agent.gateway_timeout, or a cron run older than the scheduler's in-flight allowance (max(2 × the job's interval, cron.inflight_max_minutes), 30 minutes by default), is excluded from the wait and interrupted by the restart instead.
Work that outlives the gateway does not hold the restart either: a cron run already handed to a worker in its own systemd scope (systemd-run --user --scope, the normal case on a systemd install) keeps running when the gateway stops, and its result is delivered from the durable queue by whichever gateway comes up next — so the restart proceeds instead of waiting up to the full cap. A worker that could not get its own scope (no reachable user D-Bus session, see cron.require_restart_safe_scope) stays in the gateway's cgroup and is still awaited, because the restart would kill it mid-run.
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.
Fetch fails with should_include_obj should only be called on existing objects
Git 2.53 and newer can crash while fetching into a partial clone when some of its pack files lack a
.promisor marker. That happens when a filtered fetch turned a full or shallow clone into a partial
one, or when markers were lost. hermes update marks those packs and retries the fetch once, and
an installer rerun marks them before it fetches. An install whose own updater predates that fix
can't fetch it: rerun the installer, or mark the packs by hand (with Hermes closed) and update again.
The checkout is hermes-agent under your Hermes home (~/.hermes, or HERMES_HOME when set;
on Windows %LOCALAPPDATA%\hermes unless HERMES_HOME is set).
# macOS / Linux
repo="${HERMES_HOME:-$HOME/.hermes}/hermes-agent"
for p in "$repo"/.git/objects/pack/pack-*.pack; do [ -e "${p%.pack}.promisor" ] || : > "${p%.pack}.promisor"; done
# Windows
$repo = Join-Path ($(if ($env:HERMES_HOME) { $env:HERMES_HOME } else { "$env:LOCALAPPDATA\hermes" })) 'hermes-agent'
Get-ChildItem "$repo\.git\objects\pack\pack-*.pack" | ForEach-Object {
$m = [IO.Path]::ChangeExtension($_.FullName, '.promisor')
if (-not (Test-Path -LiteralPath $m)) { New-Item -ItemType File -Path $m | Out-Null }
}
The checkout stays a partial clone. Don't remove remote.origin.promisor / partialclonefilter to
get past the crash: objects that only release tags or update backups reach were never downloaded,
so a non-partial checkout then fails git gc with bad tree object. If you already did, fetch the
missing objects and check that none are left before turning automatic cleanup back on:
git -C "$repo" rev-list --objects --missing=print --all | grep '^?' | cut -c2- | git -C "$repo" fetch -q --no-tags --stdin origin
git -C "$repo" rev-list --objects --missing=error --all >/dev/null && echo complete
git -C $repo rev-list --objects --missing=print --all | Where-Object { $_.StartsWith('?') } | ForEach-Object { $_.Substring(1) } | git -C $repo fetch -q --no-tags --stdin origin
git -C $repo rev-list --objects --missing=error --all | Out-Null; $LASTEXITCODE # 0 = complete
.git keeps growing in a partial clone
The installer's checkout is a blobless partial clone: every commit and directory listing is local,
and git downloads file contents on demand, each on-demand download written as its own small pack.
Installers from late September 2026 made treeless (--filter=tree:0) clones instead. git asks for
a missing tree without saying which ones it already has, so a treeless checkout downloaded complete
directory snapshots again on every checkout and path-filtered history walk. hermes update
converts such a checkout once, as its first step after pulling the new code and before the
dependency work: one git fetch --refetch --filter=blob:none brings every commit and tree (about
120 MB), and later updates stop re-downloading them. Re-running the installer over such a checkout
converts it the same way. If that fetch fails, the update prints a warning, carries on, and retries
the conversion next time.
A Hermes Desktop built before the conversion existed runs path-filtered history walks every few minutes, so on a treeless checkout it could fill a disk with pack files (one report reached 434 GB) and keep going after a failed update. To recover when that already happened:
- Quit Hermes Desktop, then end any leftover
git rev-list,git maintenanceorgit commit-graphprocesses; quitting the app does not stop them. - If the disk is completely full, delete the abandoned transfer files to get room back:
rm -f "$repo"/.git/objects/pack/tmp_pack_*(Windows: deletetmp_pack_*in.git\objects\pack). Each update also removes them on its own once no git process is running. - Run
hermes update. It converts the checkout first, then cleans the pack pile down over this and later updates as described below.
If hermes update itself cannot start, the conversion by hand is:
git -C "$repo" fetch --refetch --filter=blob:none origin
git -C "$repo" config remote.origin.partialclonefilter blob:none
``` `hermes update` and `hermes update --check`
set `maintenance.commit-graph.enabled`, `gc.writeCommitGraph` and `fetch.writeCommitGraph` to
`false` in that checkout, because a commit-graph write over commits the graph has not seen yet
downloads every one of their trees. Leave those settings alone, and leave `gc.auto` at its
default so git's own automatic gc can still fold packs.
Each `hermes update` also spends at most 60 seconds cleaning those packs up, picking up where the
previous update stopped. It deletes packs whose every object is also stored in another pack, then
merges the smallest remaining packs while there are more than 50. Nothing stored locally is ever
lost, and nothing depends on GitHub still serving it. Packs that a killed `git fetch` left pinned
with a `.keep` file are included; git's own repack never touches those. To fold
everything at once by hand instead (with Hermes closed; on a large checkout this is a full repack
that can run for many minutes):
```bash
git -C "$repo" -c gc.writeCommitGraph=false gc --auto
Updating against a non-default branch: --branch
--branch NAME is a one-run override that updates against a branch tip
instead of the selected channel:
hermes update --branch release-candidate
hermes update --check --branch experimental # preview behindness only
If your local checkout is on a different branch, Hermes 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), hermes 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. hermes 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), Hermes 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 commits on the target branch
Commits made directly on the update target (main) stop fast-forwards once upstream moves, and the checkout cannot tell them apart from an upstream force-push, so the update resets main to origin/main. Before the reset it saves the old HEAD as refs/hermes-update-backups/diverged-main-<stamp>-<sha> and prints that ref along with how many commits leave the branch. git log origin/main..<ref> lists them; git branch <name> <ref> or git cherry-pick brings them back. Re-running the installer over an existing checkout (install.sh / install.ps1, which desktop bootstrap does) writes the same refs. Whenever hermes update writes one, it keeps the ten newest per kind and drops any older than 30 days. To carry patches across updates, keep them on a custom branch with updates.parked_branch_strategy: update_in_place instead.
Local changes on non-interactive updates
When you run hermes update in a terminal, Hermes stashes any uncommitted source-tree changes, pulls, then asks whether to restore them — exactly as it always has. Nothing changes for interactive updates.
The autostash only ever covers source-tree changes. On a flat install — where the git checkout root is also $HERMES_HOME (for example an install made with HERMES_INSTALL_DIR=$HERMES_HOME, or one created by an older installer) — the profile's runtime state (state.db and its WAL/SHM sidecars, state-snapshots/, backups/, sessions/, cron/jobs.json, the cron/*.db stores, config.yaml, auth.json, memories/, lock/pid files, …) lives inside the checkout as untracked files. Those paths are git-ignored, so the autostash never touches them and the running gateway keeps its database through the update. If you keep other untracked files in a flat install's root, move them out of the checkout or add them to .git/info/exclude; anything untracked and not ignored is swept into the autostash like a source edit.
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:
# ~/.hermes/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 Hermes 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 hermes 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 ~/.hermes/hermes-agent # or your install root
git stash list --format='%gd %H %s' # find the hermes-update-autostash entry
git stash apply stash@{0}
You can pass --keep-stash to a terminal hermes update too if you want the same never-reapply behavior interactively.
Preview-only: hermes update --check
hermes update --check compares the checkout with the selected channel's
target (the latest stable release by default, origin/main on the main channel)
without applying code, installing dependencies, or restarting gateways. The
comparison can fetch Git metadata; it is not a promise of zero filesystem writes.
Package-owned installs report their external update method.
Fleet preview: hermes update --plan
Before updating a machine that runs several profiles or services, run hermes update --plan. It prints the install kind, running Hermes services across profiles, their supervisors and running code versions, and the restart mechanism for each service. Manually launched hermes serve / hermes dashboard backends appear with their recorded bind endpoint, but restart is deferred to their owner; the updater does not stop or relaunch them. Image- or package-managed installs report the external update command instead. The plan is read-only and safe on a live fleet.
The same inventory is embedded in every real update's receipt (~/.hermes/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 hermes update run writes a machine-readable receipt to ~/.hermes/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. The SQLite runtime repair is one of those steps (sqlite_runtime_repair): a failed repair records the actual reason (for example the uv sync error) and the SQLite version pair, a deferred or not-applicable repair lands in the skips with its reason. 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 recorded as a gateway_restart follow-up in the receipt; the code is already in place, so the update still exits 0, and every later command warns until the fleet serves the new code, so a mixed-version fleet is never silently 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.
A multiplexed default gateway is one process serving several profiles, so it appears once in the matrix and vouches for every profile in its served_profiles record. The same coverage clears the "A previous hermes update pulled new code but did not restart running gateways" hint: once that gateway (or, after a manual git pull, every gateway an update restarted) runs the current code, hermes gateway restart is enough — the hint no longer waits for the next hermes update to write a fresh receipt. The same is true of the restart obligation left by an update that died before recording which gateways it owed (or by an older updater that never recorded them): once every live gateway runs the current checkout, the obligation is retired and the hint stops. On a host that runs no gateway at all (the Desktop app alone), it is retired once no profile has a gateway that went away without a clean stop and every running backend is restarted by its own supervisor or has its own reminder. That obligation is recorded once per HOST, in the cross-profile rendezvous directory ($HERMES_GATEWAY_LOCK_DIR, else $XDG_STATE_HOME/hermes/gateway-locks) as host-update-restart-<install key>.json (one per installation, so a second Hermes checkout for the same user keeps its own; an older release's unkeyed host-update-restart.json is honoured and cleared only by an installation that has no record of its own, so another checkout still on that release keeps its debt), so every profile's CLI sees the same one: hermes -p coder update and hermes -p writer update restart the shared multiplexed gateway once between them, not once each. An obligation left behind by an older per-profile updater (fleet_restart_pending in one profile's Hermes home) is still read and cleared. An update whose pre-update plan found no gateway at all owes nothing and leaves no breadcrumb. A backend supervised by Desktop, systemd or launchd is restarted by its supervisor and never blocks this settlement; only a manual backend whose reminder could not be saved keeps the obligation open.
Manual backend restart reminders
After updating, ask the owner of each manually launched backend to relaunch hermes serve or hermes dashboard; reconnect Desktop for an SSH backend. Restarting gateways alone does not refresh these processes.
For a verified live backend, the updater saves a reminder before recording its restart as deferred. This allows the update to complete if the remaining checks pass. An unknown process identity cannot qualify for fresh deferral.
Reminders live in serve_restart_pending/ under the active Hermes home, separately from rotating update receipts. Each reminder identifies a process by PID and creation time. Startup warnings retain it while that process is alive or its liveness is unknown, and remove it only when that exact process is confirmed gone. A later update or a healthy gateway does not remove it.
If saving fails, Hermes warns and carries the unsaved obligation into later receipts for retry. Check storage permissions and free space, and restart the backend as instructed. If both reminder and receipt storage fail, durable retention cannot be guaranteed.
Automated updates from inside the gateway: --no-gateway-restart
An update launched by the gateway (a cron job, the Desktop updater, any automation that is a
child of the gateway process) cannot survive its own fleet restart: the gateway drains on
SIGUSR1 and systemd's KillMode=mixed then kills everything left in the cgroup, updater
included. hermes update --no-gateway-restart runs the full pipeline (pull, dependencies,
Node workspaces, web UI, maintenance) and skips only the restart and fleet verification; on Linux
and macOS it also skips the pause before the checkout changes, so nothing is stopped. The
pending-restart marker is kept, so the next CLI start warns and the next normal hermes update
(or hermes gateway restart) catches the fleet up. Pair it with a separate restart step, for
example a timer 10–15 minutes after the update job. The receipt records the deferral; a stale
fleet caused only by the deferral does not make the update partial.
Interrupted gateway restarts
If an earlier update pulled code but did not finish restarting the fleet, the next hermes 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. If discovery or restart fails, or a requested service cannot be verified active, the marker stays pending: the update still exits 0 (the new code is in place), prints a ⚠ line and a gateway_restart follow-up in its receipt, every CLI start warns about the owed restart, and the next hermes update retries it. Recover the affected services with the printed commands, or rerun hermes update.
A failed historical receipt alone does not prove that gateways are still stale. Startup and gateway-status warnings, as well as update catch-up, check for a live successor on the current checkout for every gateway profile recorded in that receipt. A manual gateway restart can settle receipt-only advice without rewriting the historical outcome. Manual backend obligations must first transfer to their separate reminders.
A pending restart marker records its own pre-update runtime inventory and target commit. Recovery checks that inventory, never an older receipt's inventory. Settlement requires a nonempty verified current fleet, a successor at the target commit for every recorded gateway profile, and successful handling of any identified manual backend obligations. A manual backend that is still alive or whose liveness is unknown needs a saved reminder; a confirmed exited process needs no reminder. Missing gateways, unsupported runtimes, or failed reminder writes keep the marker pending.
Legacy markers without an inventory, and malformed or unsupported inventories, cannot be automatically cleared by startup or catch-up reconciliation. Restarting all visible gateways is not enough to establish what that update owed. The warning remains until a later update completes with a new marker containing its own inventory and settles successfully.
Full pre-update backup: --backup
For high-value profiles (production gateways, shared team installs) you can opt into a full pre-pull backup of HERMES_HOME (config, auth, sessions, skills, pairing):
hermes update --backup
Or make it the default for every run:
# ~/.hermes/config.yaml
updates:
pre_update_backup: full
updates.pre_update_backup has three modes:
quicksaves the selected state files described above. This is the default.fulladds a zip archive with the backup exclusions. Large data directories can take several minutes.offdisables pre-update backups.--no-backupselects this mode for one run.
Legacy boolean values remain supported: true means full, and false means off.
:::tip Moving to a new machine instead?
Update backups protect an in-place update. If you're migrating your whole setup to different hardware, use hermes backup + hermes import instead — see Exporting Hermes to another machine and hermes backup vs hermes profile export.
:::
Windows process ownership and dependency changes
Windows can hold executable and native-extension files open while processes run. PM therefore prepares dependency generations separately instead of replacing imported libraries in place. A running process keeps its current imports until it restarts.
Follow any blocker diagnostic for the specific process and installation. Do not kill every process named Python or Hermes. Source updates and MSIX package replacement have different owners and shutdown requirements.
The parser retains --force and --force-venv for Windows compatibility. They
are not normal update instructions or a guarantee that an OS file lock can be
bypassed. Inspect hermes update --plan, the update log, and hermes pm status
before retrying a failed update.
Recommended Post-Update Validation
After a source update, run the following diagnostics. For a bundled app, use its About page and backend health instead of Git commands inside the package.
git status --short— if the tree is unexpectedly dirty, inspect before continuinghermes doctor— checks config, dependencies, and service healthhermes --version— confirm the version bumped as expected- If you use the gateway:
hermes gateway status hermes pm status— inspect dependency preparation and any failed steps.
:::warning Dirty working tree after update
If git status --short shows unexpected changes after hermes 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
hermes 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
~/.hermes/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 ~/.hermes/logs/update.log
Ctrl-C(SIGINT) and system shutdown (SIGTERM) are still honored — those are deliberate cancellations, not accidents. ACtrl-Cafter the new code is in place exits 130 and says so: the remaining steps (dependencies, builds, gateway restart) are owed, and the next launch orhermes updatefinishes them.
You no longer need to wrap hermes update in screen or tmux to survive a terminal drop.
When an update or launch refuses instead of guessing
A few situations stop with a message rather than carry on, because carrying on could leave a half-updated checkout or a gateway running old code:
- Another launch is repairing an interrupted update. When an update was killed while git moved the checkout, the next
hermeslaunch puts the checkout back first. Launches that start together (a restarting gateway or Desktop backend next to your CLI) take turns. A launch that waits more than 10 seconds for another one's repair exits with status 1:hermes: another Hermes launch is finishing an interrupted 'hermes update'; launch again in a moment.Launch again once the other one finishes. A backend started by Hermes Desktop at that moment shows up as a failed start; reopening it is enough. - Windows git failure → ZIP fallback. On Windows, a git error during
hermes updatefalls back to downloading the release ZIP only when git has not touched the checkout yet: same commit and branch as when the update started, no stash taken, no tree move begun. Once git has stashed, switched branch or started moving files, the update reports the git error and exits 1 instead, because the ZIP overlay would bury that state. Re-runhermes updateafter fixing the git problem. - Desktop builds need a writable
.git. Building Hermes Desktop from a checkout withhermes desktoptakes the same checkout lock ashermes update(which needs it too), kept in the checkout's git directory. If that directory is not writable for your user (for example a checkout owned by another account), the build refuses with.../.git/hermes-update.lock is not writableinstead of building alongside a possible update. Fix the ownership or permissions of.git. On macOS/Linux, a lock file left read-only by asudo hermes updateis still usable. - Restart record not writable.
hermes updaterecords the owed gateway restart before it moves the checkout, in the host state directory ($HERMES_GATEWAY_LOCK_DIR, else$XDG_STATE_HOME/hermes/gateway-locks). If that directory cannot be written, the record goes into the current profile's Hermes home instead. Only that profile reads it, so the update goes ahead only on an install with no other profile. If other profiles exist, or the profile list cannot be read, the update refuses before moving anything. Make the state directory writable and run it again.
Checking your current version
hermes --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 source maintenance and rollback
Use the source-install guide for an independent development checkout. Commit or otherwise preserve local work before changing its Git revision. Stop the services you own before replacing the code that those services run.
After changing the source revision, run that revision's dependency/bootstrap
procedure. PM-managed checkouts use python -m pm.cli install from the selected
source environment. A bare pip install is not equivalent: it does not prepare
the managed tool store or PM's runtime selection.
A code checkout alone is not a data rollback. Newer releases can migrate configuration or databases in ways older code cannot read. Use a coherent backup from before the update if an older release requires the earlier data format. Preserve the current data before attempting a restore.
Package-owned installations use their package manager's rollback or reinstall procedure. Do not run Git or pip inside a signed app or an immutable image.
Python 3.14 and older interpreters
Hermes runs only on Python 3.14. pyproject.toml still declares
requires-python = ">=3.11,<3.15", but every runtime dependency carries a
python_version >= '3.14' marker. The wider range exists for one reason: a
source install whose venv predates the PM migration (Python 3.11–3.13) must be
able to check out the new code and run hermes update once more. That update
hands off to a fresh completion process, PM provisions the pinned 3.14
interpreter from pm/lock.json, builds the dependency environment on it, and
repoints the launchers. After that the old interpreter is no longer used.
What this means outside hermes update:
pip install .,uv pip install .,pip install -e ., or a Homebrew/PyPI package built on Python 3.11–3.13 installshermes-agentwith no dependencies and the package does not import. These install methods are unsupported; use the source installer or a packaged build.- A Nix build outside the repo flake sees the same marker-gated dependency set and needs a 3.14 interpreter.
- A
hermeslauncher that still points at a pre-migration venv should be repaired withhermes update(orpython -m pm.cli installfrom the checkout), not by reinstalling into the old venv.
Image-managed installs (Docker): the provenance marker
Published Docker images bake a small read-only marker (/etc/hermes/image-provenance.json) that authoritatively identifies the filesystem as image-managed. hermes update, hermes update --check, and the dashboard's Update button all consult it before touching anything: on an image-managed install they refuse cleanly (exit code 2), print the actual update command (docker pull nousresearch/hermes-agent: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 hermes-agent
# Or rebuild with the latest
nix profile upgrade hermes-agent
Nix installations are immutable — rollback is handled by Nix's generation system:
nix profile rollback
See Nix Setup for more details.
Uninstalling
hermes uninstall
For source installs, review hermes uninstall --dry-run before removal.
The default removal can preserve configuration and user data. --full also
removes data. --data removes user data without deleting package-owned code.
These modes are destructive; make a backup first.
For MSIX/Store, use Windows Settings → Apps → Installed apps. For macOS, quit Hermes and move its app bundle to Trash. Docker, Nix, and Termux use the same manager that installed them. Their package files are not removed by the source uninstaller. Data deletion is separate from package removal.
:::tip Moving to a new machine rather than leaving?
Run hermes backup before you remove the installation. The full archive includes
credentials but excludes downloaded runtimes, dependency environments, caches,
and browser profiles. Review its skipped-file report before you delete source data.
hermes profile export packs one profile without credentials.
See hermes backup vs hermes profile export.
:::
Manual Uninstall
rm -f ~/.local/bin/hermes
rm -rf /path/to/hermes-agent
rm -rf ~/.hermes # Optional — keep if you plan to reinstall
:::info If you installed the gateway as a system service, stop and disable it first:
hermes gateway stop
# Linux: systemctl --user disable hermes-gateway
# macOS: launchctl remove ai.hermes.gateway
:::