Harnesses · · 2,008 words · 9 min read
Coding agent security: prompt injection, MCP servers, and secrets
Prompt injection via tool results, MCP server trust, and secrets in config: what the Claude Code, Codex, and Gemini CLI docs commit to, plus an auditor to run.
security prompt injection mcp secrets claude code
You get the three routes an attacker has into a coding agent's tools (the text its tools return, the servers you plug into it, and the secrets sitting in its configuration), what the Claude Code, Codex CLI, and Gemini CLI documentation commits to on each route as read on 2026-09-07, and a standard-library Python auditor that reads the configuration files and flags the patterns that widen the blast radius. No agent ran in this session. What ran was the auditor, on Python 3.13.12 on Windows, and every output block below is real. Versions, from each project's GitHub releases page on the same day: Claude Code v2.1.263 (2026-09-06), Codex CLI rust-v0.153.4 (2026-09-04), Gemini CLI v0.58.0 (2026-09-01).
Data that behaves like instructions
The 2023 paper that named the problem, Greshake and colleagues' "Not what you've signed up for", puts it in one line: "We argue that LLM-Integrated Applications blur the line between data and instructions." Their attack works by "strategically injecting prompts into data likely to be retrieved", and for a coding agent that is most of what it reads: the page it fetches to check an API, the README of a package it is evaluating, the issue it was told to fix, and the output of every tool call. AgentDojo, the 2024 benchmark from Debenedetti and colleagues, states the agent version directly: "AI agents are vulnerable to prompt injection attacks where data returned by external tools hijacks the agent to execute malicious tasks." Across its "97 realistic tasks" and "629 security test cases" the finding is neither reassuring nor apocalyptic: "existing prompt injection attacks break some security properties but not all."
The OWASP Top 10 for LLM Applications, 2025 edition, gives the surfaces names a review can use. Four of its ten entries land on a coding agent.
| OWASP 2025 entry | Where it lands in a coding agent | The control that answers it |
|---|---|---|
| LLM01 Prompt Injection | fetched pages, tool results, issue text, file contents | the permission gate, network rules, an isolated fetch context |
| LLM03 Supply Chain | MCP servers pulled from a registry at launch, a .mcp.json in a cloned repository |
version pinning, project-server approval, server allowlists |
| LLM02 Sensitive Information Disclosure | secrets in env and headers, .env files the agent can read |
variable references, deny rules, environment redaction |
| LLM06 Excessive Agency | bare Bash allows, trust: true, bypass modes |
the permission model itself |
OWASP's definition of the indirect case is short: "Indirect prompt injections occur when an LLM accepts input from external sources, such as websites or files." Its Excessive Agency entry names excessive functionality, excessive permissions, and excessive autonomy as the root causes, and its first prevention strategy is simply "Minimize extensions". That row's answer, the permission model, was the subject of /posts/coding-agent-permission-models-compared.html; this article is about the other three.
What the harness docs commit to on injection
Claude Code's security page lists its core protections against the attack: "Permission system", "Context-aware analysis", "Input sanitization", and "Network command approval: Commands that fetch content from the web such as curl and wget are not auto-approved by default." Two of the additional safeguards are aimed at tool results: "Isolated context windows: Web fetch uses a separate context window to avoid injecting potentially malicious prompts", and "Command injection detection: In Manual mode, suspicious bash commands require manual approval even if previously allowlisted". Of its five best practices for untrusted content, the operative ones are "Avoid piping untrusted content directly to Claude" and "Use virtual machines (VMs) to run scripts and make tool calls, especially when interacting with external web services". It also says what vendor pages rarely say: "While these protections significantly reduce risk, no system is completely immune to all attacks."
Codex answers with a default rather than a detector. "By default, the agent runs with network access turned off." When you turn it on: "Use caution when enabling network access or web search in Codex. Prompt injection can cause the agent to fetch and follow untrusted instructions." Web search goes through a cache by default, and the page says why: "This reduces exposure to prompt injection from arbitrary live content, but you should still treat web results as untrusted." Under --yolo or another full-access setting, "web search defaults to live results". One more sentence bounds what the sandbox's network proxy covers: "It does not filter web search, app or connector tool calls, MCP server connections, browser or Computer Use activity, Codex cloud tasks, or the client's model and authentication requests."
The Gemini CLI MCP page I read does not mention prompt injection at all. Its controls are structural: includeTools, allowlist behaviour in the page's words, excludeTools, and a trust flag covered below. The shape is the same everywhere: the gate decides what runs, and no page claims the model cannot be talked into asking. So the rest of this article is about what the gate can actually see, the servers and the secrets.
MCP servers: the supply chain you configured
The MCP specification's security page, in its 2026-07-28 revision, has a section called "Local MCP Server Compromise" that reads like a description of every .mcp.json in circulation. "Local MCP servers are binaries that are downloaded and executed on the same machine as the MCP client." Its first two attacks: "An attacker includes a malicious "startup" command in a client configuration" and "An attacker distributes a malicious payload inside the server itself". A client that offers one-click configuration "MUST" show "the exact command that will be executed, without truncation", and "SHOULD" warn "that MCP servers run with the same privileges as the client". The wire protocol itself was covered in /posts/mcp-server-tutorial-python.html; this is about who you let onto it.
Claude Code's MCP page carries a warning box: "Verify you trust each server before connecting it. Servers that fetch external content can expose you to prompt injection risk." A server declared in a repository's .mcp.json gets an approval prompt in interactive sessions, and then the exception that matters for CI: "In claude -p runs, Agent SDK sessions, and cloud sessions, Claude Code can't show that prompt: it loads project-scoped servers without asking." The security page says the same of trust: "Trust verification is disabled when running non-interactively with the -p flag." The knobs are in the settings reference: enableAllProjectMcpServers will "Approve every server in project .mcp.json files without a prompt", disabledMcpjsonServers will "Reject specific servers from a project's .mcp.json", and deniedMcpServers can "Block specific MCP servers by URL, command, or name". The --bare flag from /posts/headless-coding-agent-ci.html is the other half of that answer. On what the vendor vouches for, the security page is exact: Anthropic "does not security-audit or manage any MCP server."
Permission rules can name a server's tools: mcp__puppeteer "matches any tool provided by the puppeteer server", mcp__puppeteer__puppeteer_navigate matches one tool, and a deny of mcp__* "matches every MCP tool across all servers". A whole-server allow therefore approves tools the server has not announced yet, and each result can be large: the docs warn at 10,000 tokens and cap a tool result at 25,000 by default.
Gemini CLI's trust property is one line: "When true, bypasses all tool call confirmations for this server". Codex's configuration reference gives each server enabled_tools, an "Allow list of tool names exposed by the MCP server", disabled_tools, and a default_tools_approval_mode for the "Default approval behavior for MCP tools on this server unless a per-tool override exists."
Two 2026 measurements say why the launch command matters. Padilla's July 2026 scan found "over 21,000 server instances detectable on the public internet", confirmed 640 production servers, audited 414 of them dynamically, and reports that "91.8% of dynamically audited servers lack OAuth authentication" and that "687 tool instances across confirmed servers expose shell execution capabilities without access controls". TrustShiftProbe, from Rostamzadeh and colleagues, describes a server that "behaves benignly during an initial conditioning phase, building operational reliance and suppressing agent skepticism, before switching to an adversarial payload once an interaction threshold is reached", and measures "a 69.5% mean attack success rate" across the models tested. A server you approved last month is a different program today if npx -y name resolves to the newest release at every launch. Pinning does not make a server honest; it makes the thing you approved the thing that runs.
Secrets within the agent's reach
Claude Code expands ${VAR} and ${VAR:-default} in a server's command, args, env, url, and headers, and the docs' own example is "Authorization": "Bearer ${API_KEY}". That is the difference between a configuration file that can be committed and one that cannot. On the file side, the permissions page: "To block Claude's file tools from reading a file or directory, add a Read deny rule for its path, such as Read(./.env) or Read(./secrets/**)", and bare filenames match at any depth, so "Read(.env) and Read(**/.env) are equivalent".
Gemini CLI does something the other two do not document: it strips the host environment before a server starts. "By default, the CLI redacts sensitive environment variables from the base environment (inherited from the host process)", matching "*TOKEN*, *SECRET*, *PASSWORD*, *KEY*, *AUTH*, *CREDENTIAL*". The exception is the one you write yourself: variables set in a server's env "are trusted and are not subjected to the automatic redaction process." Codex keeps tokens out of the file with bearer_token_env_var, the "Environment variable sourcing the bearer token for an MCP HTTP server", and for cloud tasks "Secrets configured for cloud environments are available only during setup and are removed before the agent phase starts." The specification adds the server-side rule: "MCP servers MUST NOT accept any tokens that were not explicitly issued for the MCP server", and its scope-minimization section lists "Using wildcard or omnibus scopes (*, all, full-access)" among the common mistakes, which is the OAuth version of a bare Bash allow.
The auditor
The code box carries coding-agent-security-prompt-injection-mcp-secrets.py, just under 300 lines of standard-library Python. It reads Claude Code's .mcp.json and settings files, Gemini CLI's settings.json, and Codex's config.toml, and every check rests on a sentence quoted above: unpinned npx, uvx, and docker launches, literal secrets in env or headers, http:// endpoints off loopback, the bypasses (trust: true, enableAllProjectMcpServers, bypassPermissions, approval_policy = "never" with danger-full-access), and the broad allows (bare Bash, bare WebFetch, Bash(curl *), whole-server MCP rules). It never executes anything it reads.
--demo audits four embedded sample files. The .mcp.json among them:
{
"mcpServers": {
"docs": {
"type": "stdio",
"command": "uvx",
"args": [
"acme-docs-mcp==2.1.0"
]
},
"scraper": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"acme-scraper-mcp"
],
"env": {
"ACME_API_KEY": "ak_live_9f3c2e7d1b4a"
}
},
"tracker": {
"type": "http",
"url": "http://tracker.corp.example:8080/mcp",
"headers": {
"Authorization": "Bearer tr_8d1e5c0a"
}
},
"issues": {
"type": "http",
"url": "https://mcp.example.com/mcp",
"headers": {
"Authorization": "Bearer ${ISSUES_TOKEN}"
}
}
}
}
The run, with the nine low-level advisories cut for length:
$ python coding-agent-security-prompt-injection-mcp-secrets.py --demo
HIGH BYPASS demo/.claude/settings.json
enableAllProjectMcpServers approves every server in .mcp.json without a prompt
HIGH BYPASS demo/.codex/config.toml
approval_policy=never with danger-full-access: no prompt and no sandbox
HIGH UNPINNED demo/.codex/config.toml.mcp_servers.ci
npx resolves "acme-ci-mcp" to whatever version is newest at launch; pin it (acme-ci-mcp@x.y.z)
HIGH SECRET demo/.codex/config.toml.mcp_servers.ci.env.CI_TOKEN
CI_TOKEN holds a literal value in the config file; use a ${VAR} reference or bearer_token_env_var
HIGH BYPASS demo/.gemini/settings.json:mcpServers.browser
trust: true bypasses every tool-call confirmation for this server
HIGH UNPINNED demo/.mcp.json:mcpServers.scraper
npx resolves "acme-scraper-mcp" to whatever version is newest at launch; pin it (acme-scraper-mcp@x.y.z)
HIGH SECRET demo/.mcp.json:mcpServers.scraper.env.ACME_API_KEY
ACME_API_KEY holds a literal value in the config file; use a ${VAR} reference or bearer_token_env_var
HIGH SECRET demo/.mcp.json:mcpServers.tracker.headers.Authorization
Authorization holds a literal value in the config file; use a ${VAR} reference or bearer_token_env_var
HIGH PLAINTEXT demo/.mcp.json:mcpServers.tracker.url
http:// endpoint tracker.corp.example is not loopback; tokens and tool results cross the wire in clear
MEDIUM BROAD demo/.claude/settings.json.permissions.allow[Bash(curl *)]
network fetch commands are pre-approved; fetched text becomes tool output the model acts on
MEDIUM BROAD demo/.claude/settings.json.permissions.allow[WebFetch]
any URL can be fetched without a prompt
MEDIUM BROAD demo/.claude/settings.json.permissions.allow[mcp__scraper__*]
approves every tool of that server, including tools it adds later
21 findings: 9 high, 3 medium, 9 low
The exit status was 2, the script's code for at least one high finding, so it works as a CI step. The docs server, pinned with ==2.1.0, and the issues server, authenticated by reference, produced nothing above an advisory. The same script against the settings file shipped with the permissions article, which denies Read(./.env), curl, and wget and scopes WebFetch to one domain:
$ python coding-agent-security-prompt-injection-mcp-secrets.py coding-agent-permission-models-compared.json
0 findings: 0 high, 0 medium, 0 low
--write-demo <dir> writes the four samples to disk so you can edit them and watch findings appear and disappear, and pointing the script at a project directory finds the five conventional file names on its own.
What this does not cover
The auditor reads configuration, not behaviour. It cannot tell a benign server from a TrustShift server, and a pinned version is whatever the registry serves under that version; there is no signature or lockfile check. Secret detection is by name, so a token in a variable called SESSION passes, and any ${VAR} reference is treated as fine even when the server does not need it. I did not run an injection against any harness in this session, and AgentDojo's tasks are email, banking, and travel, not code. The Codex default_tools_approval_mode values are not listed on the page I read, so the script does not judge them, and I read allowedMcpServers and deniedMcpServers from the settings reference without exercising them. The Gemini CLI page's silence on prompt injection is a fact about one page on one day, not a fact about the tool.
Code and data
- coding-agent-security-prompt-injection-mcp-secrets.py — the complete listing used in this article.
Sources
- Anthropic, "Security" (Claude Code docs, read 2026-09-07)
- Anthropic, "Connect Claude Code to tools via MCP" (Claude Code docs, read 2026-09-07)
- Anthropic, "Configure permissions" (Claude Code docs, read 2026-09-07)
- Anthropic, "Settings reference" (Claude Code docs, read 2026-09-07)
- OpenAI, "Agent approvals & security" (Codex docs, read 2026-09-07)
- OpenAI, "Configuration Reference" (Codex docs, read 2026-09-07)
- Google, "MCP servers with Gemini CLI" (read 2026-09-07)
- Model Context Protocol, "Security Best Practices" (specification 2026-07-28, read 2026-09-07)
- OWASP, "Top 10 for LLM Applications, 2025 edition" (read 2026-09-07)
- OWASP, "LLM01:2025 Prompt Injection" (read 2026-09-07)
- OWASP, "LLM06:2025 Excessive Agency" (read 2026-09-07)
- Greshake et al., "Not what you've signed up for: Compromising Real-World LLM-Integrated Applications with Indirect Prompt Injection" (arXiv:2302.12173, read 2026-09-07)
- Debenedetti et al., "AgentDojo: A Dynamic Environment to Evaluate Prompt Injection Attacks and Defenses for LLM Agents" (arXiv:2406.13352, read 2026-09-07)
- Padilla, "Exposed by Design: A Dynamic Security Assessment of Internet-Facing MCP Servers at Scale" (arXiv:2608.00150, read 2026-09-07)
- Rostamzadeh et al., "TrustShiftProbe: Characterizing, Benchmarking, and Defending Staged Trust Attacks on MCP Servers" (arXiv:2608.23763, read 2026-09-07)
- anthropics/claude-code, "Release v2.1.263" (read 2026-09-07)
- openai/codex, "Release rust-v0.153.4" (read 2026-09-07)
- google-gemini/gemini-cli, "Release v0.58.0" (read 2026-09-07)