Skip to content

Desktop serve process leaks root .env credentials to all profile sessions (OpenViking cross-profile credential leakage) #82117

Description

@bennybuoy

Bug Description

When the Hermes desktop app runs hermes serve --isolated, the process loads the global root ~/.hermes/.env into os.environ once at startup (via load_hermes_dotenv() in hermes_cli/main.py:699). When a user opens a chat for a different profile (e.g. vish), the gateway's per-turn handler sets a HERMES_HOME contextvar via set_hermes_home_override() — but it does not reload that profile's .env into os.environ. Any plugin that reads os.environ directly picks up the root .env's credentials regardless of which profile the session is scoped to.

This causes cross-profile credential leakage: a vish session authenticates to OpenViking as default/default-agent (from the root .env) instead of vish/vish-agent (from /home/ben/.hermes/profiles/vish/.env). The auto-recall context then injects memories from the default-agent namespace — including another profile's soul/identity files — into the wrong session.

Steps to Reproduce

  1. Configure the desktop app with multiple profiles, each with its own .env containing different OPENVIKING_ACCOUNT / OPENVIKING_USER values
  2. Put OPENVIKING_ACCOUNT=default, OPENVIKING_USER=default-agent in the root ~/.hermes/.env
  3. Put OPENVIKING_ACCOUNT=vish, OPENVIKING_USER=vish-agent in ~/.hermes/profiles/vish/.env
  4. Open a chat in the desktop app as the vish profile
  5. Observe the auto-recall context injects memories from viking://user/default-agent/memories/ instead of viking://user/vish-agent/memories/

Expected Behavior

The OpenViking plugin should authenticate using the profile-scoped .env credentials (vish/vish-agent), not the root .env credentials (default/default-agent).

Actual Behavior

The plugin authenticates as default/default-agent because _resolve_connection_settings() reads os.environ directly, and os.environ was populated from the root .env at serve process startup and never updated for the active profile.

Root Cause

The OpenViking plugin's _resolve_connection_settings() (in plugins/memory/openviking/__init__.py) reads OPENVIKING_* vars from os.environ via _env_value(). The serve process loads the root .env into os.environ at startup. The per-session profile scoping (set_hermes_home_override()) sets a contextvar that get_hermes_home() respects, but os.environ is process-global and never updated per-profile.

Key code paths:

  • hermes_cli/main.py:699load_hermes_dotenv() loads root .env into os.environ at startup
  • hermes_cli/web_server.py:13654_profile_scope() sets HERMES_HOME contextvar but does NOT reload .env
  • tui_gateway/server.py:9560set_hermes_home_override(profile_home) sets contextvar per-turn
  • plugins/memory/openviking/__init__.py:1078_env_value() reads os.environ directly
  • plugins/memory/openviking/__init__.py:1089_resolve_connection_settings() calls _env_value() for all OPENVIKING_* vars

Why the fix can't be in _profile_scope

The code explicitly warns (web_server.py:3030-3033) that _profile_scope "swaps process-global" state and must not be held across awaits. Reloading .env into os.environ in _profile_scope would race with concurrent sessions on different profiles — os.environ is process-global, not thread-local.

Proposed Fix

Resolve OPENVIKING_* vars from the profile's .env file directly, using get_hermes_home() (which respects the contextvar) before falling back to os.environ. This is:

  • Thread-safe: reads the .env file on each call, never mutates os.environ
  • Backwards-compatible: falls back to os.environ when the profile .env doesn't define a key
  • Cached: lru_cache keyed by path+mtime so .env edits are picked up without a process restart
  • No impact on other plugins: only changes the OpenViking plugin's credential resolution
  • No impact on systemd gateway processes: those already have the correct .env in os.environ (loaded at import time with correct HERMES_HOME)

The fix adds a _profile_env_value() function that uses get_hermes_home() to find the profile's .env, parses it with dotenv_values() (read-only, no os.environ mutation), and returns the value if present — falling back to _env_value() (os.environ) if not.

Verification

Tested by simulating the serve process environment:

  • Set os.environ to root .env values (default/default-agent)
  • Set HERMES_HOME contextvar to /home/ben/.hermes/profiles/vish
  • Verified _profile_env_value("OPENVIKING_ACCOUNT") returns vish (from profile .env)
  • Verified _env_value("OPENVIKING_ACCOUNT") returns default (from os.environ)
  • After clearing the contextvar, _profile_env_value falls back to default (from os.environ)

Dogfooded on a live system: after restarting the serve process, the auto-recall context switched from viking://user/default-agent/memories/ to viking://user/vish-agent/memories/.

Environment

  • Hermes Agent: main branch (post v2026.8.3)
  • Desktop app (Electron) with hermes serve --isolated
  • Multiple profiles configured with individual .env files
  • OpenViking memory provider configured for all profiles

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existsarea/authAuthentication, OAuth, credential poolsarea/profilesMulti-profile isolation, HERMES_HOME scopingcomp/cliCLI entry point, hermes_cli/, setup wizardcomp/desktopElectron desktop app (apps/desktop/*)comp/gatewayGateway runner, session dispatch, deliverycomp/pluginsPlugin system and bundled pluginssweeper:risk-compatibilitySweeper risk: may break existing users, config, migrations, defaults, or upgradessweeper:risk-security-boundarySweeper risk: may affect sandboxing, auth, credentials, or sensitive datasweeper:risk-session-stateSweeper risk: may lose/corrupt/mis-associate session or context statetool/memoryMemory tool and memory providerstype/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions