Agent-Aware CLI Behaviour via Environment Variable¶
A harness sets a well-known environment variable on agent-spawned subprocesses; a CLI that checks it switches to machine-readable output. The contract needs both ends.
When this pattern applies¶
The env-var contract does not replace system-prompt flag instructions. It pays off when:
- You control the CLI, or its maintainers honor a name your harness sets. CLIs that have never heard of
VSCODE_AGENTignore it. - The subprocess environment is preserved.
sudo,docker run,ssh, and CI runners strip env vars by default. Inheritance breaks at any boundary that does not pass-Eor--env VSCODE_AGENT. - Variable naming converges, or you check several. Every harness picks its own name today, so a CLI supporting "any agent" keeps an allow-list.
When those conditions hold, the agent sheds tool-specific flag knowledge. Otherwise, fall back to the agent-side override.
The contract¶
VS Code 1.121 (May 2026) ships the first widely deployed implementation. The release notes state verbatim: "VS Code now sets a VSCODE_AGENT environment variable for agent-initiated terminal commands." CLIs can "switch to machine-readable output, suppress progress animations, or skip prompts that would otherwise block the session" (VS Code v1.121 release notes).
Claude Code implements the same shape. Every spawned subprocess inherits CLAUDECODE=1, "Set to 1 in subprocesses Claude Code spawns (Bash and PowerShell tools, tmux sessions, hook commands, status line commands)" (Claude Code environment variables). An undocumented AI_AGENT=claude-code_<version>_agent is also observable in subprocess env.
graph LR
A[Agent harness] -->|sets VSCODE_AGENT=1| B[Subprocess]
B --> C{CLI checks env}
C -->|variable set| D[Machine-readable mode]
C -->|variable unset| E[Interactive default]
D --> F[No pager, no prompts, no animations]
The CLI side is a single conditional:
def is_agent() -> bool:
return bool(os.environ.get("VSCODE_AGENT") or os.environ.get("CLAUDECODE"))
if is_agent():
sys.stdout.reconfigure(line_buffering=True)
disable_pager()
disable_progress_bars()
emit_machine_readable_errors()
The agent side is harness configuration, not per-call work:
{
"env": {
"VSCODE_AGENT": "1",
"AGENT_MODE": "1"
}
}
Two-axis model¶
The pattern adds to CI=true, it does not replace it. VS Code positions it explicitly: "If you maintain scripts or CLIs that already adjust behavior for CI or other agents, you can use the same pattern for commands launched from Copilot Chat" (VS Code v1.121 release notes).
| Variable | Signals | Set by |
|---|---|---|
CI=true |
Non-interactive batch context | 50+ CI vendors (watson/ci-info) |
VSCODE_AGENT / CLAUDECODE |
Agent-initiated execution inside a developer session | Agent harness |
NO_COLOR / FORCE_COLOR |
Colour preference | User or harness |
GIT_TERMINAL_PROMPT=0 |
Per-tool prompt suppression | User or harness |
CI runs are non-interactive with no user present. Agent runs are non-interactive with the user watching, and may want richer error detail than a CI mode would emit. Collapsing them to one axis loses that distinction.
Why it works¶
The contract inverts the direction of CLI-specific knowledge. Today, the agent's prompt carries N flags per CLI ("--no-pager for git, CI=true for npm, -y for apt"). It sits on the side that changes most often and aims at a moving target. The env-var contract moves that knowledge to the CLI's own source, where its maintainer already tracks which subcommands prompt and where pagers launch. The harness declares context once, the CLI decides behavior, the contract survives flag renames on either side (VS Code v1.121 release notes). This is the mechanism that made CI=true succeed: ci-info catalogues 50+ vendors that set it and CLIs (npm, gh, gcloud, pip) that branch on it (watson/ci-info).
Example¶
Claude Code sets two variables on every subprocess it spawns. Running env | grep -E "(CLAUDE|AGENT)" inside a Claude Code session returns:
AI_AGENT=claude-code_2-1-145_agent
CLAUDECODE=1
CLAUDE_CODE_SESSION_ID=<uuid>
A CLI written to honour the contract checks one of these variables and adapts:
# Before — agent's system prompt has to remember flags
# "Always run git with --no-pager"
# "Always pass --no-progress to npm"
# "Always set CI=true before pip"
# After — CLI checks the harness signal once
if os.environ.get("VSCODE_AGENT") or os.environ.get("CLAUDECODE"):
args = ["--no-pager"] + args # for git wrapper
suppress_progress = True
auto_yes_safe_prompts = True
The same CLI binary serves both human and agent callers; the system prompt no longer encodes per-tool flag knowledge.
When this backfires¶
Reach for the agent-side override or headless mode when:
- The CLI has not adopted any agent variable. Setting
VSCODE_AGENT=1against a tool that ignores it does nothing. Coverage is sparse, so the long tail still needs--no-pagerin the prompt. - Env vars do not propagate.
sudo,docker run,ssh, and pruning CI runners break the chain. The agent must avoid the boundary or passsudo -E,--env VSCODE_AGENT, or-o SendEnv=VSCODE_AGENT— the exact tool-specific prompt knowledge the contract was meant to eliminate. CI=truealready covers the surface. Many CLIs (npm,gh,apt-get) already honorCI=true, so a second variable is redundant for those.- The naming has not converged. Until the industry agrees on a vendor-neutral name, CLI authors face an N-variable check and users see inconsistent behavior.
- The signal needs to be load-bearing for safety. Env-var presence is not authenticated, and any process can set
VSCODE_AGENT=1. Treat the variable as a behavioral hint, not a permission gate. - The branch itself becomes an injection surface. The env-var channel is injectable: Cursor's allowlist bypass (CVE-2026-22708) let a prompt-injection payload run shell built-ins like
exportto poison environment variables and steer trusted commands (CVE-2026-22708 — NVD). If a CLI auto-confirms prompts on the agent variable, an attacker who sets it inherits that lever. Keep the agent branch cosmetic — pagers, color, progress bars — never auto-confirmation of consequential actions.
Key Takeaways¶
- The harness sets a well-known env var on every agent-initiated subprocess; CLIs that honour it switch to machine-readable mode without per-call flag knowledge in the prompt
- VS Code 1.121 ships
VSCODE_AGENT; Claude Code setsCLAUDECODE=1andAI_AGENT; the shape is the same, the names diverge - The contract is additive to
CI=true, not a replacement — agent runs are non-interactive but the user is watching - The pattern earns its keep when you control both the harness and the CLI; against third-party CLIs that have not adopted any variable, you still need the agent-side flag override
- Treat the variable as a behavioural hint, not an authorisation signal — any process can set it