Skip to content

Worker processes inherit host proxy env (HTTP_PROXY) and break streaming agents in worktrees #2951

Description

@axisrow

Summary

Agent worker processes (codex, opencode, claude, …) inherit the daemon's full os.Environ() with no sanitization. Per-project/role env config only layers on top of the inherited environment (role wins on collision) rather than replacing it, so a user cannot remove an inherited variable from a worker — only override it.

How this was found. A codex worker spawned into a worktree failed to connect to its API (stream disconnected / request timed out), while the same codex run from the user's own shell worked. The host runs a forward HTTP proxy on 127.0.0.1:8118 (exported via HTTP_PROXY/HTTPS_PROXY). Tracing showed the worker process was picking up the host/desktop proxy env rather than the user's shell setup — which led into the env-forwarding code below, and from there to the broader concern described at the end.

Reproduction (codex + local HTTP proxy):

  1. Host has a forward HTTP proxy on 127.0.0.1:8118 exported via HTTP_PROXY/HTTPS_PROXY in the environment that launches AO.
  2. codex launched from the user's own shell (with a working proxy) — works.
  3. ao spawn --agent codex … into a worktree — fails to connect: stream disconnected / request timed out.

Code path & feature detail (current main). The environment a worker runs in is assembled across several layers, all additive — nothing filters by content:

  • Daemon reconstructs a login-shell env at startup. frontend/src/main.ts (daemonEnv, ~:465-486) + frontend/src/shared/shell-env.ts (:81-90) run zsh -ilc 'env -0', parse the full block, and use it as the daemon's base env — so anything exported in ~/.zshrc/~/.zprofile reaches the daemon even when the GUI was launched from Finder/Dock.
  • Workers inherit os.Environ() of the daemon. backend/internal/adapters/runtime/tmux/tmux.go:68 (cmd.Env = append(os.Environ(), env...)) and :295 (attachEnv(os.Environ())). attachEnv (:324) only rewrites TERM; every other var passes through unchanged. On Windows, backend/internal/adapters/runtime/conpty/spawn_windows.go:83 merges os.Environ() + cfg env the same way.
  • Config env is a free-form, unvalidated map layered on top. backend/internal/domain/projectconfig.go:28 (ProjectConfig.Env) and backend/internal/domain/agentconfig.go:46 (AgentConfig.Env) are map[string]string; Validate() (agentconfig.go:82-102) checks Permissions/MCP/PluginDirs but not env keys. backend/internal/adapters/runtime/tmux/tmux.go:484-507 (validateEnvKeys) only enforces key syntax ([A-Za-z_][A-Za-z0-9_]*), not content.
  • Merge semantics: layer, not replace. backend/internal/session_manager/manager.go:539,2170-2179 (mergeEnv) overlays role→project env on top of the inherited env; a user can override an inherited value but cannot delete an inherited variable from a worker. Only AO-internal keys (AO_SESSION_ID, AO_PROJECT_ID, …) and PATH/GH_PATH are pinned against override.

Net effect: every worker sees the daemon's full reconstructed environment, workers of different projects are not isolated by environment, and per-project/role env can only add/override — never narrow.

Architectural note / severity. I don't think this is a flaw introduced by AO — it reads as a property of this whole class of orchestrator designs, where the shell environment is forwarded down to workers so a GUI-launched daemon can find tmux/git/credentials (docs/daemon-environment.md frames forwarding as correct for exactly that UX reason). That said, I personally consider this CRITICAL. The combination "a worker inherits the daemon's environment wholesale and a worker's behavior is driven by config files (env / MCP / plugins)" is, in my assessment, a high-severity security concern — not a cosmetic env-hygiene issue — and it's worth a deliberate threat-model decision (env-key allowlist / per-worker secret scoping / replace-instead-of-merge) rather than leaving it as an incidental side effect of env forwarding. I'm intentionally not posting the specifics of why I rate it critical in this public issue. I'm ready to share the details privately with a maintainer if there's a private channel (a SECURITY.md / responsible-disclosure process), or to set one up if none exists yet.

Environment: macOS, current main (post #2838, #2847); reproduces conceptually for any proxy-aware agent.

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions