Bug Description
On macOS, a profile-scoped gateway service can be installed as a profile-specific launchd agent (for example ai.hermes.gateway-<profile>.plist) with EnvironmentVariables -> HERMES_HOME=~/.hermes/profiles/<profile>.
However, live cron executions triggered through that gateway can still resolve Hermes home as the default ~/.hermes instead of the profile-scoped Hermes home.
This causes cron jobs to observe configuration/runtime values from the default home rather than the intended profile, even though the active profile and launchd plist both point at the named profile.
Additional Context
This was discovered while validating cron-based Telegram delivery in a profile-scoped deployment.
There is a separate, already-known cron MEDIA delivery issue/fix path:
That fix addresses raw MEDIA:/... text delivery, but it does not explain the profile/home mismatch above.
Related issues:
Steps to Reproduce
- Create a named profile, e.g.
<profile>.
- Ensure:
~/.hermes/active_profile contains that profile name
~/Library/LaunchAgents/ai.hermes.gateway-<profile>.plist exists
- that plist contains
HERMES_HOME=~/.hermes/profiles/<profile>
- Start the profile-scoped gateway via launchd.
- Create and run a one-shot cron job that prints:
HOME
HERMES_HOME
hermes_constants.get_hermes_home()
- whether default config exists at
~/.hermes/config.yaml
- whether profile config exists at
~/.hermes/profiles/<profile>/config.yaml
- Observe the cron output.
Expected Behavior
Cron jobs running under the profile-scoped gateway should inherit the same effective Hermes home as the gateway service:
get_hermes_home() should resolve to ~/.hermes/profiles/<profile>
- config resolution should use the named profile
- cron should therefore see the same model/provider/terminal/gateway settings as the active profile-scoped runtime
Actual Behavior
Observed live facts from cron runs in a named-profile setup:
HOME resolves to the operator user home
HERMES_HOME=
get_hermes_home()=~/.hermes
- default config exists
- named profile config also exists
- active profile points to the named profile
- launchd plist HERMES_HOME points to
~/.hermes/profiles/<profile>
So the cron runtime falls back to the default Hermes home despite the profile-scoped launchd service being present and active.
Affected Component
Configuration (config.yaml, .env, hermes setup)
Messaging Platform (if gateway-related)
Telegram
Operating System
macOS
Python Version
3.11.x
Hermes Version
2.x
Relevant Logs / Traceback
**Relevant Logs / Traceback**
No single Python traceback was consistently surfaced to the chat. The most relevant observed runtime facts were:
HERMES_HOME=
GET_HERMES_HOME=~/.hermes
WRAP_RESPONSE=True
ACTIVE_PROFILE_VALUE=<profile>
PLIST_HERMES_HOME=~/.hermes/profiles/<profile>
Sanitized host-side facts:
- operator user home is the active `HOME`
- `ACTIVE_PROFILE_VALUE=<profile>`
- `PLIST_EXISTS=yes`
- `PLIST_HERMES_HOME=~/.hermes/profiles/<profile>`
- profile-scoped `launchctl print` succeeds
- separate cron facts run reported:
- `HERMES_HOME=`
- `GET_HERMES_HOME=~/.hermes`
- `WRAP_RESPONSE=True`
Root Cause Analysis (optional)
This appears to be a stale or incorrectly scoped HERMES_HOME resolution inside the cron subsystem, not just a user/config misunderstanding.
Most relevant code paths:
-
hermes_constants.get_hermes_home()
- single source of truth:
return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))
- if
HERMES_HOME is missing, Hermes falls back to the default home.
-
hermes_cli.main._apply_profile_override()
- correctly sets
HERMES_HOME early for CLI -p/--profile runs.
- this part of profile selection looks correct.
-
hermes_cli.gateway.generate_launchd_plist()
- generates a profile-scoped launchd plist with:
EnvironmentVariables -> HERMES_HOME=~/.hermes/profiles/<profile>
- so the service definition itself appears correct.
-
cron/scheduler.py
- module-level initialization captures Hermes home once:
- line 47:
_hermes_home = get_hermes_home()
- later job execution uses that cached module-global value:
- lines 352-354: loads dotenv from
_hermes_home / ".env"
- lines 369-372: loads config from
_hermes_home / "config.yaml"
- lines 394-396: resolves relative prefill files under
_hermes_home
-
cron/jobs.py
- also caches Hermes home at import time:
- line 34:
HERMES_DIR = get_hermes_home()
- then derives cron storage paths (
jobs.json, output dir) from that cached value.
Why this is likely the bug:
- The live launchd/profile evidence says the profile-scoped service exists and embeds the profile-specific
HERMES_HOME.
- But the observed cron run still resolved
get_hermes_home() to the default ~/.hermes.
- The cron subsystem relies on module-level cached home/path globals in both
cron/scheduler.py and cron/jobs.py.
- If those modules are imported before the intended profile environment is fully in effect, or are imported/reused in a context that does not have the profile-scoped
HERMES_HOME, cron will continue using stale default-home paths for dotenv/config/job storage.
In other words:
- profile selection logic is dynamic,
- but cron path resolution is partially frozen at import time.
That mismatch is the most plausible root cause of why a profile-scoped gateway can still produce cron runs that behave as if they are attached to the default Hermes home.
Separate but related issue:
Proposed Fix (optional)
No response
Are you willing to submit a PR for this?
Bug Description
On macOS, a profile-scoped gateway service can be installed as a profile-specific launchd agent (for example
ai.hermes.gateway-<profile>.plist) withEnvironmentVariables -> HERMES_HOME=~/.hermes/profiles/<profile>.However, live cron executions triggered through that gateway can still resolve Hermes home as the default
~/.hermesinstead of the profile-scoped Hermes home.This causes cron jobs to observe configuration/runtime values from the default home rather than the intended profile, even though the active profile and launchd plist both point at the named profile.
Additional Context
This was discovered while validating cron-based Telegram delivery in a profile-scoped deployment.
There is a separate, already-known cron MEDIA delivery issue/fix path:
fix: deliver cron MEDIA tags as native attachmentsThat fix addresses raw
MEDIA:/...text delivery, but it does not explain the profile/home mismatch above.Related issues:
Steps to Reproduce
<profile>.~/.hermes/active_profilecontains that profile name~/Library/LaunchAgents/ai.hermes.gateway-<profile>.plistexistsHERMES_HOME=~/.hermes/profiles/<profile>HOMEHERMES_HOMEhermes_constants.get_hermes_home()~/.hermes/config.yaml~/.hermes/profiles/<profile>/config.yamlExpected Behavior
Cron jobs running under the profile-scoped gateway should inherit the same effective Hermes home as the gateway service:
get_hermes_home()should resolve to~/.hermes/profiles/<profile>Actual Behavior
Observed live facts from cron runs in a named-profile setup:
HOMEresolves to the operator user homeHERMES_HOME=get_hermes_home()=~/.hermes~/.hermes/profiles/<profile>So the cron runtime falls back to the default Hermes home despite the profile-scoped launchd service being present and active.
Affected Component
Configuration (config.yaml, .env, hermes setup)
Messaging Platform (if gateway-related)
Telegram
Operating System
macOS
Python Version
3.11.x
Hermes Version
2.x
Relevant Logs / Traceback
Root Cause Analysis (optional)
This appears to be a stale or incorrectly scoped
HERMES_HOMEresolution inside the cron subsystem, not just a user/config misunderstanding.Most relevant code paths:
hermes_constants.get_hermes_home()return Path(os.getenv("HERMES_HOME", Path.home() / ".hermes"))HERMES_HOMEis missing, Hermes falls back to the default home.hermes_cli.main._apply_profile_override()HERMES_HOMEearly for CLI-p/--profileruns.hermes_cli.gateway.generate_launchd_plist()EnvironmentVariables -> HERMES_HOME=~/.hermes/profiles/<profile>cron/scheduler.py_hermes_home = get_hermes_home()_hermes_home / ".env"_hermes_home / "config.yaml"_hermes_homecron/jobs.pyHERMES_DIR = get_hermes_home()jobs.json, output dir) from that cached value.Why this is likely the bug:
HERMES_HOME.get_hermes_home()to the default~/.hermes.cron/scheduler.pyandcron/jobs.py.HERMES_HOME, cron will continue using stale default-home paths for dotenv/config/job storage.In other words:
That mismatch is the most plausible root cause of why a profile-scoped gateway can still produce cron runs that behave as if they are attached to the default Hermes home.
Separate but related issue:
MEDIA:/...text delivery, but not the profile/home mismatch.Proposed Fix (optional)
No response
Are you willing to submit a PR for this?