Skip to content

fix(docker): hermes update prints docker pull guidance instead of bogus git error#33659

Merged
benbarclay merged 2 commits into
mainfrom
docker-update-message
May 28, 2026
Merged

fix(docker): hermes update prints docker pull guidance instead of bogus git error#33659
benbarclay merged 2 commits into
mainfrom
docker-update-message

Conversation

@benbarclay

Copy link
Copy Markdown
Collaborator

Problem

Inside the published Docker image, hermes update falls through to the wrong fallback:

✗ Not a git repository. Please reinstall:
  curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash

That message is doubly wrong inside a container:

  1. The curl installer installs Hermes on the host — running it inside the container is nonsensical, and running it on the host installs a parallel checkout, not an update of the running container.
  2. It never mentions docker pull — the actual right action.

hermes update --check was even worse: "✗ Not a git repository — cannot check for updates." and nothing else.

Both happen because .dockerignore excludes .git from the build context (rightly — see the parallel "bake build SHA into the image" PR for the read side of this), so the git-based update path can never succeed inside the container.

Fix

Detect the Docker install method (already stamped by docker/stage2-hook.sh~/.hermes/.install_method and surfaced by detect_install_method()) and bail with a long-form message that covers everything a Docker user needs:

✗ ``hermes update`` doesn't apply inside the Docker container.

Hermes Agent runs as a published image (nousresearch/hermes-agent), not a
git checkout — the container has no working tree to pull into.  Update by
pulling a fresh image and restarting your container instead:

  docker pull nousresearch/hermes-agent:latest
  # then restart whatever started the container, e.g.:
  docker compose up -d --force-recreate hermes-agent
  # or, for ad-hoc runs, exit the current container and `docker run` again

Verify the new version after restart:
  docker run --rm nousresearch/hermes-agent:latest --version

Notes:
  • If you pinned a specific tag (e.g. ``:v0.14.0``) the ``:latest`` tag
    won't move your container — pull the newer tag you actually want, or
    switch to ``:latest`` / ``:main`` for rolling updates.  See available
    tags at https://hub.docker.com/r/nousresearch/hermes-agent/tags
  • Your config and session history live under ``$HERMES_HOME`` (``/opt/data``
    in the container, typically bind-mounted from the host) and persist
    across image upgrades — re-pulling doesn't lose any state.
  • Running a fork?  Build your own image with this repo's ``Dockerfile``
    and replace the ``docker pull`` step with your build/push pipeline.

Exits with code 1 (matches the managed_error semantic for "tried to update but can't update this way").

Plumbing

File What
hermes_cli/config.py New format_docker_update_message() helper next to the existing _NIX_UPDATE_MSG / format_managed_message() family — single source of truth for the text.
hermes_cli/main.py cmd_update() bails right after the is_managed() gate; _cmd_update_check() bails at the top before the method == "pip" branch. Neither path touches subprocess.run / git when method == "docker".

Test coverage

7 new tests in tests/hermes_cli/test_cmd_update_docker.py:

  • hermes update in Docker → message + exit 1, zero git calls (asserted on subprocess.run mock)
  • hermes update --check (via cmd_update) → same
  • --yes / --force don't bypass the bail-out (intentional — the underlying constraint is "no git fetch can ever work here", not "user hasn't confirmed yet")
  • _cmd_update_check called directly → bails too
  • git installs unaffected → docker message must NOT appear (regression guard)
  • pip installs still use PyPI check → docker message must NOT appear (regression guard)
  • Content-lock test pinning the five user-actionable bits the message must contain (primary command, restart guidance, version verify, tag-pinning caveat, config persistence)

No regressions:

  • Full tests/hermes_cli/test_cmd_update.py (21 tests) — pass
  • tests/hermes_cli/test_managed_installs.py (5 tests) — pass
  • Full tests/hermes_cli/ suite under per-file isolation — 5575 tests pass, 0 failures

End-to-end verified in real Docker:

$ docker run --rm <image-built-from-this-branch> update
... (skill sync noise at first boot)
✗ ``hermes update`` doesn't apply inside the Docker container.
... (full message as above)
$ echo $?
1

$ docker run --rm <image-built-from-this-branch> update --check
... (same message + exit 1)

Follow-up to

PR #33655 (fix(docker): bake build-time git SHA into the image) — that PR addressed the read side of .git being unavailable (hermes dump reporting [(unknown)]). This is the write side: not just "can't read git state" but "can't perform a git-based update either, so steer users to docker pull."

Lane

  • hermes_cli/config.py — config helper module, area I touch routinely for the Docker stack.
  • hermes_cli/main.pyflagging for review: this is the giant CLI argv-dispatcher / subcommand handler (~14k LOC). The change is a 25-line bail-out at the top of two existing subcommands (cmd_update and _cmd_update_check), structurally parallel to the existing is_managed() / method == "pip" gates already in those functions. No new runtime behaviour outside the Docker container; no changes to run_agent, gateway, model resolution, credential pool, or any of the load-bearing pieces. Up to you whether this qualifies as solo-landable area/docker or wants a Teknium pass.
  • tests/ — new file.

… bogus git error

Inside the published Docker image, `hermes update` was hitting the
".git missing → reinstall via curl" fallback:

    ✗ Not a git repository. Please reinstall:
      curl -fsSL https://raw.githubusercontent.com/.../install.sh | bash

That message is wrong on two counts:
  1. It tells the user to run the host-side installer, which would
     install a *new* Hermes on the host — not update the running
     container.
  2. It doesn't mention `docker pull` at all, leaving Docker users
     to figure out the right action from scratch.

`hermes update --check` was worse: it bailed with "Not a git
repository — cannot check for updates." and nothing else.

Fix: detect the Docker install method (already stamped by
`docker/stage2-hook.sh` and surfaced by `detect_install_method()`)
in both update entry points and print a long-form message that
covers:

  - The right command: `docker pull nousresearch/hermes-agent:latest`
  - Restart guidance (`docker compose up -d --force-recreate` /
    re-run `docker run`)
  - How to verify the new version after restart
  - Tag-pinning caveat (`:latest` doesn't move a pinned tag)
  - Config persistence across upgrades (state under `HERMES_HOME` /
    `/opt/data` is bind-mounted and survives)
  - Fork escape hatch (build your own image with the repo's Dockerfile)

Exit code is 1 (matches `managed_error` semantic for "tried to
update but can't update this way").

Plumbing:
  - hermes_cli/config.py: new `format_docker_update_message()` helper
    sits next to the existing `_NIX_UPDATE_MSG` /
    `format_managed_message()` family so the wording lives in one
    place and both call sites (apply path + check path) consume it.
  - hermes_cli/main.py:
      * `cmd_update()`: bail right after the `is_managed()` gate, before
        any of the apply-path branches.
      * `_cmd_update_check()`: bail at the top of the function, before
        the existing `method == "pip"` branch.
    Neither path touches subprocess.run / git when method == "docker".

Coverage:
  - 7 new tests in `tests/hermes_cli/test_cmd_update_docker.py`:
      * `hermes update` in Docker → message + exit 1, no git calls
      * `hermes update --check` (via cmd_update) → same
      * `--yes` / `--force` don't bypass (intentional)
      * `_cmd_update_check` called directly → bails too
      * git/pip installs still take their normal paths (regression guards)
      * `format_docker_update_message` content-lock test pinning the
        five user-actionable bits the message must contain
  - Existing test_cmd_update.py (21 tests) + test_managed_installs.py
    (5 tests) still pass — no regression on the source-install path.
  - Verified end-to-end in a real container: `docker run ... update`
    and `docker run ... update --check` both render the message and
    exit 1.
@github-actions

github-actions Bot commented May 28, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: docker-update-message vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9521 on HEAD, 9520 on base (🆕 +1)

🆕 New issues (1):

Rule Count
unresolved-import 1
First entries
tests/hermes_cli/test_cmd_update_docker.py:22: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`

✅ Fixed issues: none

Unchanged: 5016 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/bug Something isn't working area/docker Docker image, Compose, packaging comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists labels May 28, 2026
The regression-guard test
`test_cmd_update_on_git_install_does_not_print_docker_message` mocked
`is_managed` and `detect_install_method` but not `subprocess.run`, so
once `cmd_update(check=True)` decided this was a git install it shelled
out to a real `git fetch upstream` / `git fetch origin`. On CI runners
the worktree has no `upstream` remote configured and the fetch hung
past the 30s pytest-timeout — test (4) slice failed in #33659 CI.

Fix: stub `subprocess.run` with a successful CompletedProcess-shaped
object whose stdout is `"0\n"`, so:
  - no real git command is ever invoked
  - the rev-list parsing later in the flow (`int(stdout.strip())`)
    succeeds rather than `ValueError`-ing through the test's
    SystemExit catch
  - the flow proceeds far enough to confirm the docker banner is
    absent (the actual assertion)

Also broaden the except clause to `(SystemExit, Exception)`: the only
assertion in this test is the negative-banner check on captured stdout;
any further failure in the rest of the update flow is irrelevant to
that contract.

Verified locally: all 7 tests in
`tests/hermes_cli/test_cmd_update_docker.py` pass in 0.39s (previously
the regression-guard test alone consumed 30s+ and got SIGTERM'd).
@liuhao1024

Copy link
Copy Markdown
Contributor

The check-path fix is solid — detect_install_method returning "docker" now surfaces the docker pull guidance instead of the misleading git error. One gap though: the apply path (cmd_update, ~line 8715) still falls through to the old message.

When .git doesn't exist and the install method is "docker", the apply path hits:

print("✗ Not a git repository. Please reinstall:")
print("  curl -fsSL https://raw.githubusercontent.com/.../install.sh | bash")
sys.exit(1)

That curl | bash instruction installs a host-side Hermes — it doesn't update a running container. A Docker user who runs hermes update (without --check) will get this misleading advice.

Suggested fix — add the same docker guard to cmd_update before the pip check:

if not git_dir.exists():
    if sys.platform == "win32":
        use_zip_update = True
    else:
        from hermes_cli.config import detect_install_method
        method = detect_install_method(PROJECT_ROOT)
        if method == "docker":
            from hermes_cli.config import format_docker_update_message
            print(format_docker_update_message())
            sys.exit(1)
        if method == "pip":
            _cmd_update_pip(args)
            return
        print("✗ Not a git repository. Please reinstall:")
        ...

This reuses the same _DOCKER_UPDATE_MESSAGE constant the PR already defines in config.py, so no new wording is needed.

@benbarclay benbarclay merged commit 875d930 into main May 28, 2026
25 checks passed
@benbarclay benbarclay deleted the docker-update-message branch May 28, 2026 05:50
mathias3 pushed a commit to mathias3/hermes-agent that referenced this pull request May 28, 2026
The regression-guard test
`test_cmd_update_on_git_install_does_not_print_docker_message` mocked
`is_managed` and `detect_install_method` but not `subprocess.run`, so
once `cmd_update(check=True)` decided this was a git install it shelled
out to a real `git fetch upstream` / `git fetch origin`. On CI runners
the worktree has no `upstream` remote configured and the fetch hung
past the 30s pytest-timeout — test (4) slice failed in NousResearch#33659 CI.

Fix: stub `subprocess.run` with a successful CompletedProcess-shaped
object whose stdout is `"0\n"`, so:
  - no real git command is ever invoked
  - the rev-list parsing later in the flow (`int(stdout.strip())`)
    succeeds rather than `ValueError`-ing through the test's
    SystemExit catch
  - the flow proceeds far enough to confirm the docker banner is
    absent (the actual assertion)

Also broaden the except clause to `(SystemExit, Exception)`: the only
assertion in this test is the negative-banner check on captured stdout;
any further failure in the rest of the update flow is irrelevant to
that contract.

Verified locally: all 7 tests in
`tests/hermes_cli/test_cmd_update_docker.py` pass in 0.39s (previously
the regression-guard test alone consumed 30s+ and got SIGTERM'd).
Bryce-huang pushed a commit to wbkunlun/hermes-agent that referenced this pull request May 29, 2026
The regression-guard test
`test_cmd_update_on_git_install_does_not_print_docker_message` mocked
`is_managed` and `detect_install_method` but not `subprocess.run`, so
once `cmd_update(check=True)` decided this was a git install it shelled
out to a real `git fetch upstream` / `git fetch origin`. On CI runners
the worktree has no `upstream` remote configured and the fetch hung
past the 30s pytest-timeout — test (4) slice failed in NousResearch#33659 CI.

Fix: stub `subprocess.run` with a successful CompletedProcess-shaped
object whose stdout is `"0\n"`, so:
  - no real git command is ever invoked
  - the rev-list parsing later in the flow (`int(stdout.strip())`)
    succeeds rather than `ValueError`-ing through the test's
    SystemExit catch
  - the flow proceeds far enough to confirm the docker banner is
    absent (the actual assertion)

Also broaden the except clause to `(SystemExit, Exception)`: the only
assertion in this test is the negative-banner check on captured stdout;
any further failure in the rest of the update flow is irrelevant to
that contract.

Verified locally: all 7 tests in
`tests/hermes_cli/test_cmd_update_docker.py` pass in 0.39s (previously
the regression-guard test alone consumed 30s+ and got SIGTERM'd).

#AI commit#
zwolniony pushed a commit to zwolniony/hermes-agent that referenced this pull request May 29, 2026
The regression-guard test
`test_cmd_update_on_git_install_does_not_print_docker_message` mocked
`is_managed` and `detect_install_method` but not `subprocess.run`, so
once `cmd_update(check=True)` decided this was a git install it shelled
out to a real `git fetch upstream` / `git fetch origin`. On CI runners
the worktree has no `upstream` remote configured and the fetch hung
past the 30s pytest-timeout — test (4) slice failed in NousResearch#33659 CI.

Fix: stub `subprocess.run` with a successful CompletedProcess-shaped
object whose stdout is `"0\n"`, so:
  - no real git command is ever invoked
  - the rev-list parsing later in the flow (`int(stdout.strip())`)
    succeeds rather than `ValueError`-ing through the test's
    SystemExit catch
  - the flow proceeds far enough to confirm the docker banner is
    absent (the actual assertion)

Also broaden the except clause to `(SystemExit, Exception)`: the only
assertion in this test is the negative-banner check on captured stdout;
any further failure in the rest of the update flow is irrelevant to
that contract.

Verified locally: all 7 tests in
`tests/hermes_cli/test_cmd_update_docker.py` pass in 0.39s (previously
the regression-guard test alone consumed 30s+ and got SIGTERM'd).
mosaiq-systems pushed a commit to mosaiq-systems/hermes-agent that referenced this pull request May 29, 2026
The regression-guard test
`test_cmd_update_on_git_install_does_not_print_docker_message` mocked
`is_managed` and `detect_install_method` but not `subprocess.run`, so
once `cmd_update(check=True)` decided this was a git install it shelled
out to a real `git fetch upstream` / `git fetch origin`. On CI runners
the worktree has no `upstream` remote configured and the fetch hung
past the 30s pytest-timeout — test (4) slice failed in NousResearch#33659 CI.

Fix: stub `subprocess.run` with a successful CompletedProcess-shaped
object whose stdout is `"0\n"`, so:
  - no real git command is ever invoked
  - the rev-list parsing later in the flow (`int(stdout.strip())`)
    succeeds rather than `ValueError`-ing through the test's
    SystemExit catch
  - the flow proceeds far enough to confirm the docker banner is
    absent (the actual assertion)

Also broaden the except clause to `(SystemExit, Exception)`: the only
assertion in this test is the negative-banner check on captured stdout;
any further failure in the rest of the update flow is irrelevant to
that contract.

Verified locally: all 7 tests in
`tests/hermes_cli/test_cmd_update_docker.py` pass in 0.39s (previously
the regression-guard test alone consumed 30s+ and got SIGTERM'd).
KKT-OPT pushed a commit to KKT-OPT/hermes-agent that referenced this pull request May 31, 2026
The regression-guard test
`test_cmd_update_on_git_install_does_not_print_docker_message` mocked
`is_managed` and `detect_install_method` but not `subprocess.run`, so
once `cmd_update(check=True)` decided this was a git install it shelled
out to a real `git fetch upstream` / `git fetch origin`. On CI runners
the worktree has no `upstream` remote configured and the fetch hung
past the 30s pytest-timeout — test (4) slice failed in NousResearch#33659 CI.

Fix: stub `subprocess.run` with a successful CompletedProcess-shaped
object whose stdout is `"0\n"`, so:
  - no real git command is ever invoked
  - the rev-list parsing later in the flow (`int(stdout.strip())`)
    succeeds rather than `ValueError`-ing through the test's
    SystemExit catch
  - the flow proceeds far enough to confirm the docker banner is
    absent (the actual assertion)

Also broaden the except clause to `(SystemExit, Exception)`: the only
assertion in this test is the negative-banner check on captured stdout;
any further failure in the rest of the update flow is irrelevant to
that contract.

Verified locally: all 7 tests in
`tests/hermes_cli/test_cmd_update_docker.py` pass in 0.39s (previously
the regression-guard test alone consumed 30s+ and got SIGTERM'd).
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 2026
The regression-guard test
`test_cmd_update_on_git_install_does_not_print_docker_message` mocked
`is_managed` and `detect_install_method` but not `subprocess.run`, so
once `cmd_update(check=True)` decided this was a git install it shelled
out to a real `git fetch upstream` / `git fetch origin`. On CI runners
the worktree has no `upstream` remote configured and the fetch hung
past the 30s pytest-timeout — test (4) slice failed in NousResearch#33659 CI.

Fix: stub `subprocess.run` with a successful CompletedProcess-shaped
object whose stdout is `"0\n"`, so:
  - no real git command is ever invoked
  - the rev-list parsing later in the flow (`int(stdout.strip())`)
    succeeds rather than `ValueError`-ing through the test's
    SystemExit catch
  - the flow proceeds far enough to confirm the docker banner is
    absent (the actual assertion)

Also broaden the except clause to `(SystemExit, Exception)`: the only
assertion in this test is the negative-banner check on captured stdout;
any further failure in the rest of the update flow is irrelevant to
that contract.

Verified locally: all 7 tests in
`tests/hermes_cli/test_cmd_update_docker.py` pass in 0.39s (previously
the regression-guard test alone consumed 30s+ and got SIGTERM'd).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docker Docker image, Compose, packaging comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants