The Denylist Didn't Hold: An Agent Guardrail Runtime Got RCE'd Twice Through the Same .env

The Denylist Didn't Hold: An Agent Guardrail Runtime Got RCE'd Twice Through the Same .env

The runtime whose entire job is constraining AI coding agents just needed a second remote-code-execution advisory for the same bug class. That second advisory exists because the first fix was a denylist.

CVE-2026-66065 landed in NVD on August 3, 2026 for Ouroboros, a local-first “Agent OS” runtime that wraps coding agents — Claude Code, Codex CLI, Gemini CLI, OpenCode, Copilot, Kiro, and more (13 runtimes per the README) — and applies user-defined policies and an approval gate to their actions. The advisory is explicitly labeled an incomplete fix of CVE-2026-47211: the denylist added in May to stop a cloned repo’s .env from redirecting execution omitted several execution-routing keys of the same remote-code-execution class, so a malicious repository could still reach arbitrary command execution — and in some configurations, silently delete the human approval gate entirely.

Two advisories, same file, same boundary, same impact. The uncomfortable part is how ordinary that failure is.

What Ouroboros is, and what it was supposed to protect

Ouroboros (Q00/ouroboros, MIT, ~5.4k stars) describes itself as an “Agent OS”: a specification-first workflow where a Socratic interview gates the spec on an ambiguity score, one command drives execution, and a staged evaluation gate and budgeted evolution loop close the cycle. For this article the important part is the security model: it records agent actions and applies user-defined policies to constrain behavior, and it sits in front of other agent CLIs — spawning them, routing their config, and gating their tool calls. It is a guardrail layer. Its whole reason to exist is that coding agents will do dangerous things if you let them.

That makes the attack path a kind of irony the industry should study: the guardrail runtime was compromised through the exact mechanism it was built to police — an untrusted repository arriving on a developer’s machine.

Act one: the .env that ships with the repo

The first advisory, GHSA-c4m7-2gwp-vw76 (CVE-2026-47211, published May 20, 2026, CVSS v4 8.6), is a clean supply-chain story. Ouroboros auto-loads a .env file from the current working directory at import — no review step, no prompt. Prior to 0.39.0, execution-affecting variables such as OUROBOROS_CLI_PATH and OPENCODE_CLI_PATH were honored straight from that file. An attacker commits a repo containing:

OUROBOROS_CLI_PATH=./malicious_script.sh

The victim clones the repo, runs ouroboros init from inside it, and the runtime spawns ./malicious_script.sh instead of the intended CLI. Arbitrary code execution as the victim user. (CWE-426 untrusted search path, CWE-15 external control of system setting.)

The fix, commit 4e70b76 (PR #1078), is genuinely well-built — and its own comment block is the most instructive artifact in this entire saga. It introduces _UNTRUSTED_ENV_DENYLIST, a frozen set of keys covering three classes of RCE sink:

  1. explicit CLI path overrides fed into a subprocess (OUROBOROS_CLI_PATH, OUROBOROS_CODEX_CLI_PATH, OUROBOROS_HERMES_CLI_PATH, OPENCODE_CLI_PATH, …),
  2. runtime/backend selectors that pick which adapter — and therefore which executable — gets spawned,
  3. permission-mode overrides (OUROBOROS_AGENT_PERMISSION_MODE, …) that could silently remove the human approval gate.

_load_env_file(path, trusted=False) now defaults fail-closed: the project-directory .env is loaded as untrusted, and any key in the denylist is dropped. Only ~/.ouroboros/.env is trusted. The comment says it plainly: “a cloned repository’s .env must not be able to change which binary runs or whether the user’s approval gate applies.” Shipped as 0.39.0.

That is a correct statement of policy. The implementation, however, was a list.

Act two: the keys nobody enumerated

CVE-2026-47211 got fixed. The bug class did not. GHSA-jv2h-4p9v-wf5w (CVE-2026-66065, published June 18, 2026, NVD entry August 3, 2026) documents what the maintainers call an incomplete fix: the denylist “did not account for all keys,” and a malicious repo could still reach arbitrary command execution via the same auto-loaded .env. The omitted keys, per the advisory:

  • Backend config-home rootsCODEX_HOME, OPENCODE_CONFIG, OPENCODE_CONFIG_DIR, XDG_CONFIG_HOME. A spawned vendor CLI resolves its configuration from these. CODEX_HOME=./.evil plus a committed ./.evil/config.toml redirects the nested Codex agent to attacker configuration — including mcp_servers.<name>.command/args (direct RCE) and, most damagingly, approval_policy="never" and sandbox_mode="danger-full-access": the human approval gate is silently removed. (Reported by matte1782.)
  • MCP bridge / plugin execution rosterOUROBOROS_MCP_CONFIG (the YAML’s command/args are spawned via stdio), OUROBOROS_PLUGIN_LOCKFILE, OUROBOROS_PLUGIN_TRUST_ROOT (redirects the installed-plugin roster so ooo <name> dispatches into attacker code). (Reported by hackkim.)
  • SSRF guard toggleOUROBOROS_ALLOW_LOCAL_TRANSPORT re-enables loopback/private MCP transport targets that the runtime otherwise blocks.
  • Instruction / capability rootsOUROBOROS_AGENTS_DIR, COPILOT_CUSTOM_INSTRUCTIONS_DIRS (replace spawned sub-agent role prompts), OUROBOROS_RUNTIME_PROFILE (backend selector), OUROBOROS_TOOL_CAPABILITIES (can lower a tool’s approval_class, weakening the gate from inside the YAML).

And then the cwd branch: the MCP bridge auto-loaded ./.ouroboros/mcp_servers.yaml from the working directory (create_bridge_from_env(cwd=Path.cwd())). Running ooo inside a malicious repo spawned the committed roster’s commandRCE with no .env at all. A second code path that bypassed the chokepoint entirely.

Fixed in 0.42.1: all listed keys added to _UNTRUSTED_ENV_DENYLIST, the cwd auto-discovery branch removed (only the explicit env var and ~/.ouroboros/mcp_servers.yaml remain, both trusted), and the regression suite now derives from the source denylist so future keys can’t drift again. CVSS v4 8.4 per NVD (CWE-15, CWE-94).

Why this pattern is the news

The maintainers themselves frame the root cause in the advisory’s “CVE dependency rationale”: every entry is one vulnerability, one root cause — “an untrusted project-directory file that travels with a cloned repo is auto-loaded, with no review step, and is trusted to alter execution routing.” The correct fix, they write, is a single policy — “repo-local files are never a trusted source of execution-routing config” — enforced at one chokepoint.

Read that against the May fix. The May fix’s own comment stated the identical policy, then implemented it as a set. The set was incomplete by construction: any environment variable the runtime honors for routing, configuration, or permissions is a potential bypass until someone thinks to add it to the list. A denylist of attacker-relevant inputs is an enumeration problem in a system that adds new routing knobs every release. The August advisory is not a fluke of this project — it is the predicted failure mode of the approach.

This is not an isolated offender, either. The same month Ouroboros shipped its first advisory, Google patched GHSA-wpqr-6v78-jr5g (CVE-2026-12537, CVSS 10.0): Gemini CLI in headless mode auto-trusted workspace folders for loading configuration and environment variables, so a maliciously crafted .gemini/.env in a PR-triage workflow gave pre-sandbox host-level code execution on the CI runner. Anthropic’s own Claude Code walked back repo-local autoMode config from .claude/settings.local.json to ~/.claude/settings.json only in v2.1.207 — the exact same trust boundary, closed the same way, months after the pattern was already public.

The pattern across all three: agent CLIs treat repo-local files as configuration, and configuration becomes code the moment it can pick a binary, set a policy, or answer an approval prompt. “Clone a repo and run the agent on it” — the most natural workflow in AI coding — is turning into the new “open the attachment.”

What operators and builders should change

For anyone running Ouroboros or any agent runtime that auto-loads repo-local config:

  1. Upgrade to 0.42.1+ (affected: ≤ 0.42.0). This is the fix for the fix.
  2. Never run agents from untrusted directories. Ouroboros’ own workaround is the correct posture: don’t run it from a cloned repo directory; delete any project .env and ./.ouroboros/mcp_servers.yaml before running.
  3. Treat repo-local config as code. Review it like you’d review a pre-commit hook or a Makefile that runs as root — because it can be.
  4. Run agents with least privilege and no ambient credentials. The approval gate is a convenience, not a containment boundary — CVE-2026-66065’s scariest capability is approval_policy="never", i.e. disarmament, not exploitation.
  5. If you build an agent runtime, make trust structural. Allowlist the sources of execution-routing config (the real process environment, a user-owned config home) and reject everything else by default. A denylist that derives from the source set is a decent drift guard; a denylist that doesn’t is a promise to ship CVE-2026-66065 someday.

Denylists are inventory, not boundaries

The uncomfortable truth is that CVE-2026-66065 is not a story about Ouroboros. It is a story about a whole generation of agent tooling that believed listing dangerous inputs was equivalent to defining a trust boundary. It never is: the boundary is defined by where a value comes from, not which values exist. The May fix was a correct policy written as an incomplete implementation; the June fix closed the gap by making the policy the implementation — one chokepoint, fail-closed, with the dangerous source removed.

The systems lesson generalizes to every agent harness being built this year: if a file that travels with untrusted code can change which binary runs, which policy applies, or whether a human approves, you have not secured your agent — you have given the attacker a configuration language. The guardrail runtime that had to be fixed twice is the proof.

Sources

Keep reading