Skip to content

bug(gateway): macOS launchd can re-poison TERMINAL_CWD and load repo AGENTS.md #10817

@BrennerSpear

Description

@BrennerSpear

Bug Description

fix(gateway): stop loading hermes repo AGENTS.md into gateway sessions (~10k wasted tokens) (#2891) fixed the direct os.getcwd() path in run_agent.py, but the underlying bug can still happen on macOS launchd installs, which are a canonically supported gateway mode.

In the current macOS service setup, the generated launchd plist sets the gateway process WorkingDirectory to the Hermes repo root. That would be fine if it stayed an internal service-manager detail, but cli.py can still be imported indirectly during normal gateway runtime and has an import/config side effect that rewrites terminal.cwd: . to os.getcwd() for the local backend. Under launchd, that becomes the repo path, so TERMINAL_CWD can get flipped to $HOME/.hermes/hermes-agent.

Once that happens, the gateway again loads the repo's top-level AGENTS.md as project context, contrary to the documented messaging behavior (~ by default, or MESSAGING_CWD when set).

This is not a weird/nonstandard launchd setup. It is a supported path bug caused by the interaction between:

  1. canonical launchd install behavior
  2. the current default/example local terminal config shape (terminal.cwd: ".")
  3. runtime codepaths that can still re-import / reuse CLI config behavior in gateway execution

Steps to Reproduce

  1. Install the gateway as a macOS launchd service.
  2. Use the current default/example local terminal config shape:
    • terminal.backend: local
    • terminal.cwd: .
  3. Start the gateway via launchd and send a message that causes a tool-call turn.
  4. On a tool-call turn, gateway runtime can indirectly import cli.py via:
    • run_agent.py::_cap_delegate_task_calls()
    • tools.delegate_tool::_get_max_concurrent_children()
    • tools.delegate_tool::_load_config()
    • from cli import CLI_CONFIG
  5. Because cli.py loads config at import time, it resolves terminal.cwd: . to os.getcwd() and writes that back to TERMINAL_CWD.
  6. Since launchd set the process working directory to the repo root, TERMINAL_CWD becomes $HOME/.hermes/hermes-agent.
  7. Subsequent system prompt builds use TERMINAL_CWD for context discovery and load the repo AGENTS.md again.

Notes:

  • MESSAGING_CWD is not required to reproduce the underlying bug.
  • Setting MESSAGING_CWD just makes the expected-vs-actual mismatch more obvious.

Expected Behavior

Messaging gateway sessions should use the documented messaging working directory semantics:

  • default to ~
  • or use MESSAGING_CWD when configured

The launchd/systemd service process working directory should not leak into the agent's effective project-context cwd.

More precisely: the launchd service may run with WorkingDirectory=$PROJECT_ROOT, but messaging sessions must not treat that as the user/project workspace.

Actual Behavior

On macOS launchd installs, TERMINAL_CWD can be rewritten to the Hermes repo path during normal runtime, causing the gateway to load the repo's AGENTS.md into system prompts again.

Environment

  • OS: macOS 26.3.1 (build 25D771280a)
  • Version/Commit: v2026.4.13 / 1af2e18d408a9dcc2c61d6fc1eef5c6667f8e254
  • Python version: 3.13.9
  • Install mode: launchd service (ai.hermes.gateway)

Error Output

No exception is required to trigger this; it is a runtime config/context leak.

Additional Context

Relevant code / evidence:

  • hermes_cli/gateway.py generates launchd service files with WorkingDirectory={PROJECT_ROOT}
  • cli.py default config includes terminal.cwd = "."
  • cli-config.yaml.example also includes cwd: "."
  • cli.py::load_cli_config() rewrites terminal.cwd: . to os.getcwd() for the local backend and exports it to TERMINAL_CWD
  • run_agent.py::_cap_delegate_task_calls() imports tools.delegate_tool::_get_max_concurrent_children() on tool-call turns
  • tools.delegate_tool::_load_config() first tries from cli import CLI_CONFIG, which triggers the import/config side effect above
  • run_agent.py uses TERMINAL_CWD for context file discovery, so once it points at the repo, the repo AGENTS.md is loaded again
  • gateway/run.py still documents / implements messaging fallback semantics as MESSAGING_CWD or Path.home()
  • docs explicitly document macOS launchd support via hermes gateway install/start/stop/status

Docs that seem inconsistent with the current behavior:

  • website/docs/user-guide/messaging/index.md documents launchd as a supported gateway mode
  • website/docs/user-guide/configuration.md says messaging gateway cwd should be ~ by default or MESSAGING_CWD
  • website/docs/reference/environment-variables.md documents MESSAGING_CWD as the working directory for terminal commands in messaging mode

This looks like a regression / incomplete fix relative to #2891: the direct os.getcwd() path was removed from gateway prompt building, but an indirect cli.py import can still re-poison TERMINAL_CWD under launchd.

Possible Fixes

Primary fix

Stop gateway / non-CLI codepaths from importing cli.py or depending on import-time CLI config mutation, so messaging cwd resolution stays independent from process cwd.

Defense in depth

Reconsider whether launchd should install with WorkingDirectory=$PROJECT_ROOT, or at least ensure that value can never leak into messaging context resolution.

Current Workaround

Removing terminal.cwd: "." from the user config avoids the bad behavior locally, but that should not be required for a default supported launchd install to behave correctly.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions