fix(constants): raise ValueError when HERMES_HOME unset in profile mode#18600
fix(constants): raise ValueError when HERMES_HOME unset in profile mode#18600liuhao1024 wants to merge 1 commit into
Conversation
When a non-default profile is active (active_profile file exists) but HERMES_HOME is not inherited by a sub-process, get_hermes_home() now raises ValueError instead of silently returning ~/.hermes. This prevents cross-profile data corruption where cron jobs, skills, and sub-agents write to the wrong profile's database. Classic mode (no active_profile or 'default') is unaffected — the fallback to ~/.hermes remains the correct behavior there. Closes NousResearch#18594
…active profile (#18746) When HERMES_HOME is unset but ~/.hermes/active_profile names a non-default profile, any data this process writes lands in the default profile — not the one the operator expects. Before this change the fallback was silent, so cross-profile contamination (#18594) was invisible until a user noticed their memory/state ended up in the wrong place. Now we emit a one-shot warning to stderr the first time this happens in a process. No raise — there are 30+ module-level callers of get_hermes_home() and raising from any of them would brick import. Behavior is otherwise unchanged; subprocess spawners (systemd template, kanban dispatcher, docker entrypoint) already propagate HERMES_HOME correctly. Bypasses logging.getLogger() because this runs before logging is configured in a significant fraction of callers (module import time). Refs #18594. Credit to @liuhao1024 for surfacing the silent-fallback case in PR #18600; we kept the diagnostic signal without the import-time raise.
|
Thanks for catching this silent-fallback case — closed in favor of #18746 which takes a narrower approach. Your PR raised #18746 keeps the diagnostic signal you identified as valuable (surfacing cross-profile fallback) by writing a one-shot warning to stderr, without the import-time raise. You're credited in the commit message. |
Community PRs applied: - NousResearch#18596: Enable secret redaction by default (SECURITY) - NousResearch#18650: Sanitize malformed tool messages + auto-recover on API 400 - NousResearch#18607: Emergency compression before max_iterations exhaustion - NousResearch#18603: Compression fallback to main model on 413 rate limit - NousResearch#18638: Pass threshold_percent on model switch - NousResearch#18663: Strip extra_content from tool_calls for strict APIs - NousResearch#18618: Forward explicit_api_key to OpenRouter - NousResearch#18632: Show cache tokens in /insights breakdown - NousResearch#18614: Add idempotency guard for patch duplicate loops - NousResearch#18600: Raise ValueError when HERMES_HOME unset in profile mode - NousResearch#18616: Allow ZWJ emoji in context files - NousResearch#18582: Reload .env on /restart - NousResearch#18547: Stabilize system prompt prefix for KV cache reuse - NousResearch#18692: Strip FTS5 operators from session search truncation terms Fix: Add order_by_last_active=True to list_sessions_rich call (pre-existing commit 142b4bf code sync)
…active profile (NousResearch#18746) When HERMES_HOME is unset but ~/.hermes/active_profile names a non-default profile, any data this process writes lands in the default profile — not the one the operator expects. Before this change the fallback was silent, so cross-profile contamination (NousResearch#18594) was invisible until a user noticed their memory/state ended up in the wrong place. Now we emit a one-shot warning to stderr the first time this happens in a process. No raise — there are 30+ module-level callers of get_hermes_home() and raising from any of them would brick import. Behavior is otherwise unchanged; subprocess spawners (systemd template, kanban dispatcher, docker entrypoint) already propagate HERMES_HOME correctly. Bypasses logging.getLogger() because this runs before logging is configured in a significant fraction of callers (module import time). Refs NousResearch#18594. Credit to @liuhao1024 for surfacing the silent-fallback case in PR NousResearch#18600; we kept the diagnostic signal without the import-time raise.
…active profile (NousResearch#18746) When HERMES_HOME is unset but ~/.hermes/active_profile names a non-default profile, any data this process writes lands in the default profile — not the one the operator expects. Before this change the fallback was silent, so cross-profile contamination (NousResearch#18594) was invisible until a user noticed their memory/state ended up in the wrong place. Now we emit a one-shot warning to stderr the first time this happens in a process. No raise — there are 30+ module-level callers of get_hermes_home() and raising from any of them would brick import. Behavior is otherwise unchanged; subprocess spawners (systemd template, kanban dispatcher, docker entrypoint) already propagate HERMES_HOME correctly. Bypasses logging.getLogger() because this runs before logging is configured in a significant fraction of callers (module import time). Refs NousResearch#18594. Credit to @liuhao1024 for surfacing the silent-fallback case in PR NousResearch#18600; we kept the diagnostic signal without the import-time raise.
…active profile (NousResearch#18746) When HERMES_HOME is unset but ~/.hermes/active_profile names a non-default profile, any data this process writes lands in the default profile — not the one the operator expects. Before this change the fallback was silent, so cross-profile contamination (NousResearch#18594) was invisible until a user noticed their memory/state ended up in the wrong place. Now we emit a one-shot warning to stderr the first time this happens in a process. No raise — there are 30+ module-level callers of get_hermes_home() and raising from any of them would brick import. Behavior is otherwise unchanged; subprocess spawners (systemd template, kanban dispatcher, docker entrypoint) already propagate HERMES_HOME correctly. Bypasses logging.getLogger() because this runs before logging is configured in a significant fraction of callers (module import time). Refs NousResearch#18594. Credit to @liuhao1024 for surfacing the silent-fallback case in PR NousResearch#18600; we kept the diagnostic signal without the import-time raise.
…active profile (NousResearch#18746) When HERMES_HOME is unset but ~/.hermes/active_profile names a non-default profile, any data this process writes lands in the default profile — not the one the operator expects. Before this change the fallback was silent, so cross-profile contamination (NousResearch#18594) was invisible until a user noticed their memory/state ended up in the wrong place. Now we emit a one-shot warning to stderr the first time this happens in a process. No raise — there are 30+ module-level callers of get_hermes_home() and raising from any of them would brick import. Behavior is otherwise unchanged; subprocess spawners (systemd template, kanban dispatcher, docker entrypoint) already propagate HERMES_HOME correctly. Bypasses logging.getLogger() because this runs before logging is configured in a significant fraction of callers (module import time). Refs NousResearch#18594. Credit to @liuhao1024 for surfacing the silent-fallback case in PR NousResearch#18600; we kept the diagnostic signal without the import-time raise.
…active profile (NousResearch#18746) When HERMES_HOME is unset but ~/.hermes/active_profile names a non-default profile, any data this process writes lands in the default profile — not the one the operator expects. Before this change the fallback was silent, so cross-profile contamination (NousResearch#18594) was invisible until a user noticed their memory/state ended up in the wrong place. Now we emit a one-shot warning to stderr the first time this happens in a process. No raise — there are 30+ module-level callers of get_hermes_home() and raising from any of them would brick import. Behavior is otherwise unchanged; subprocess spawners (systemd template, kanban dispatcher, docker entrypoint) already propagate HERMES_HOME correctly. Bypasses logging.getLogger() because this runs before logging is configured in a significant fraction of callers (module import time). Refs NousResearch#18594. Credit to @liuhao1024 for surfacing the silent-fallback case in PR NousResearch#18600; we kept the diagnostic signal without the import-time raise.
Summary
When a non-default profile is active (
~/.hermes/active_profileexists with a non-defaultvalue) butHERMES_HOMEis not inherited by a sub-process,get_hermes_home()now raisesValueErrorinstead of silently returning~/.hermes.Problem: In profile mode, the CLI launcher sets
HERMES_HOME=<root>/profiles/<name>before spawning sub-processes (cron jobs, skills, sub-agents). These sub-processes do not inherit the parent's environment variables. Whenget_hermes_home()is called withoutHERMES_HOME, it falls back to~/.hermes— the default profile — causing silent cross-profile data corruption.Real incident (May 1 2026): A holographic-memory cron job ran SQL queries against
~/.hermes/memory_store.dbinstead of the profile's database becauseHERMES_HOMEwas not inherited, contaminating the default profile with personal data.Fix
Reads
~/.hermes/active_profilewhenHERMES_HOMEis unset. If a non-default profile is active, raisesValueErrorwith a clear message directing the operator to setHERMES_HOME. Classic mode (noactive_profilefile ordefaultvalue) is unaffected — the fallback to~/.hermesremains correct.Test Plan
test_profile_mode_raises_when_hermes_home_unsetfails on current main (DID NOT RAISE)tests/hermes_cli/test_profiles.py— 94 passedtests/test_subprocess_home_isolation.py— all passedhermes_cli/test suite — 0 new failures vs mainDiff Stats
hermes_constants.py: +23 lines (guard clause inget_hermes_home())tests/hermes_cli/test_get_hermes_home_profile_fallback.py: +71 lines (5 test cases)Closes #18594