Skip to content

fix(constants): display_hermes_home returns absolute path when HERMES_HOME == HOME - #21929

Closed
li0near wants to merge 1 commit into
NousResearch:mainfrom
li0near:fix/display-hermes-home-confusing-shorthand
Closed

fix(constants): display_hermes_home returns absolute path when HERMES_HOME == HOME#21929
li0near wants to merge 1 commit into
NousResearch:mainfrom
li0near:fix/display-hermes-home-confusing-shorthand

Conversation

@li0near

@li0near li0near commented May 8, 2026

Copy link
Copy Markdown
Contributor

Symptom

Tool descriptions, log messages, and prompts that embed display_hermes_home() rendered strings like:

Relative paths resolve under ~/./scripts/.

in some Docker setups. The ~ is the problem. ~ universally means the current reader's $HOME — that's its only meaning across shells, libraries, and the broader ecosystem. Whoever expands the shorthand — a human running a shell command, an LLM agent placing a file — looks up $HOME in their own environment and resolves the path against that. They have no way to know it was meant to point at HERMES_HOME instead.

In LLM-driven workflows that mismatch causes silent miscompilation: the agent, seeing ~/./scripts/ in a tool description, reads it as $HOME/scripts/ (or worse, pattern-matches it against the conventional ~/.hermes/scripts/ path) and writes script files there. The cron scheduler, which resolves script paths against HERMES_HOME/scripts/, then can't find them at execution time.

Root cause

The official Docker image sets the unix user's home directory equal to HERMES_HOME:

  • Dockerfile:21RUN useradd -u 10000 -m -d /opt/data hermes
  • Dockerfile:86ENV HERMES_HOME=/opt/data

So inside the agent process, Path.home() and get_hermes_home() both resolve to /opt/data. The old implementation:

home = get_hermes_home()
try:
    return "~/" + str(home.relative_to(Path.home()))
except ValueError:
    return str(home)

evaluates Path('/opt/data').relative_to(Path('/opt/data'))Path('.'), producing the shorthand ~/.. That gets concatenated into the cronjob tool description as ~/./scripts/.

The deeper issue: the ~/ shorthand is only safe when the reader can resolve it back to the same absolute path. That requires Path.home() (the reader's $HOME) to be the prefix of get_hermes_home(). When they're equal, there's nothing left to shorten, and the resulting string is meaningless to anyone whose $HOME doesn't happen to match the writer's.

Fix

When home == Path.home() the shorthand cannot produce a useful string, so fall back to the absolute path:

if home == Path.home():
    return str(home)

In the broken Docker layout this returns /opt/data instead of ~/.. The cronjob tool description renders as:

Relative paths resolve under /opt/data/scripts/.

Unambiguous to any reader.

What's unchanged

  • Standard installs (HERMES_HOME=~/.hermes) still render ~/.hermes.
  • Profile-mode installs (HERMES_HOME=~/.hermes/profiles/coder) still render ~/.hermes/profiles/coder.
  • Custom paths outside the user's home (e.g. HERMES_HOME=/opt/hermes-custom) were already taking the ValueError branch and returning the absolute form — still do.

Tests

Two tests in tests/test_hermes_constants.py:

  1. test_default_install_uses_shorthand — the happy path; ~/.hermes rendering still works.
  2. test_hermes_home_equals_user_home_returns_absolute — the regression. Fails on unpatched code with the literal ~/. output, passes on patched code.

The profile-install and custom-path-outside-home cases I had earlier exercised the same relative_to / ValueError branches as #1 — I dropped them as redundant.

Test results

Regression test fails on unpatched upstream/main:

$ git checkout upstream/main -- hermes_constants.py
$ pytest tests/test_hermes_constants.py::TestDisplayHermesHome -v

FAILED test_hermes_home_equals_user_home_returns_absolute
  AssertionError:
  - /tmp/pytest-of-hermes/.../test_hermes_home_equals_user_h0
  + ~/.

Proves the test catches the bug.

With the patch — full tests/test_hermes_constants.py passes:

$ pytest tests/test_hermes_constants.py -q
36 passed in 5.54s

Existing callers continue to pass:

$ pytest tests/cli/test_cli_status_command.py tests/cli/test_personality_none.py \
         tests/skills/test_google_oauth_setup.py tests/gateway/test_session.py \
         tests/hermes_cli/test_memory_reset.py -q
126 passed

End-to-end: verified inside the official Docker container that the cronjob tool description previously rendered as Relative paths resolve under ~/./scripts/. After this fix it renders as Relative paths resolve under /opt/data/scripts/.

Risk

Low. The new branch only fires when home == Path.home() exactly — a condition that already produced broken output. The existing relative_to and ValueError branches are untouched.

@li0near
li0near force-pushed the fix/display-hermes-home-confusing-shorthand branch from 1e71a92 to 88803d4 Compare May 8, 2026 15:27
…_HOME == HOME

The ~/ shorthand is only meaningful if the reader can resolve it.
`~` universally means "$HOME for the current user". Whoever sees a
description like `Relative paths resolve under ~/./scripts/.` —
human or LLM — will expand `~` against their own $HOME, not against
HERMES_HOME, and look for files in the wrong place.

That mismatch fires in the official Docker image, where:

  Dockerfile:21  RUN useradd -u 10000 -m -d /opt/data hermes
  Dockerfile:86  ENV HERMES_HOME=/opt/data

set the unix user's home directory equal to HERMES_HOME. Inside the
agent process, `Path.home()` and `get_hermes_home()` then resolve
to the same path. The old implementation:

    home.relative_to(Path.home())        # = Path('.')
    return "~/" + str(...)              # = "~/."

produced `~/.`, which concatenated into the cronjob tool description
as `~/./scripts/`. Agents reading that string place script files
under $HOME (`/opt/data/home`) instead of HERMES_HOME (`/opt/data`),
and the cron scheduler — which resolves script paths against
`HERMES_HOME/scripts/` — fails to find them.

Fix: when `home == Path.home()` the shorthand has nothing to shorten,
so return the absolute path. Standard installs (HERMES_HOME under
HOME) and custom paths outside HOME are unchanged.

Tests:
- test_default_install_uses_shorthand — happy path, ~/.hermes form.
- test_hermes_home_equals_user_home_returns_absolute — regression
  guard. Fails on unpatched code with the literal '~/.' output.
@li0near
li0near force-pushed the fix/display-hermes-home-confusing-shorthand branch from 88803d4 to 0f816e8 Compare May 11, 2026 03:58
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint area/docker Docker image, Compose, packaging labels May 11, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for identifying a current Docker-path defect. The canonical implementation still returns "~/." for equal paths at hermes_constants.py:644-648, and the cron schema appends /scripts/ at tools/cronjob_tools.py:1042, so the reported ~/./scripts/ output remains reproducible by inspection.

Problems

  • Two fallback implementations preserve the same bug when hermes_constants is unavailable: plugins/platforms/google_chat/oauth.py:87-92 and skills/productivity/google-workspace/scripts/_hermes_home.py:34-42. The latter explicitly says it mirrors the canonical helper contract at lines 5-11.

Suggested changes

  • Apply the equality guard to both fallback helpers as well, so all advertised display_hermes_home() implementations agree.

This is an automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 13, 2026
@li0near li0near closed this by deleting the head repository Aug 22, 2026
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/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants