🤖HermesBlog
Hermes Official Tutorials · Part 48/9/2026

What’s New in This Tutorial?

Tutorial 4: Updating & Uninstalling Hermes — easy-to-understand guide based on official docs

Servicing Hermes: update & uninstall dashboard

What’s New in This Tutorial?

In our last tutorial, we got Hermes up and running. Now let’s talk about keeping it fresh and, when the time comes, saying goodbye. The good news? Both are a lot easier than you might think.

Updating Hermes: One Command, Done

Updating Hermes is as simple as:

hermes update

That’s it. This single command does a lot of heavy lifting for you. It pulls the latest code, updates all dependencies, and even checks if there are any new configuration options you should know about. If there are, it’ll ask you right then and there if you want to add them. 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.

Pro tip: If you skip that configuration prompt (maybe you were in a hurry), you can always run hermes config check later to see what’s missing, and hermes config migrate to add those options interactively.

Note: Pinned or noninteractive installations can disable passive CLI version and banner update checks with hermes config set updates.check false. This suppresses cached update notices and passive update-check network requests (the default is true); explicit hermes update --check and hermes update still work, and this setting does not control the Desktop application’s updater.

Planning ahead: Before updating a machine that runs several profiles or services, run hermes update --plan. It’s read-only and safe on a live fleet: it prints the install kind, every running Hermes service across profiles with its supervisor and the code version it’s actually serving, and the restart mechanism each one will get. Manually launched hermes serve / hermes dashboard backends show up with their recorded bind endpoint, but the updater leaves them for their owner to restart. Image- or package-managed installs report the external update command instead.

Pick the update method that matches your install

Not every Hermes install updates the same way. Match yours to the table below:

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.

One thing worth repeating: hermes update does not rewrite package-owned application files. PM manages dependencies, not application distribution, so hermes pm update is a maintainer pin-update command, not an alternative application updater. The installers (install.sh, install.ps1 and Hermes-Setup) put a new install on the latest stable release too. Re-running one over an install that never chose a channel moves it onto that release only when the release descends from its checkout. Any other checkout (one that followed main, or one already past the release) stays where it is, and hermes update decides that move: it refuses a release that predates the session stores the checkout already uses. Over an install that chose a channel other than main, a rerun keeps its checkout where it is: only hermes update moves it. To install the tip of main instead, pass --channel main (-Channel main for install.ps1, or set HERMES_INSTALL_CHANNEL=main before an irm | iex one-liner); the installer saves that choice the way hermes update --set-channel main does. An explicit --commit pin still applies.

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.

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.

What Actually Happens During an Update?

You might be wondering, “What’s going on under the hood?” Here’s the breakdown:

  1. Safety snapshot first — Before anything changes, Hermes takes a quick snapshot of your important files (pairing data, cron jobs, config, and more). This is your safety net. If anything goes wrong, you can restore from this snapshot. By default it’s a “quick” snapshot, but you can set it to “full” for a complete backup of everything. On multi-profile installs, each profile gets its own snapshot. The snapshot is best-effort: if it fails, the update prints a ⚠ Pre-update snapshot FAILED warning and keeps going.

  2. 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. The update prepares dependencies through PM and reports configuration changes and process-restart results.

  3. Post-pull syntax validation + auto-rollback — Here’s a cool part: after pulling, Hermes compiles the nine critical files every hermes invocation imports at startup. If any fails to parse (for example an orphan merge-conflict marker or 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 with git checkout --detach <sha> / git checkout <branch> (a branch it cannot check out again is restored detached at the same commit). Re-run hermes update once the upstream fix lands. After this point the updater re-executes itself on the freshly pulled code, so the remaining steps never mix old and new modules in one process — if you briefly see two hermes update processes, that’s the hand-off.

  4. Dependency preparation — PM now handles this step: it provisions the required tools and prepares a complete Python environment from the new lock, the existing extras and the requirements of enabled plugins, and validates that environment before publishing its selection. A plugin never fails the update: if a plugin no longer fits the new core (its required Python version, a newer manifest version, or dependencies that can’t resolve), the update prints ⚠ Disabled plugin '<name>' in <home>: <reason>, records it in the update receipt and carries on — re-enable it with hermes plugins enable <name> once it ships a compatible release. Only a core that cannot build on its own fails this step.

  5. Config migration — New options are detected and you’re prompted to set them.

  6. 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 holding release/win-unpacked is 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 a build follow-up in its receipt, and the next launch, hermes update or hermes desktop --build-only --force-build retries the rebuild. On macOS the rebuilt bundle is then copied (with ditto, signature intact) over a stale /Applications/Hermes.app or ~/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 run hermes update again. 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 uninstall removes 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 run hermes desktop from a terminal, or use Update now in Settings → About, to rebuild and reopen it.

  7. 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 restart through their service manager, and manual gateways are relaunched when Hermes can map their PID to a profile. Manually launched hermes serve / hermes dashboard backends are different: the updater leaves them running and asks their owner to restart them. Backends owned by a running Desktop app stay the app’s responsibility.

  8. 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; if a blocker exists, the update prints the blockers with their fixes and changes nothing. Single-profile installs are never touched.

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 to logs/gateway-update-resume.log in that home instead of your terminal.
  • /update from 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 hermes command (or hermes 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-restart stops 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 the old code until they are refreshed.

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. 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).

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.

Checking what happened

Every hermes update run writes a machine-readable receipt to ~/.hermes/logs/update_receipts/ (the last 20 are kept, and 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 also 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.

Updating from a Different Branch

Most people will stick with the default channel, but if you’re testing a release candidate or a feature branch, you can do:

hermes update --branch release-candidate

And if you just want to check how far behind you are without updating:

hermes update --check --branch experimental

Source channels and install identity

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.

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; the Desktop names that move as a rollback to the release, and hermes update --check says the checkout is newer, instead of announcing a new update. Windows Store, MSIX and bundled desktop installs are unaffected; their channel is baked into the package.

You can also inspect and change that per-install:

hermes update --install-id
hermes update --set-channel stable
hermes update --channel stable --check
# Or track published canary commits in this source installation:
hermes update --set-channel canary
hermes update

--install-id prints the installation identity and path. --set-channel changes only that installation’s configuration, then exits without applying an update. --channel is a one-run override. An explicit --branch takes precedence for a source checkout. Channel names are registered in the release archive on Cloudflare R2, not in a fixed list shipped with Hermes, so an unavailable or invalid record reports an error rather than falling back to main or another release. Switching a source channel does not install a desktop package.

hermes update --check compares the checkout with its configured channel.

What If My Checkout Is on a Feature Branch?

Sometimes you might have left your code on a feature branch. Hermes handles this smartly:

  • Fully merged branch? It switches you back to main and tells you it did so.
  • Unmerged commits but clean tree? It still switches to main so the update can proceed, but it prints a loud notice telling you exactly how to get back to your work.
  • Dirty tree (uncommitted changes)? Hermes won’t touch it. The update is marked as SKIPPED with a warning, and you get clear instructions on what to do.

If you want to stay on your custom branch (say you maintain local patches), you can set updates.parked_branch_strategy: update_in_place in your config. Then updates merge main into your branch instead of switching away. You can also set updates.auto_switch_parked_branch: false to disable the auto-switch entirely (the skip warning still fires).

Troubleshooting Update Errors

Fetch fails with not a git repository

hermes update stops with “This Hermes checkout’s git data is damaged” when the install’s .git folder is still there but git no longer accepts it: a lost or empty HEAD, or a .git file pointing at a folder that is gone. A killed update, a crash mid-write, or a sync or antivirus tool touching .git can leave it that way. No update can fetch on such a checkout. Gateways the update paused are started again on the unchanged code, since nothing was written.

Run the installer again to repair it. It moves the damaged checkout aside as hermes-agent.broken-<timestamp> (nothing is deleted) and clones fresh; your config, sessions and other data live outside the checkout and are kept. The error prints the exact command for your checkout. For the default location it is:

  • Linux / macOS / WSL2: curl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash
  • Windows (PowerShell): iex (irm https://hermes-agent.nousresearch.com/install.ps1)

A checkout installed somewhere else needs its directory passed again (bash -s -- --dir <path> on Linux/macOS, -InstallDir <path> on Windows), as in the printed command; without it the installer repairs the default location instead.

Once Hermes works again you can delete the .broken-* folder.

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 partial clone: git downloads trees and blobs on demand, and each on-demand download is written as its own small pack. hermes update and hermes update --check fold them back together with git gc --auto (git’s own gc.autoPackLimit, 50 by default), so a healthy checkout pays a no-op. They also 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: gc.auto=0 stops the fold. The first fold on a checkout that has piled up thousands of packs is a full repack and can take several minutes; the update says so before it starts, and if the fold runs past 20 minutes it stops and prints the command below. To fold by hand (with Hermes closed):

git -C "$repo" -c gc.writeCommitGraph=false gc --auto

Uninstalling Hermes

While the docs focus heavily on updating, uninstalling is straightforward. You can remove Hermes by deleting the installation directory and cleaning up any configuration files in your home directory. For complete removal, check your system’s package manager if you installed Hermes that way, or simply remove the hermes binary from your PATH.

Wrapping Up

Keeping Hermes updated is a one-command affair with built-in safety nets. The auto-rollback on syntax errors and the snapshot system mean you can update with confidence. And if you ever need to step away, uninstalling is just as simple.

Next up in our series, we’ll dive into more advanced configuration options. Until then, happy updating!

📖 Official Docs

This article is based on the official Hermes Agent documentation:Official docs › getting-started/updating