---
name: agent-harness-security-review
description: Security review for coding agents and the code around them - AI CLIs, IDE agent modes, agent SDK wrappers, MCP servers and bridges, agent web UIs, and CI bots that run an agent. Use whenever someone asks to security review, audit or threat-model a coding agent or AI CLI tool, is deciding whether a team can safely adopt one, or is building or changing code that loads project config, approves or blocks shell commands and tool calls, checks file paths, sandboxes execution, fetches URLs, renders model output, opens a local server, or runs an agent unattended - even if they never say "harness". Every check maps to a published CVE class from Claude Code, Codex, Gemini CLI, Antigravity, Copilot, OpenCode, Pi and their ecosystem.
---

# Agent harness security review

A harness is the code between the model and the machine: it reads the workspace, decides which
tool calls run, executes them, and talks to the network. Almost every published vulnerability in
coding agents is a harness bug, not a model bug. Prompt injection is usually only the delivery.

This checklist comes from ~120 public advisories (Jun 2025 – Oct 2026). Each section names the
class, the question to ask, what to grep for, and real CVEs that show the failure.
Source data: https://bluebear.io/research/agent-harness-cves/agent-harness-cves.csv

## How to run the review

1. **Map the surfaces first.** List, with file paths:
   - every file, directory or env var the harness reads from the workspace (and *when*: before or after trust)
   - every external program the harness itself runs (git, package managers, linters, `rg`, `fd`, shells)
   - the command/tool approval path: where a tool call is classified as safe, prompted, or denied
   - path confinement: where a path is checked, and where it is later opened
   - every outbound network path: fetch tools, image/link rendering, schema/URL resolution, telemetry
   - every listening socket, IPC channel, browser extension bridge, or WebSocket
   - every non-interactive mode (headless, CI, `--yolo`, auto-approve, bots, email/chat triggers)
2. **Walk the 12 classes below against that map.** For each candidate, note file:line, what the
   attacker has to control, and the class.
3. **Assume the attacker controls**: the repository (including `.git/`, `.env`, dotfiles, filenames,
   branch names), any text the model reads (files, web pages, issues, tool and MCP output), and any
   web page the user visits while the harness runs. Do not assume they control the user's home config.
4. **Confirm each finding before you report it.** Trace from the attacker-controlled input to the
   dangerous call and check nothing in between stops it: a trust gate, an auth check, a flag that is
   off by default. Read the code that runs at that version, not comments or docs. If you can't
   close the trace, report it as *unconfirmed* and say which link is missing. The reverse matters
   just as much: a filter or validator is not a reason to mark something clean until you have checked
   it against the bypasses its class is known for (case, encoding, control characters, symlinks,
   alternate syntax). Most CVEs in this list are bypasses of a check that existed.
5. **Report** findings ranked by: no model needed > needs prompt injection > needs user action. A bug
   that fires before the model is contacted is the worst kind.

## 1. Workspace config runs before trust

*Question:* What does the harness read or execute from the workspace before the user has made a
trust decision, and what can those files change?

- Nothing from the workspace may be parsed into behavior before trust: settings, env blocks,
  hooks, MCP server lists, extensions/plugins, permission modes, model endpoints.
- `.env` in the cwd is workspace content. It must not set variables that relocate the harness's
  own config home or binaries (`CODEX_HOME`, `GEMINI_CLI_HOME`, `*_CLI_PATH`).
- Anything that changes *where trust is evaluated* (worktree `commondir`, symlinked project roots,
  parent-folder trust) is a trust bypass.
- Headless/CI mode is not a reason to auto-trust.
- External programs run at startup read repo config too (`yarn --version` loads `.yarnrc.yml` plugins).

Grep: `dotenv`, `loadEnv`, `process.env[` near config paths, `settings.json`, `mcp`, `hooks`,
`trust`, `isTrusted`, `HOME`, startup `exec`/`spawn` before the trust prompt.

Evidence: Claude Code CVE-2025-59536, CVE-2026-21852 (`ANTHROPIC_BASE_URL` sent the API key before
trust), CVE-2026-33068 (`bypassPermissions` from repo settings skipped the dialog), CVE-2026-40068
(`commondir`), CVE-2025-59828/CVE-2025-65099 (Yarn); Codex CVE-2025-61260 (`.env` → `CODEX_HOME`);
Gemini CLI CVE-2026-13745 (`.env` → `GEMINI_CLI_HOME`), CVE-2026-12537 (headless auto-trust);
Pi CVE-2026-54325 (`.pi` extensions); Ouroboros CVE-2026-47211; Aider CVE-2026-85674.

## 2. The harness's own git calls execute repo config

*Question:* When the harness runs git (status, diff, worktree, metadata), can the repository's
`.git/config`, a nested bare repo, or attributes make git run a program?

- Keys that execute: `core.fsmonitor`, `core.hooksPath`, `core.sshCommand`, `core.pager`,
  `diff.external`, `filter.*.clean/smudge/process` (with `attr.tree` or `.gitattributes`),
  `credential.helper`, `uploadpack`, aliases, and more.
- Fix shape: run git with a scrubbed environment and explicit overrides
  (`-c core.fsmonitor=false -c core.hooksPath=/dev/null ...`, `GIT_CONFIG_NOSYSTEM=1`,
  `GIT_CEILING_DIRECTORIES`, `safe.bareRepository=explicit`), or use a library (libgit2/gitoxide)
  that does not execute config. Do this for *every* internal git call, including ones in
  status bars and pre-trust code.

Grep: `spawn('git'`, `execFile("git"`, `simple-git`, `git status`, `git diff`, `worktree`.

Evidence: Codex CVE-2026-19590 (`hooksPath`), CVE-2026-19592 (`fsmonitor`), CVE-2026-19593
(`attr.tree` + filter, 9.8, no model involved); Copilot CLI CVE-2026-45033 (nested bare repo);
Claude Code CVE-2026-55607; Goose CVE-2026-72718; Qwen Code (unpatched at disclosure).

## 3. Command approval by string parsing

*Question:* Is any shell command auto-approved because a parser decided it is "read-only" or
matches an allowlist?

Treat this as the highest-volume bug class. Every vendor shipped it more than once.
- Command *names* are not safe; arguments are. `rg --pre`, `find -exec`, `sed .../e`, `sed -i`,
  `git show/ls-remote --upload-pack` (git accepts abbreviated flags), `man --html=`,
  `sort --compress-program`, `xargs`, `env`, `history -a`, `fd -X`.
- The parser must agree with the real shell: `$(...)` and backticks, `${var@P}`, `${!var}`,
  `${x=...}`, `<(...)`, `$IFS`, newlines, `;`/`&&`/`|`, zsh `>!`/`>|`, PowerShell `--%`, and `cd`
  changing the directory later writes are resolved against.
- Only the first command being checked is a bypass (`grep x ; curl ...`).
- A policy layer that sees a different string than the executor is a bypass (truncation, decoding,
  normalization).
- Prefer: a real parser (tree-sitter-bash or the shell's own), fail closed on any construct it does
  not fully model, allowlist argv shapes not command names, and run approved commands in a sandbox
  anyway. The parser is a UX feature. The sandbox is the boundary.

Grep: `readOnly`, `isSafe`, `allowlist`, `SAFE_COMMANDS`, regex over command strings, `split(' ')`,
`startsWith(` on commands.

Evidence: Claude Code CVE-2025-66032 (8 bypasses in one report), CVE-2025-54795, CVE-2025-58764,
CVE-2025-64755, CVE-2026-24053, CVE-2026-24887, CVE-2026-25722, CVE-2026-25723; Codex CVE-2025-54558,
CVE-2026-19591; Copilot CLI CVE-2026-29783, PromptArmor `env` bypass; Gemini CLI Tracebit and Cyera
backtick bypasses; ClaudeHookBridge CVE-2026-73614 (500-char truncation); Roo Code ~10 CVEs.

## 4. The agent can rewrite its own controls

*Question:* Can a file write by the agent change what the agent is allowed to do next, or what
runs on the next launch?

- Protected paths must include *every* file that grants permissions or executes: the harness's
  settings/hooks/MCP config (project **and** global), IDE settings (`.vscode/settings.json`,
  `tasks.json`, `*.code-workspace`), git hooks/config, shell rc files, package manager config.
- Protect paths that do not exist yet. Match case-insensitively on case-insensitive filesystems
  and after canonicalization (NTFS short names, trailing dots, ADS).
- Writes from inside the sandbox must not be able to create these files either.
- Re-approve when an approved config entry changes; bind approval to content (hash), not name.

Evidence: Copilot CVE-2025-53773 (`chat.tools.autoApprove`), CVE-2025-64660 (`.code-workspace`),
CVE-2026-41109; Claude Code CVE-2026-25725 (settings file created from the sandbox); Antigravity
Mindgard (global `mcp_config.json`) and `tasks.json`; Cursor CVE-2025-54135, CVE-2025-54136,
CVE-2025-59944 (case).

## 5. Path confinement

*Question:* Where is "inside the workspace" decided, and is the same path opened later?

- Compare canonical paths by component (`realpath` + `is_relative_to`), never string prefix.
- Resolve symlinks for allow **and** deny rules; check every component, not just the leaf.
- Check and use must be the same operation: open with `O_NOFOLLOW`/`openat2(RESOLVE_BENEATH)` or
  open-then-verify the fd. A check followed by a later `writeFile(path)` is a TOCTOU.
- The sandbox root comes from where the user started the session, never from a model-supplied `cwd`.
- `file://` in fetch tools is a path read.

Evidence: Claude Code CVE-2025-54794 (prefix), CVE-2025-59829 and CVE-2026-25724 (symlink deny rule,
fixed twice), CVE-2026-103435 (TOCTOU); Codex CVE-2025-55345, CVE-2025-59532 (model `cwd`);
Copilot CVE-2025-62449, CVE-2026-65675, CVE-2025-66389 (`file://`); gemini-bridge CVE-2026-54785.

## 6. Egress through "read-only" channels

*Question:* List every way data can leave the machine without an approval prompt. Each one is an
exfiltration channel.

- DNS (`ping`, `dig`, `nslookup`, `host`), auto-approved fetch domains, Markdown images, link
  previews, JSON `$schema` resolution, browser subagents, telemetry.
- Domain allowlists: exact host match after URL parsing; never `startsWith`/`includes`; never a
  host that serves user content (`huggingface.co`, `webhook.site`, gist/raw hosts, pastebins).
- Do not auto-render remote images from model output. Proxy or block them.
- A GET with a query string is a write.

Evidence: Claude Code CVE-2025-55284 (DNS), CVE-2026-24052 (`startsWith`), CVE-2026-54316
(huggingface.co); Codex CVE-2026-14898 (images); Antigravity PromptArmor (webhook.site on the
default allowlist), Embrace The Red, 0DIN; Copilot IDEsaster `$schema`, CVE-2026-81380.

## 7. Local servers and bridges

*Question:* Does anything listen (HTTP, WebSocket, IPC, extension messaging)? Who can reach it?

- Assume every web page the user opens can reach `localhost`. Require a random per-session token,
  check `Origin` and `Host` (DNS rebinding), bind to loopback, no `Access-Control-Allow-Origin: *`.
- Reject `text/plain` and form-encoded bodies on JSON endpoints (simple requests skip preflight).
- No unauthenticated endpoint may run shell, open a PTY, install packages, or change permissions.
- Browser extension `externally_connectable` must not match all URLs.
- Rendered model output in a local web UI is XSS on the origin that holds your RCE endpoints.

Evidence: Claude Code IDE CVE-2025-52882 (WebSocket, no Origin check); OpenCode CVE-2026-22812
(no auth, CORS `*`, `/pty`), CVE-2026-22813 (XSS), GHSA-632h-h47v-g4x4 (`/global/upgrade` via
form POST); Cline CVE-2026-44211, CVE-2026-59723; Kilo CVE-2026-79403; AgentAPI CVE-2025-59956
(DNS rebinding); Ruflo CVE-2026-59726; Claude Code Templates CVE-2026-73222; Antigravity Hacktron.

## 8. Argument and command injection in harness code

*Question:* Does the harness build a shell string or argv from anything untrusted (filenames,
branch names, git config values, session IDs, tool arguments, hook payloads)?

- Use `execFile`/`spawn` with an argv array, never `exec` with interpolation.
- Put `--` before positional arguments; reject values starting with `-` (`fd -X`, `--mcp-config`).
- Never interpolate into another language's string literal (Python, JS) in hooks or status lines.

Evidence: Claude Code CVE-2025-59041 (`git config user.email`); Claude Agent SDK CVE-2026-96620
(`resume` → `--mcp-config`); Antigravity `find_by_name` → `fd -X`; Gemini CLI `/ide install`
(`execSync` with a filename); Codex cloud branch-name injection; OpenHands CVE-2026-33718;
Claude Code UI CVE-2026-31861/31862/31975; claude-code-cache-fix CVE-2026-45136.

## 9. Sandbox gaps

*Question:* Enumerate what the sandbox does **not** block, then ask how the agent reaches the
host through each gap.

- Profiles must be deny-by-default (`(allow default)` in a Seatbelt profile is not a sandbox).
- Block the Docker/Podman socket, `mount`, Launch Services/`open`, and IPC to unsandboxed helpers.
- Unsandboxed parts of the harness must not follow symlinks or read files the sandbox created.
- Native tools (search, patch, REPL) must run under the same policy as shell commands.
- A host-tool relay must authenticate the caller with something the sandbox cannot forge
  (not `/proc/<pid>/cmdline`).

Evidence: Claude Code CVE-2026-39861; Antigravity Pillar seatbelt; Gemini/Codex/Cursor Docker
socket; Codex Heapjack (`node_repl` shared heap); `@ai-sdk/harness-*` CVE-2026-64650/64651.

## 10. Unattended runs: CI, bots and chat triggers

*Question:* When no human is watching, who supplies the prompt, and what credentials are in reach?

- Issue titles, PR bodies, comments, emails and chat messages are attacker input.
- Don't put long-lived secrets in the agent's env or `.git/config` (`persist-credentials: false`).
  The agent can read `/proc/$PPID/environ`.
- `--yolo` or auto-approve must still honor explicit tool allowlists. An unknown or deprecated
  config key must fail closed, not fall back to allow-all.
- Never check out and execute PR-head config (`.mcp.json`, settings) in a privileged workflow.
- Permission prompts relayed to chat must verify the approver is the owner.

Evidence: Gemini CLI CVE-2026-12537 (`--yolo` skipped the allowlist), Pillar WIF (deprecated
`coreTools` key → allow-all → GCP project), PromptPwnd, Comment and Control; claude-code-action
CVE-2026-47751; AgenticMail CVE-2026-57495; OpenClaw CVE-2026-100585.

## 11. The model as the policy

*Question:* Is any security decision made by the model rather than by code?

- "The model decides whether the command is safe to auto-run" is not a control.
- Tool results and fetched content must not be able to impersonate system or harness messages
  (escape or strip harness-reserved tags), and invisible Unicode (Tag block, bidi) should be shown
  or stripped.
- MCP tool calls need the same approval policy as shell commands.

Evidence: Antigravity Embrace The Red (auto-execute, Unicode Tags, MCP without approval), deadbits
`<EPHEMERAL_MESSAGE>` forgery.

## 12. Boring local bugs

*Question:* What does the harness write to shared locations, or read from places another local
user can write, and does any policy or credential step fail open?

Predictable `/tmp` paths, write-then-chmod races on credential files, world-readable output,
config loaded from user-writable system directories, DLL search order, policy fetch that fails open,
plugin pins that resolve to a branch with the same name as the SHA.

Evidence: Pi CVE-2026-54327, CVE-2026-54328; Claude Code CVE-2026-46406, CVE-2026-35603,
CVE-2026-103012, CVE-2026-86063.

## Output format

```
## Surface map
<the lists from step 1, with file paths>

## Findings
### [class N] <one-line title>  — severity, needs: none | prompt injection | user action
Status: confirmed | unconfirmed (<missing link>)
Code path: <file:line where attacker-controlled data enters> -> <file:line of the dangerous call>
Precondition: <what the attacker must control, in one sentence>
Impact: <what they get>
Fix: <specific change, with file:line>
Closest public precedent: <CVE id>

## Checked and clean
<classes reviewed with no finding, what was verified, and which known bypasses were checked>
```

Describe the mechanism and the code path, and stop there. Don't write runnable exploit payloads,
proof-of-concept scripts or copy-paste attack strings. The maintainer needs to know where the
input enters, why nothing stops it, and what to change. A working payload adds nothing to that,
and it makes the report harmful if it leaks before the fix ships.

Don't report "prompt injection is possible" as a finding on its own. The finding is what the harness
lets injected text do.
