Detect whether a CLI is being invoked by an AI coding agent, and identify which one.
Follows the AI_AGENT environment variable proposal (originally AGENT, which it keeps as a legacy fallback). Lets your CLI adapt its output — structured errors, more verbose tracebacks, no interactive prompts — when it's running under an agent rather than a human.
Detection provides cooperative attribution from environment variables and a small filesystem check. Inherited markers do not prove that a model initiated this particular command, and no match does not prove a human caller. It identifies the harness, not the model selected inside it.
[dependencies]
is-ai-agent = "0.7"The crate also ships an is-ai-agent binary. Prebuilt binaries for Linux
(musl, x86_64/aarch64), macOS (x86_64/aarch64) and Windows (x86_64/aarch64)
are attached to each GitHub release.
mise use -g github:sdairs/is-ai-agent # or: cargo install is-ai-agentIt is silent by default and answers through its exit status:
| Exit | Meaning |
|---|---|
0 |
an AI agent was detected |
1 |
no AI agent was detected |
2 |
usage error |
if is-ai-agent; then echo "i am AI"; fi-o, --output [<FORMAT>] prints details to stdout without changing the exit status.
A bare --output defaults to id:
| Format | Agent detected | No agent |
|---|---|---|
id (default) |
claude-code |
(nothing) |
name |
Claude Code |
(nothing) |
text |
Claude Code (claude-code) detected via env CLAUDECODE=1 |
no AI agent detected |
json |
full result (below) | same keys, null values |
agent=$(is-ai-agent --output) && echo "running under $agent"
is-ai-agent -o json | jq .{
"detected": true,
"id": "claude-code",
"name": "Claude Code",
"signal": { "type": "env", "name": "CLAUDECODE", "value": "1" },
"session_id": null,
"trace_id": null,
"traceparent": null
}signal is {"type":"file","path":"/opt/.devin"} for filesystem signals.
use is_ai_agent::{detect, is_ai_agent};
if is_ai_agent() {
// emit structured/agent-friendly output
}
if let Some(agent) = detect() {
eprintln!("running under {}", agent.name);
// URL-safe slug, e.g. "claude-code"
let slug = agent.id.as_str();
// Opaque correlation id; its scope depends on the harness.
if let Some(session) = &agent.session_id {
eprintln!("session: {session}");
}
// Active W3C trace context, when present — forward it downstream.
if let Some(trace_id) = agent.trace_id() {
eprintln!("trace: {trace_id}");
}
}Agent::session_id surfaces a harness-provided correlation identifier. A downstream tool can attach it to outbound requests (headers, query comments), but session, thread, task and run identifiers have different lifetimes.
The value is opaque: pair it with agent.id, and account for the scope below before correlating. None means no supported identifier was available for the detected identity. Identity and metadata are read on every detection call; there is no cross-call session cache.
| Agent | Source env var | Notes |
|---|---|---|
| Claude Code / Claude Cowork | CLAUDE_CODE_SESSION_ID |
Tool/hook children track /clear; long-lived MCP servers can retain their startup ID. See lifecycle notes below. |
| CodeBuddy | CODEBUDDY_SESSION_ID, then CLAUDE_SESSION_ID |
Dual-written to a CLAUDE_* mirror. |
| OpenAI Codex | CODEX_THREAD_ID |
Thread correlation only. CODEX_SESSION_ID identifies the shared root session and is not returned in this field. |
| Amp | AMP_CURRENT_THREAD_ID, then AGENT_THREAD_ID |
|
| Qwen Code | QWEN_CODE_SESSION_ID |
|
| Cursor / Cursor CLI | CURSOR_TRACE_ID |
Trace id; per-session vs per-command scope is undocumented, so correlation may fragment. |
| GitHub Copilot | COPILOT_AGENT_SESSION_ID, then COPILOT_AGENT_JOB_ID |
Job id is set by the Actions-based cloud coding agent. |
| Warp | OZ_RUN_ID |
Run id; scope undocumented, so correlation may fragment. |
| Cline | CLINE_TASK_ID |
|
| Roo Code | ROO_CODE_TASK_ID |
|
| DeepSeek Harness | DSH_SESSION_ID |
Nonblank, only after identity is established; the human web terminal also receives this variable. |
| Hermes Agent | HERMES_SESSION_ID |
Nonblank session ID, not HERMES_SESSION_THREAD_ID messaging-thread routing metadata. |
| Grok Build | GROK_SESSION_ID |
Nonblank; documented for tool commands and MCP servers since 1.0.4. |
| Pi | PI_SESSION_ID |
Nonblank; injected into agent shell-tool commands, not human ! / !! commands. |
New session sources reject whitespace-only values and preserve accepted values exactly. DSH_PTY_SESSION_ID, KILO_RUN_ID, HERMES_SESSION_THREAD_ID and trace context are not substitutes for these session IDs. Junie's observed MATTERHORN_SESSION_ID remains a research lead. There is no GEMINI_CLI_SESSION_ID or OPENCODE_SESSION_ID rule.
With only CODEX_SESSION_ID, detection returns Codex with session_id=None; when CODEX_THREAD_ID is also present, the thread ID is returned. CODEX_VERSION and CODEX_PERMISSION_PROFILE are metadata, not identity rules or permission guarantees. See the Codex subprocess contract.
For Claude, Bash/PowerShell tools and hooks receive the current conversation ID, including after /clear. A stdio MCP server keeps its spawn-time value; implicit resume/continue can leave it with a startup ID. Bridge/cloud IDs use different namespaces and are not substituted. The child marker covers tools, hooks and statusline commands, not stdio MCP servers; the broader CLAUDECODE marker can also appear in IDE terminals. See Claude's environment reference.
Pi's CLI/RPC identity markers and shell-tool session injection have different scope: SDK embedding does not automatically set the former, and human shell commands do not receive the latter. See the Pi environment contract.
When the agent runs under OpenTelemetry tracing, it may publish a TRACEPARENT to its subprocesses. Agent::traceparent exposes the raw W3C value (forward it as a traceparent header to keep downstream requests on the same distributed trace), and Agent::trace_id() extracts just the 32-hex trace-id correlation key.
TRACEPARENT is read after an identity matches; it is not itself an identity or session signal. It may be inherited from the ambient environment. Claude Code and Qwen Code both support propagation:
- Claude requires telemetry and enhanced tracing (
CLAUDE_CODE_ENABLE_TELEMETRY=1andCLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1). For a customANTHROPIC_BASE_URL, additionally setCLAUDE_CODE_PROPAGATE_TRACEPARENT=1; the direct Anthropic route does not require that extra flag. See Claude tracing. - Qwen Code gates propagation on telemetry and
outboundCorrelation.propagateTraceContext. Its shell context is prepared per session. A trace ID must not be treated as a conversation ID.
For tests or callers that want to consult a captured environment instead of the live process, use detect_with:
use is_ai_agent::detect_with;
let agent = detect_with(
|name| if name == "AI_AGENT" { Some("goose".into()) } else { None },
|_| false,
);-
The standard
AI_AGENT(the agents.md proposal, also read first by @vercel/detect-agent) and then the legacyAGENTenv var, when their value names a known agent. If both name known agents,AI_AGENTwins. Existing canonical slugs aregoose,amp,claude-code,cowork,cursor,cursor-cli,gemini-cli,codex,augment,cline,opencode,trae,devin,replit,antigravity,github-copilot,crush,qwen-code,iflow-cli,amazon-q-cli,roo-code,codebuddy,grok-cli,warp,pi,kiro,firebender,openhands,vecli, andv0; the seven new slugs and aliases are below. Generic aliases are explicit caller assertions, not claims that vendors export every spelling. A known outer identity wins over an inner harness marker. Whole-name matching comes before existing@version/ underscore suffix handling, sogithub_copilot_vscode_agent, the newgithub_copilot_app_agent,goose@1.2.3, andclaude-code_2-1-201_agentclassify correctly.Blank/whitespace-only values and trimmed
0,false,no, oroff(case-insensitive) are ignored. They do not disable other detection:AGENT=falsewithKILO=1still identifies Kilo Code. -
Tool-specific env vars:
New rules use the exact predicates below. Exact means case-sensitive, without trimming; nonblank rejects whitespace-only values and preserves the original matched value.
Variant / display name Canonical slug; extra generic aliases Automatic detection Evidence / scope DeepSeekHarness/ DeepSeek Harnessdeepseek-harness;dshExact DSH_SHELL=1Shell registry, agent PTY; inspected model tools and persistent terminal backend. Hermes/ Hermes Agenthermes-agent;hermesExact HERMES_AGENT=true, then nonblankHERMES_SESSION_IDfallbackSubprocess contract. OpenClaw/ OpenClawopenclawExact OPENCLAW_SHELL=execExec contract; tui-local,acp-client,acpand other values do not match.GrokBuild/ Grok Buildgrok-buildNonblank GROK_SESSION_ID1.0.4 changelog, tool commands and MCP servers. Separate from legacy Grok CLI and Grok Bot. KiloCode/ Kilo Codekilo-code;kilo,kilocodeExact KILO=1, before OpenCodeCLI setup; inspected CLI, not every IDE surface. Junie/ JuniejunieNonblank JUNIE_SHIM_PATHOfficial launcher; shim-launched ancestry, without a human-command distinction. No JUNIE_DATAfallback.VTCode/ VTCodevtcodeExact VTCODE=1Bootstrap. Existing identities gain exact
CLAUDE_CODE_CHILD_SESSION=1before broader Claude markers, nonblankCODEX_SESSION_IDas a Codex fallback, and nonblankPI_SESSION_IDas a Pi fallback. Other retained rules are listed below. Unless a predicate is written explicitly, these legacy rules accept any nonempty value; this refresh does not certify or tighten every legacy rule.Variable Agent CLAUDECODE,CLAUDE_CODE_ENTRYPOINT,CLAUDE_CODE_SESSION_ID,CLAUDE_CODE_EXECPATHClaude Code CLAUDE_CODE_IS_COWORK(refines any Claude Code identification when nonempty)Claude Cowork CODEBUDDY,CODEBUDDY_SESSION_ID,CODEBUDDY_PROJECT_DIR(checked before Claude Code — CodeBuddy mirrors someCLAUDE_*vars)CodeBuddy CURSOR_AGENT(set by both the CLI and the IDE's agent terminals)Cursor CURSOR_TRACE_IDandPAGER=head -n 10000 | cat(older builds; the PAGER override distinguishes agent mode from a human in Cursor's terminal)Cursor CURSOR_SANDBOX,CURSOR_EXTENSION_HOST_ROLE=agent-execCursor CLI GEMINI_CLIGemini CLI QWEN_CODEQwen Code VECLI_SANDBOX,VECLI_DIR(checked beforeGEMINI_CLI— veCLI is a Gemini CLI fork)veCLI CODEX_THREAD_ID,CODEX_SANDBOX,CODEX_SANDBOX_NETWORK_DISABLED,CODEX_CIOpenAI Codex ANTIGRAVITY_AGENT,ANTIGRAVITY_PROJECT_IDAntigravity AUGMENT_AGENTAugment CLINE_ACTIVE,CLINE_TASK_IDCline ROO_CODE_TASK_IDRoo Code CRUSHCrush IFLOW_CLIiFlow CLI OZ_RUN_IDWarp PI_CODING_AGENTPi KIRO_AGENT_PATHKiro AGENT_CONTEXT_OUTandAGENT_DISPLAY_OUT(the Kiro CLI exports this FIFO pair only while its agent drives the command; either alone is too generic)Kiro FIREBENDER_TERMINALFirebender PS1orPROMPT_COMMANDcontaining###PS1JSON###OpenHands OPENCODE,OPENCODE_PID,OPENCODE_BIN_PATH,OPENCODE_SERVER,OPENCODE_APP_INFO,OPENCODE_MODES,OPENCODE_CLIENTOpenCode TRAE_AI_SHELL_IDTRAE AI GOOSE_TERMINALGoose AMP_CURRENT_THREAD_IDAmp COPILOT_AGENT_SESSION_ID,COPILOT_AGENT,COPILOT_CLI,COPILOT_AGENT_JOB_IDGitHub Copilot AWS_EXECUTION_ENVcontainingAmazonQ-For-CLIAmazon Q Developer CLI Ordering disambiguates compatibility shims and forks: Amp and CodeBuddy precede Claude, Cowork refinement is preserved, Qwen Code and veCLI precede Gemini CLI, and Kilo precedes OpenCode. Known generic names still win over these markers; bare generic true values do not.
-
Filesystem signals:
Path Agent /opt/.devinDevin -
An unknown nonblank
AI_AGENTname, or a bare true (1,true,yes,on)AI_AGENTorAGENTvalue, as a last resort, resolving toAgentId::Unknown. An unknownAGENTname such asAGENT=build-01is ignored, because the name is generic enough to collide with unrelated tooling. Tool-specific and filesystem signals outrank it, soAGENT=1,KILO=1, andOPENCODE=1together identify Kilo Code.
The detected Agent carries the Signal that matched, so callers can see exactly how detection fired.
COPILOT_GITHUB_TOKEN, COPILOT_MODEL, COPILOT_ALLOW_ALL, bare REPL_ID, and bare GROK_AGENT no longer trigger detection. Credentials, configuration and a workspace ID are insufficient; matching a token also copied it into the public/debug result. GROK_AGENT is a profile selector. GrokCli and Replit remain available through their existing generic aliases (grok / grok-cli and replit). No REPLIT_MODE replacement is added.
DSH_SESSION_ID alone is insufficient because the human web terminal also receives it. DEEPSEEK_API_KEY, DSH_HOME, DSH_PROFILE, and DSH_PROFILE_DIR are not identity rules. The inspected DeepSeek tools supply fresh tool-owned values; arbitrary plugin propagation is not assumed. Selecting a DeepSeek or Qwen model in another harness does not identify that harness as DeepSeek Harness or Qwen Code.
Qwen Live Harness delegates to other coding harnesses. Its settings and QWEN_CODE_NO_RELAUNCH do not establish Qwen Code execution or a separate Qwen Live identity; the underlying executor is identified when its own marker is present. No new model, version, root-session, confidence, or multi-match fields are added.
The captured inventory records exact environments from real command tools using a scripted provider. The harness README explains how to replay it against the detector; the discovery report explains pinned versions, controls and limits. Six new identities were observed there: DeepSeek Harness, Hermes Agent, OpenClaw, Kilo Code, Junie and VTCode. Grok Build is supported from vendor documentation, without a live probe in this inventory. Source inspection and those controlled runs do not establish every installed version, operating system, IDE, human-command, nested or resumed-session path.
Cline CLI's observed wrapper/connector variables also appear in a non-agent command control. Goose 1.52.0 exposes generic AGENT_SESSION_ID, while its provider/model variables are inherited configuration. Those observations do not justify new identity rules; both detection gaps remain. Junie's shim path identifies launcher ancestry, and the attempted interactive human-command control was inconclusive. The retained legacy rules remain subject to separate evidence review, as recorded in issue #5.
Dual-licensed under MIT or Apache-2.0, at your option.