You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hermes should treat cron execution ownership as part of the scheduler contract, not as an incidental race between ticker processes. Currently, a Desktop-spawned dashboard cron ticker can execute a profile’s cron job instead of the launchd-owned gateway ticker, which breaks macOS TCC / Full Disk Access provenance for jobs that need protected local data.
The problem has three parts:
Multiple processes can tick the same profile cron state. In this incident, both the launchd-owned gateway and Desktop-spawned dashboard backends were able to operate on the same profile’s cron/jobs.json and cron/.tick.lock.
The shared lock only provides at-most-once execution. It prevents duplicate execution at a single tick, but it does not guarantee that the authorised process chain executes the job. Whichever scheduler-capable process wins the lock runs the job.
On macOS, that distinction matters. TCC / Full Disk Access permissions depend on process ancestry. The same cron job had access to protected local data when run by the launchd gateway process chain, but lacked that access when a Desktop dashboard backend won the tick.
This occurred with a named profile (la), but the issue is not specific to that profile name. The general bug is that multiple scheduler-capable processes can execute jobs for the same profile without an explicit, enforceable scheduler owner.
Steps to Reproduce
On macOS, run a profile with a launchd-managed gateway, for example:
$HOME/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main --profile la gateway run --replace
Ensure a cron job exists for the same profile. In this case it was an hourly job stored in:
$HOME/.hermes/profiles/la/cron/jobs.json
Open/restart Hermes Desktop in a way that leaves Desktop-spawned dashboard backends alive for the same profile. The dashboard processes observed here looked like:
Observe that the cron run may be executed by a Desktop dashboard backend instead of the launchd gateway, depending on which ticker wins cron/.tick.lock.
Expected Behavior
For a profile with a launchd-managed gateway, cron execution should be owned by the authorised gateway process chain, or by a single explicit scheduler owner for that profile.
Dashboard/Desktop should be able to create, edit, pause, and trigger jobs, but actual execution should either be delegated to the gateway or refused with a clear warning when another scheduler owner is active.
At-most-once execution is not enough on macOS. The system needs deterministic execution provenance for jobs that access TCC/FDA-protected data.
Actual Behavior
A Desktop-spawned dashboard backend sometimes won the cron lock and executed the hourly job. That job then lacked the Full Disk Access provenance required to read protected local data.
The relevant runtime binaries already had Full Disk Access when reached from the launchd gateway path. The failure was caused by the job being executed from a different process ancestry.
Observed failing ancestry included dashboard PIDs such as 45291 on port 9120:
That succeeding run returned the expected protected local data.
Affected Component
Gateway (Telegram/Discord/Slack/WhatsApp)
Messaging Platform (if gateway-related)
No response
Debug Report
Key local diagnostics from the incident:
- OS: macOS 26.5.1 (25F80)
- Hermes: v0.16.0 (2026.6.5), upstream acd7932c
- Python: 3.11.15
- Expected cron owner: `XPC_SERVICE_NAME=ai.hermes.gateway-la`, PID `20812`
- Desktop dashboard cron owner observed: `HERMES_DESKTOP=1`, `XPC_SERVICE_NAME=application.com.nousresearch.hermes...`, ports `9120`, `9122`, `9123`, `9124`
- Failure: the job lacked the gateway process chain’s Full Disk Access provenance when the Desktop dashboard ancestry won the tick
- Success: same job succeeded after Desktop dashboard backends were terminated and the gateway ancestry won the tick
Representative process ownership check:
ps -axo pid,ppid,lstart,command | rg 'hermes_cli.main --profile la (dashboard|gateway)'forpidin$(ps -axo pid,command | awk '/hermes_cli.main --profile la/ && !/awk/ {print $1}');doecho"PID:$pid"
ps eww -p "$pid" -o command= | tr '''\n'| rg 'HERMES_HOME|HERMES_DESKTOP|XPC_SERVICE_NAME'done
The Desktop dashboard backends had `HERMES_DESKTOP=1` and listened on `127.0.0.1:9120+`. After cleanup, only `ai.hermes.gateway-la` and the canonical `ai.hermes.dashboard-la` remained.
Root Cause Analysis (optional)
The relevant design appears to be in hermes_cli/web_server.py, where Desktop-spawned dashboard backends start a cron ticker when HERMES_DESKTOP=1:
_start_desktop_cron_ticker(...) calls cron.scheduler.tick(...) every 60 seconds.
The code comments say this is “cross-process safe” because cron/.tick.lock prevents double-firing alongside a real gateway.
That is only at-most-once safe. It does not guarantee correct scheduler ownership. On macOS, ownership/provenance is part of the security model because Full Disk Access/TCC checks are affected by the executing process chain.
The gateway ticker in gateway/run.py is the expected owner for this profile, but the Desktop dashboard ticker can still win the shared lock.
Related issues / PRs:
Server-side cron jobs should not depend on dashboard clients or gateway ticker #25737 asks for cron execution to be owned by a durable server-side runner rather than depending on dashboard clients or the gateway ticker. This report is related, but narrower: even when a launchd gateway ticker exists, a Desktop dashboard ticker can win the shared cron lock and execute the job from the wrong process ancestry.
feat(desktop): first-class cron jobs in the sidebar + dashboard scheduler #40684 added the Desktop dashboard cron ticker and notes that it is cross-process safe via cron/.tick.lock. The observed problem is that this is only at-most-once safe. It does not guarantee correct execution provenance, which matters on macOS because TCC/FDA permissions depend on the executing process chain.
fix(cron): release tick lock before job execution #43652 discusses releasing the tick lock before job execution. That is adjacent scheduler-locking work, but it does not address scheduler ownership. Depending on final semantics, it may make provenance races easier to observe because different scheduler processes can take later ticks while earlier jobs are still running.
Why granting FDA to every possible executor is not a viable workaround:
On macOS, Full Disk Access grants are tied to executable/app identity and can need re-granting after app, bundled runtime, Homebrew, or Python updates. Granting FDA to Hermes Desktop, helper processes, Python runtimes, and every possible dashboard/gateway execution path is brittle and easy to regress. For jobs that access protected data, Hermes needs deterministic execution provenance.
Proposed Fix (optional)
Possible fixes:
Introduce a single authoritative scheduler owner per profile.
Make Desktop/dashboard delegate cron execution to the gateway when a gateway scheduler is active for that profile.
Add an execution-owner field or lock metadata so a process cannot run jobs for a profile unless it is the configured owner.
Make hermes cron status report all active scheduler-capable processes for a profile, including Desktop dashboard tickers, not just whether a gateway exists.
If multiple scheduler-capable processes are detected for the same profile, warn loudly and avoid running protected jobs from non-authoritative owners.
Bug Description
Hermes should treat cron execution ownership as part of the scheduler contract, not as an incidental race between ticker processes. Currently, a Desktop-spawned dashboard cron ticker can execute a profile’s cron job instead of the launchd-owned gateway ticker, which breaks macOS TCC / Full Disk Access provenance for jobs that need protected local data.
The problem has three parts:
Multiple processes can tick the same profile cron state. In this incident, both the launchd-owned gateway and Desktop-spawned dashboard backends were able to operate on the same profile’s
cron/jobs.jsonandcron/.tick.lock.The shared lock only provides at-most-once execution. It prevents duplicate execution at a single tick, but it does not guarantee that the authorised process chain executes the job. Whichever scheduler-capable process wins the lock runs the job.
On macOS, that distinction matters. TCC / Full Disk Access permissions depend on process ancestry. The same cron job had access to protected local data when run by the launchd gateway process chain, but lacked that access when a Desktop dashboard backend won the tick.
This occurred with a named profile (
la), but the issue is not specific to that profile name. The general bug is that multiple scheduler-capable processes can execute jobs for the same profile without an explicit, enforceable scheduler owner.Steps to Reproduce
On macOS, run a profile with a launchd-managed gateway, for example:
$HOME/.hermes/hermes-agent/venv/bin/python -m hermes_cli.main --profile la gateway run --replaceEnsure a cron job exists for the same profile. In this case it was an hourly job stored in:
$HOME/.hermes/profiles/la/cron/jobs.jsonOpen/restart Hermes Desktop in a way that leaves Desktop-spawned dashboard backends alive for the same profile. The dashboard processes observed here looked like:
python -m hermes_cli.main --profile la dashboard --no-open --host 127.0.0.1 --port 9120They had:
HERMES_DESKTOP=1HERMES_HOME=$HOME/.hermesXPC_SERVICE_NAME=application.com.nousresearch.hermes...Trigger or wait for the cron job.
Observe that the cron run may be executed by a Desktop dashboard backend instead of the launchd gateway, depending on which ticker wins
cron/.tick.lock.Expected Behavior
For a profile with a launchd-managed gateway, cron execution should be owned by the authorised gateway process chain, or by a single explicit scheduler owner for that profile.
Dashboard/Desktop should be able to create, edit, pause, and trigger jobs, but actual execution should either be delegated to the gateway or refused with a clear warning when another scheduler owner is active.
At-most-once execution is not enough on macOS. The system needs deterministic execution provenance for jobs that access TCC/FDA-protected data.
Actual Behavior
A Desktop-spawned dashboard backend sometimes won the cron lock and executed the hourly job. That job then lacked the Full Disk Access provenance required to read protected local data.
The relevant runtime binaries already had Full Disk Access when reached from the launchd gateway path. The failure was caused by the job being executed from a different process ancestry.
Observed failing ancestry included dashboard PIDs such as
45291on port9120:launchd -> Desktop-spawned dashboard backend (HERMES_DESKTOP=1) -> cron worker Python -> protected-data accessAfter terminating the Desktop dashboard backends, a triggered cron run succeeded from the canonical gateway process:
launchd -> ai.hermes.gateway-la PID 20812 -> cron worker Python -> protected-data accessThat succeeding run returned the expected protected local data.
Affected Component
Gateway (Telegram/Discord/Slack/WhatsApp)
Messaging Platform (if gateway-related)
No response
Debug Report
Operating System
macOS 26.5.1 (25F80)
Python Version
3.11.15
Hermes Version
Hermes Agent v0.16.0 (2026.6.5), upstream acd7932
Additional Logs / Traceback (optional)
Root Cause Analysis (optional)
The relevant design appears to be in
hermes_cli/web_server.py, where Desktop-spawned dashboard backends start a cron ticker whenHERMES_DESKTOP=1:_start_desktop_cron_ticker(...)callscron.scheduler.tick(...)every 60 seconds.cron/.tick.lockprevents double-firing alongside a real gateway.That is only at-most-once safe. It does not guarantee correct scheduler ownership. On macOS, ownership/provenance is part of the security model because Full Disk Access/TCC checks are affected by the executing process chain.
The gateway ticker in
gateway/run.pyis the expected owner for this profile, but the Desktop dashboard ticker can still win the shared lock.Related issues / PRs:
cron/.tick.lock. The observed problem is that this is only at-most-once safe. It does not guarantee correct execution provenance, which matters on macOS because TCC/FDA permissions depend on the executing process chain.Why granting FDA to every possible executor is not a viable workaround:
On macOS, Full Disk Access grants are tied to executable/app identity and can need re-granting after app, bundled runtime, Homebrew, or Python updates. Granting FDA to Hermes Desktop, helper processes, Python runtimes, and every possible dashboard/gateway execution path is brittle and easy to regress. For jobs that access protected data, Hermes needs deterministic execution provenance.
Proposed Fix (optional)
Possible fixes:
hermes cron statusreport all active scheduler-capable processes for a profile, including Desktop dashboard tickers, not just whether a gateway exists.Are you willing to submit a PR for this?