Skip to content

fix(agent): inject process HERMES_HOME into subprocess env when context override unset#38308

Open
spiky02plateau wants to merge 1 commit into
NousResearch:mainfrom
spiky02plateau:fix/central-hermes-home-inject
Open

fix(agent): inject process HERMES_HOME into subprocess env when context override unset#38308
spiky02plateau wants to merge 1 commit into
NousResearch:mainfrom
spiky02plateau:fix/central-hermes-home-inject

Conversation

@spiky02plateau

Copy link
Copy Markdown

What does this PR do?

Subprocesses spawned under a profile-scoped gateway lose their HERMES_HOME, so the child resolves get_hermes_home() to the default ~/.hermes and reads the wrong profile's config/auth/memory — the cross-profile data-corruption / isolation break tracked in #4707.

Both subprocess env builders in tools/environments/local.py_sanitize_subprocess_env (background + PTY spawns) and _make_run_env — pin HOME to the profile's home/ dir, but inject HERMES_HOME only from the _HERMES_HOME_OVERRIDE ContextVar via _inject_context_hermes_home(). That ContextVar is unset for the common background/PTY/cron spawns, so the child ends up with HOME={profile}/home and no HERMES_HOME. _sanitize_subprocess_env makes this worse because it builds from base_env or {} (not os.environ), so even the parent's own HERMES_HOME doesn't carry through.

This fixes it centrally, at the one shared seam both builders already call, rather than adding yet another per-spawner pin. A single os.getenv("HERMES_HOME") fallback in _inject_context_hermes_home() covers the common single-profile-gateway case while preserving ContextVar precedence:

value = get_hermes_home_override() or os.getenv("HERMES_HOME")

Why this shape (vs. another per-spawner fix)

The repo already carries several narrow, per-call-site HERMES_HOME/HOME propagation fixes (e.g. #4729, #15330, #36742, #25151, #27260), and #4707 keeps re-surfacing because each new spawner re-opens the same class of bug. _inject_context_hermes_home() is the single seam both env builders funnel through, so fixing it here closes the current spawners and future ones in one place — and subsumes the narrower open PRs against #4707.

Security / isolation

This is the secure direction, not a leak:

  • Only one key is touched — HERMES_HOME, a non-secret path var — inside the helper whose sole job is already to inject that one key. The secret-stripping machinery (_HERMES_PROVIDER_ENV_BLOCKLIST, is_env_passthrough, the _HERMES_FORCE_ allowlist) and the base_env or {} build are untouched — zero new secret-exposure surface.
  • ContextVar keeps precedence (get_hermes_home_override() or …), so multi-profile-per-process sessions that mutate the home per session (fix(agent): prevent silent tool result loss during context compression #1976) still get the right value; os.getenv is only the fallback when the ContextVar was never set.
  • The unpatched state is the actual isolation hole: a profile subprocess silently reading the default profile's auth.json/config/memory. Pinning the correct HERMES_HOME shrinks blast radius — a compromised/injected profile-scoped skill stays in its own profile instead of reaching the default profile.

One honest edge: multi-profile-in-one-process with the ContextVar unsetos.getenv returns the process's startup HERMES_HOME. But the override still wins when set, so this only hits the unset case, and even then it's strictly no worse than today (today injects nothing → default fallback). Happy to instead pin directly in _sanitize_subprocess_env if maintainers prefer keeping the helper ContextVar-only.

Related Issue

Fixes #4707

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • 🔒 Security fix

Changes Made

How to Test

  1. The new _sanitize_subprocess_env test fails on main (KeyError: 'HERMES_HOME') and passes with this change — that delta is the bug and the fix.
  2. uv run pytest tests/test_subprocess_home_isolation.py tests/tools/test_local_env_blocklist.py -q → all pass (40).
  3. uv run ruff check tools/environments/local.py tests/test_subprocess_home_isolation.py → clean.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(agent): …)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix
  • I've run the tests above and they pass
  • I've added tests for my changes
  • I've tested on my platform: macOS 15 (Apple Silicon)

Documentation & Housekeeping

  • No docs/config/architecture changes needed — N/A
  • Cross-platform: pure env-var logic, no platform-specific paths — N/A

…xt override unset

Subprocess env builders (_sanitize_subprocess_env, _make_run_env) pin
HOME to the profile's home/ dir but only inject HERMES_HOME from the
_HERMES_HOME_OVERRIDE ContextVar, which is unset for background/PTY/cron
spawns. The child then has HOME={profile}/home but no HERMES_HOME, so
get_hermes_home() falls back to ~/.hermes and reads the default profile's
config/auth/memory instead of its own — cross-profile data corruption.

Add a single os.getenv("HERMES_HOME") fallback in the shared
_inject_context_hermes_home() so the common single-profile-gateway case
is covered. The ContextVar keeps precedence (per-session profile
mutation, NousResearch#1976); only one key (HERMES_HOME, a non-secret path) is
touched, so the secret-isolation invariant is intact.

Fixes NousResearch#4707

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent loop, run_agent.py, prompt builder P2 Medium — degraded but workaround exists labels Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent loop, run_agent.py, prompt builder 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.

[Bug]: cron under profile-scoped launchd gateway falls back to default ~/.hermes instead of profile HERMES_HOME

2 participants