Skip to content

Commit 561d9f9

Browse files
BagToadCopilot
andauthored
Detect additional third-party coding agents (cli#13722)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 015e6cf commit 561d9f9

2 files changed

Lines changed: 190 additions & 13 deletions

File tree

internal/agents/detect.go

Lines changed: 95 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,28 @@ import (
44
"fmt"
55
"os"
66
"regexp"
7+
"strings"
78
)
89

910
// AgentName is a validated agent identifier safe for use in HTTP headers.
1011
type AgentName string
1112

1213
const (
13-
agentAmp AgentName = "amp"
14-
agentClaudeCode AgentName = "claude-code"
15-
agentCodex AgentName = "codex"
16-
agentCopilotCLI AgentName = "copilot-cli"
17-
agentGeminiCLI AgentName = "gemini-cli"
18-
agentOpencode AgentName = "opencode"
14+
agentAmp AgentName = "amp"
15+
agentClaudeCode AgentName = "claude-code"
16+
agentCodex AgentName = "codex"
17+
agentCopilotCLI AgentName = "copilot-cli"
18+
agentGeminiCLI AgentName = "gemini-cli"
19+
agentOpencode AgentName = "opencode"
20+
agentAntigravity AgentName = "antigravity"
21+
agentAugmentCLI AgentName = "augment-cli"
22+
agentReplit AgentName = "replit"
23+
agentGoose AgentName = "goose"
24+
agentCowork AgentName = "cowork"
25+
agentCursor AgentName = "cursor"
26+
agentCursorCLI AgentName = "cursor-cli"
27+
agentKiro AgentName = "kiro"
28+
agentPi AgentName = "pi"
1929
)
2030

2131
var validAgentName = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
@@ -46,7 +56,7 @@ func detectWith(lookup func(string) (string, bool)) AgentName {
4656
return v
4757
}
4858

49-
// Generic agent identifiers checked first because they are the most specific signal.
59+
// Generic agent identifiers - checked first because they are the most specific signal.
5060
if v, ok := lookup("AI_AGENT"); ok && v != "" {
5161
if name, err := parseAgentName(v); err == nil {
5262
return name
@@ -60,15 +70,15 @@ func detectWith(lookup func(string) (string, bool)) AgentName {
6070
return agentAmp
6171
}
6272

63-
// OpenAI Codex CLI https://github.com/openai/codex
73+
// OpenAI Codex CLI - https://github.com/openai/codex
6474
// CODEX_SANDBOX: https://github.com/openai/codex/blob/95e1d5993985019ce0ce0d10689caf1375f95120/codex-rs/core/src/spawn.rs#L25
6575
// CODEX_THREAD_ID: https://github.com/openai/codex/blob/95e1d5993985019ce0ce0d10689caf1375f95120/codex-rs/core/src/exec_env.rs#L8
6676
// CODEX_CI: https://github.com/openai/codex/blob/95e1d5993985019ce0ce0d10689caf1375f95120/codex-rs/core/src/unified_exec/process_manager.rs#L64
6777
if isSet("CODEX_SANDBOX") || isSet("CODEX_CI") || isSet("CODEX_THREAD_ID") {
6878
return agentCodex
6979
}
7080

71-
// Google Gemini CLI https://github.com/google-gemini/gemini-cli
81+
// Google Gemini CLI - https://github.com/google-gemini/gemini-cli
7282
// GEMINI_CLI: https://github.com/google-gemini/gemini-cli/blob/46fd7b4864111032a1c7dfa1821b2000fc7531da/docs/tools/shell.md#L96-L97
7383
if isSet("GEMINI_CLI") {
7484
return agentGeminiCLI
@@ -80,20 +90,92 @@ func detectWith(lookup func(string) (string, bool)) AgentName {
8090
return agentCopilotCLI
8191
}
8292

83-
// OpenCode https://github.com/anomalyco/opencode
93+
// OpenCode - https://github.com/anomalyco/opencode
8494
// OPENCODE: https://github.com/anomalyco/opencode/blob/fde201c286a83ff32dda9b41d61d734a4449fe70/packages/opencode/src/index.ts#L78-L80
95+
// Not OPENCODE_CALLER or OPENCODE_CLIENT: they name the client that launched
96+
// opencode (e.g. the VS Code extension), not the running agent.
8597
if isSet("OPENCODE") {
8698
return agentOpencode
8799
}
88100

89-
// Anthropic Claude Code — https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview
101+
// Antigravity
102+
// No first-party docs
103+
if isSet("ANTIGRAVITY_AGENT") {
104+
return agentAntigravity
105+
}
106+
107+
// Augment CLI
108+
// No first-party docs
109+
if isSet("AUGMENT_AGENT") {
110+
return agentAugmentCLI
111+
}
112+
113+
// Replit
114+
// REPL_ID is present throughout any Replit environment, not only when a
115+
// Replit agent is driving the CLI, so it is a broad, low-confidence signal.
116+
// REPL_ID: https://github.com/replit/go-replidentity/blob/2966ea2d227d572f6054ee8f077ad16a1be02663/examples/extract.go#L25
117+
if isSet("REPL_ID") {
118+
return agentReplit
119+
}
120+
121+
// Anthropic Claude Code - https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview
90122
// CLAUDECODE: https://code.claude.com/docs/en/env-vars (CLAUDECODE section)
91-
// Checked last because other agents (e.g. Amp) set CLAUDECODE=1 alongside their own vars.
92-
if isSet("CLAUDECODE") {
123+
// CLAUDE_CODE, CLAUDE_CODE_IS_COWORK: no first-party docs
124+
//
125+
// Cowork is a Claude Code mode that also sets CLAUDECODE, so it is checked
126+
// first to win over the generic Claude Code signal below.
127+
if isSet("CLAUDE_CODE_IS_COWORK") {
128+
return agentCowork
129+
}
130+
131+
// Claude Code is checked after Amp and Cowork, which also set CLAUDECODE, so
132+
// those more specific agents are detected first.
133+
if isSet("CLAUDECODE") || isSet("CLAUDE_CODE") {
93134
// There is a CLAUDE_CODE_ENTRYPOINT env var that is set to `cli` or `desktop` etc, but it's not documented
94135
// so we don't want to rely on it too heavily. We'll just return a generic claude-code agent name.
95136
return agentClaudeCode
96137
}
97138

139+
// Cursor
140+
// No first-party docs
141+
// CURSOR_TRACE_ID (IDE) takes precedence over the Cursor CLI signal below.
142+
if isSet("CURSOR_TRACE_ID") {
143+
return agentCursor
144+
}
145+
146+
// Cursor CLI
147+
// No first-party docs
148+
if isSet("CURSOR_AGENT") || valueOf("CURSOR_EXTENSION_HOST_ROLE") == "agent-exec" {
149+
return agentCursorCLI
150+
}
151+
152+
// Single-source signals matched against one environment variable. These
153+
// carry lower corroboration than the presence-based agents above, so they
154+
// are checked after them.
155+
156+
// Kiro
157+
// No first-party docs
158+
if valueOf("TERM_PROGRAM") == "kiro" {
159+
return agentKiro
160+
}
161+
162+
// Pi
163+
// No first-party docs
164+
// Anchored to a path separator so it only matches ".pi/agent" as a real
165+
// path segment, not an incidental substring. The Windows separator is
166+
// matched too, though confidence there is lower since it is unconfirmed
167+
// that pi uses this layout on Windows.
168+
if strings.Contains(valueOf("PATH"), "/.pi/agent") || strings.Contains(valueOf("PATH"), `\.pi\agent`) {
169+
return agentPi
170+
}
171+
172+
// Goose is checked last because GOOSE_PROVIDER only indicates that Goose is
173+
// configured as a model provider, not that it is driving the CLI, so any
174+
// more specific signal above should win.
175+
// GOOSE_PROVIDER: https://github.com/aaif-goose/goose/blob/48a2a3d1804ae75eb7b208a5d0d73fd976511b80/crates/goose/src/config/providers.rs#L93
176+
if isSet("GOOSE_PROVIDER") {
177+
return agentGoose
178+
}
179+
98180
return ""
99181
}

internal/agents/detect_test.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,101 @@ func TestDetectWith(t *testing.T) {
138138
env: map[string]string{"AI_AGENT": "bad agent", "GEMINI_CLI": "1"},
139139
wantAgent: "gemini-cli",
140140
},
141+
{
142+
name: "ANTIGRAVITY_AGENT",
143+
env: map[string]string{"ANTIGRAVITY_AGENT": "1"},
144+
wantAgent: "antigravity",
145+
},
146+
{
147+
name: "AUGMENT_AGENT",
148+
env: map[string]string{"AUGMENT_AGENT": "1"},
149+
wantAgent: "augment-cli",
150+
},
151+
{
152+
name: "REPL_ID",
153+
env: map[string]string{"REPL_ID": "abc123"},
154+
wantAgent: "replit",
155+
},
156+
{
157+
name: "GOOSE_PROVIDER",
158+
env: map[string]string{"GOOSE_PROVIDER": "anthropic"},
159+
wantAgent: "goose",
160+
},
161+
{
162+
name: "claude-code takes priority over goose",
163+
env: map[string]string{"GOOSE_PROVIDER": "anthropic", "CLAUDECODE": "1"},
164+
wantAgent: "claude-code",
165+
},
166+
{
167+
name: "kiro takes priority over goose",
168+
env: map[string]string{"GOOSE_PROVIDER": "anthropic", "TERM_PROGRAM": "kiro"},
169+
wantAgent: "kiro",
170+
},
171+
{
172+
name: "CLAUDE_CODE_IS_COWORK detected as cowork",
173+
env: map[string]string{"CLAUDE_CODE_IS_COWORK": "1"},
174+
wantAgent: "cowork",
175+
},
176+
{
177+
name: "cowork takes priority over CLAUDECODE",
178+
env: map[string]string{"CLAUDE_CODE_IS_COWORK": "1", "CLAUDECODE": "1"},
179+
wantAgent: "cowork",
180+
},
181+
{
182+
name: "CLAUDE_CODE",
183+
env: map[string]string{"CLAUDE_CODE": "1"},
184+
wantAgent: "claude-code",
185+
},
186+
{
187+
name: "CURSOR_TRACE_ID detected as cursor",
188+
env: map[string]string{"CURSOR_TRACE_ID": "abc"},
189+
wantAgent: "cursor",
190+
},
191+
{
192+
name: "CURSOR_AGENT detected as cursor-cli",
193+
env: map[string]string{"CURSOR_AGENT": "1"},
194+
wantAgent: "cursor-cli",
195+
},
196+
{
197+
name: "CURSOR_EXTENSION_HOST_ROLE agent-exec detected as cursor-cli",
198+
env: map[string]string{"CURSOR_EXTENSION_HOST_ROLE": "agent-exec"},
199+
wantAgent: "cursor-cli",
200+
},
201+
{
202+
name: "CURSOR_EXTENSION_HOST_ROLE with other value is ignored",
203+
env: map[string]string{"CURSOR_EXTENSION_HOST_ROLE": "worker"},
204+
wantAgent: "",
205+
},
206+
{
207+
name: "CURSOR_TRACE_ID takes priority over CURSOR_AGENT",
208+
env: map[string]string{"CURSOR_TRACE_ID": "abc", "CURSOR_AGENT": "1"},
209+
wantAgent: "cursor",
210+
},
211+
{
212+
name: "TERM_PROGRAM kiro detected as kiro",
213+
env: map[string]string{"TERM_PROGRAM": "kiro"},
214+
wantAgent: "kiro",
215+
},
216+
{
217+
name: "TERM_PROGRAM with kiro as a substring is ignored",
218+
env: map[string]string{"TERM_PROGRAM": "kirostudio"},
219+
wantAgent: "",
220+
},
221+
{
222+
name: "PATH containing .pi/agent detected as pi",
223+
env: map[string]string{"PATH": "/usr/bin:/home/user/.pi/agent/bin"},
224+
wantAgent: "pi",
225+
},
226+
{
227+
name: "PATH with .pi/agent not on a path boundary is ignored",
228+
env: map[string]string{"PATH": "/usr/bin:/home/user/x.pi/agent"},
229+
wantAgent: "",
230+
},
231+
{
232+
name: "PATH with Windows .pi\\agent separators detected as pi",
233+
env: map[string]string{"PATH": `C:\Windows;C:\Users\user\.pi\agent\bin`},
234+
wantAgent: "pi",
235+
},
141236
}
142237

143238
for _, tt := range tests {

0 commit comments

Comments
 (0)