From 43989495a1b34cbec36903e07c12fce87704361a Mon Sep 17 00:00:00 2001 From: Alberto Gimeno <50486+gimenete@users.noreply.github.com> Date: Mon, 10 Aug 2026 09:25:44 +0200 Subject: [PATCH 1/4] fix: avoid agent detection from host process names Match parent-process fallbacks against exact executable basenames so desktop hosts and installation paths containing agent terms do not force non-interactive behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../runcontext/agentdetect/detect_process.go | 42 ++++------ .../runcontext/agentdetect/detect_test.go | 84 ++++++++++++++++--- 2 files changed, 89 insertions(+), 37 deletions(-) diff --git a/cli/azd/internal/runcontext/agentdetect/detect_process.go b/cli/azd/internal/runcontext/agentdetect/detect_process.go index a81b7eba4a5..73fce0b7398 100644 --- a/cli/azd/internal/runcontext/agentdetect/detect_process.go +++ b/cli/azd/internal/runcontext/agentdetect/detect_process.go @@ -6,14 +6,13 @@ package agentdetect import ( "log" "os" - "path/filepath" "strings" ) -// processNamePatterns maps process name patterns to agent types. -// Patterns are matched case-insensitively against process names and executable paths. +// processNamePatterns maps exact process executable names to agent types. +// Names are matched case-insensitively after removing a Windows executable extension. var processNamePatterns = []struct { - patterns []string // lowercase patterns to match + patterns []string // lowercase executable names to match agentType AgentType }{ // Claude Code (Anthropic) - installed via npm, homebrew, or direct download @@ -86,18 +85,12 @@ func matchProcessToAgent(info parentProcessInfo) AgentInfo { return NoAgent() } - nameLower := strings.ToLower(info.Name) - execLower := strings.ToLower(info.Executable) - execBaseLower := strings.ToLower(filepath.Base(info.Executable)) - - // Remove common executable extensions for matching - nameLower = strings.TrimSuffix(nameLower, ".exe") - execBaseLower = strings.TrimSuffix(execBaseLower, ".exe") + nameLower := normalizeProcessName(info.Name) + execBaseLower := normalizeProcessName(info.Executable) for _, entry := range processNamePatterns { for _, pattern := range entry.patterns { - // Check against process name - if nameLower == pattern || strings.Contains(nameLower, pattern) { + if nameLower == pattern { return AgentInfo{ Type: entry.agentType, Name: entry.agentType.DisplayName(), @@ -107,19 +100,7 @@ func matchProcessToAgent(info parentProcessInfo) AgentInfo { } } - // Check against executable base name - if execBaseLower == pattern || strings.Contains(execBaseLower, pattern) { - return AgentInfo{ - Type: entry.agentType, - Name: entry.agentType.DisplayName(), - Source: DetectionSourceParentProcess, - Detected: true, - Details: info.Executable, - } - } - - // Check if pattern appears in full executable path (for detection via install paths) - if strings.Contains(execLower, pattern) { + if execBaseLower == pattern { return AgentInfo{ Type: entry.agentType, Name: entry.agentType.DisplayName(), @@ -133,3 +114,12 @@ func matchProcessToAgent(info parentProcessInfo) AgentInfo { return NoAgent() } + +func normalizeProcessName(processPath string) string { + normalizedPath := strings.ReplaceAll(processPath, "\\", "/") + if index := strings.LastIndex(normalizedPath, "/"); index >= 0 { + normalizedPath = normalizedPath[index+1:] + } + + return strings.TrimSuffix(strings.ToLower(normalizedPath), ".exe") +} diff --git a/cli/azd/internal/runcontext/agentdetect/detect_test.go b/cli/azd/internal/runcontext/agentdetect/detect_test.go index d6f05384297..dd395acad3c 100644 --- a/cli/azd/internal/runcontext/agentdetect/detect_test.go +++ b/cli/azd/internal/runcontext/agentdetect/detect_test.go @@ -5,6 +5,7 @@ package agentdetect import ( "os" + "strings" "testing" "github.com/azure/azure-dev/cli/azd/internal" @@ -254,13 +255,11 @@ func TestMatchProcessToAgent(t *testing.T) { name string processInfo parentProcessInfo expectedAgent AgentType - detected bool }{ { name: "Empty process info", processInfo: parentProcessInfo{}, expectedAgent: AgentTypeUnknown, - detected: false, }, { name: "Claude process name", @@ -268,7 +267,6 @@ func TestMatchProcessToAgent(t *testing.T) { Name: "claude", }, expectedAgent: AgentTypeClaudeCode, - detected: true, }, { name: "Claude Code process name", @@ -276,15 +274,27 @@ func TestMatchProcessToAgent(t *testing.T) { Name: "claude-code", }, expectedAgent: AgentTypeClaudeCode, - detected: true, }, { - name: "GitHub Copilot CLI", + name: "GitHub Copilot CLI process name", processInfo: parentProcessInfo{ Name: "gh-copilot", }, expectedAgent: AgentTypeGitHubCopilotCLI, - detected: true, + }, + { + name: "GitHub Copilot CLI Windows executable", + processInfo: parentProcessInfo{ + Executable: `C:\Users\example\AppData\Local\Programs\GitHub CLI\gh-copilot.exe`, + }, + expectedAgent: AgentTypeGitHubCopilotCLI, + }, + { + name: "GitHub Copilot CLI executable variants", + processInfo: parentProcessInfo{ + Executable: "/usr/local/bin/github-copilot-cli", + }, + expectedAgent: AgentTypeGitHubCopilotCLI, }, { name: "Gemini process", @@ -292,7 +302,6 @@ func TestMatchProcessToAgent(t *testing.T) { Name: "gemini", }, expectedAgent: AgentTypeGemini, - detected: true, }, { name: "OpenCode process", @@ -300,7 +309,6 @@ func TestMatchProcessToAgent(t *testing.T) { Name: "opencode", }, expectedAgent: AgentTypeOpenCode, - detected: true, }, { name: "Unknown process", @@ -309,7 +317,29 @@ func TestMatchProcessToAgent(t *testing.T) { Executable: "/bin/bash", }, expectedAgent: AgentTypeUnknown, - detected: false, + }, + { + name: "GitHub Copilot desktop app is not Copilot CLI", + processInfo: parentProcessInfo{ + Name: "GitHub Copilot.exe", + Executable: `C:\Users\example\AppData\Local\GitHub Copilot\GitHub Copilot.exe`, + }, + expectedAgent: AgentTypeUnknown, + }, + { + name: "Agent name in host path does not match", + processInfo: parentProcessInfo{ + Name: "pwsh.exe", + Executable: `C:\Users\example\copilot-workspace\pwsh.exe`, + }, + expectedAgent: AgentTypeUnknown, + }, + { + name: "Agent name substring does not match", + processInfo: parentProcessInfo{ + Name: "my-claude-wrapper", + }, + expectedAgent: AgentTypeUnknown, }, } @@ -317,16 +347,48 @@ func TestMatchProcessToAgent(t *testing.T) { t.Run(tt.name, func(t *testing.T) { result := matchProcessToAgent(tt.processInfo) - assert.Equal(t, tt.detected, result.Detected) + assert.Equal(t, tt.expectedAgent != AgentTypeUnknown, result.Detected) assert.Equal(t, tt.expectedAgent, result.Type) - if tt.detected { + if result.Detected { assert.Equal(t, DetectionSourceParentProcess, result.Source) } }) } } +func TestMatchProcessToAgent_ExactExecutableNames(t *testing.T) { + tests := []struct { + processName string + expectedAgent AgentType + }{ + {"claude", AgentTypeClaudeCode}, + {"claude-code", AgentTypeClaudeCode}, + {"copilot", AgentTypeGitHubCopilotCLI}, + {"copilot-cli", AgentTypeGitHubCopilotCLI}, + {"gh-copilot", AgentTypeGitHubCopilotCLI}, + {"github-copilot", AgentTypeGitHubCopilotCLI}, + {"github-copilot-cli", AgentTypeGitHubCopilotCLI}, + {"gemini", AgentTypeGemini}, + {"gemini-code", AgentTypeGemini}, + {"google-gemini", AgentTypeGemini}, + {"opencode", AgentTypeOpenCode}, + } + + for _, tt := range tests { + t.Run(tt.processName, func(t *testing.T) { + result := matchProcessToAgent(parentProcessInfo{ + Name: strings.ToUpper(tt.processName) + ".EXE", + Executable: `C:\tools\` + tt.processName + ".exe", + }) + + assert.True(t, result.Detected) + assert.Equal(t, tt.expectedAgent, result.Type) + assert.Equal(t, DetectionSourceParentProcess, result.Source) + }) + } +} + func TestGetCallingAgent_Caching(t *testing.T) { clearAgentEnvVars(t) ResetDetection() From e8177eb00ac64402b6abc5854184b78f6c517f4a Mon Sep 17 00:00:00 2001 From: Alberto Gimeno <50486+gimenete@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:20:42 +0200 Subject: [PATCH 2/4] fix: preserve non-Copilot process fallback matching Keep legacy substring and installation-path detection for Claude, Gemini, and OpenCode while restricting only Copilot to exact executable names. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../runcontext/agentdetect/detect_process.go | 27 ++++++++++++------ .../runcontext/agentdetect/detect_test.go | 28 +++++++++++++++++-- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/cli/azd/internal/runcontext/agentdetect/detect_process.go b/cli/azd/internal/runcontext/agentdetect/detect_process.go index 73fce0b7398..0f95a1f11a9 100644 --- a/cli/azd/internal/runcontext/agentdetect/detect_process.go +++ b/cli/azd/internal/runcontext/agentdetect/detect_process.go @@ -9,11 +9,11 @@ import ( "strings" ) -// processNamePatterns maps exact process executable names to agent types. -// Names are matched case-insensitively after removing a Windows executable extension. +// processNamePatterns maps process executable names to agent types. var processNamePatterns = []struct { - patterns []string // lowercase executable names to match - agentType AgentType + patterns []string // lowercase executable names to match + agentType AgentType + exactMatch bool }{ // Claude Code (Anthropic) - installed via npm, homebrew, or direct download { @@ -22,8 +22,10 @@ var processNamePatterns = []struct { }, // GitHub Copilot CLI - installed via npm (@github/copilot) or as gh extension { - patterns: []string{"copilot", "copilot-cli", "gh-copilot", "github-copilot", "github-copilot-cli"}, - agentType: AgentTypeGitHubCopilotCLI, + patterns: []string{"copilot", "copilot-cli", "gh-copilot", "github-copilot", "github-copilot-cli"}, + agentType: AgentTypeGitHubCopilotCLI, + // Avoid classifying host applications and installation paths containing the generic "copilot" term. + exactMatch: true, }, // Google Gemini CLI { @@ -90,7 +92,7 @@ func matchProcessToAgent(info parentProcessInfo) AgentInfo { for _, entry := range processNamePatterns { for _, pattern := range entry.patterns { - if nameLower == pattern { + if processNameMatches(nameLower, info.Executable, pattern, entry.exactMatch) { return AgentInfo{ Type: entry.agentType, Name: entry.agentType.DisplayName(), @@ -100,7 +102,7 @@ func matchProcessToAgent(info parentProcessInfo) AgentInfo { } } - if execBaseLower == pattern { + if processNameMatches(execBaseLower, info.Executable, pattern, entry.exactMatch) { return AgentInfo{ Type: entry.agentType, Name: entry.agentType.DisplayName(), @@ -115,6 +117,15 @@ func matchProcessToAgent(info parentProcessInfo) AgentInfo { return NoAgent() } +func processNameMatches(processName string, executable string, pattern string, exactMatch bool) bool { + if processName == pattern { + return true + } + + return !exactMatch && + (strings.Contains(processName, pattern) || strings.Contains(strings.ToLower(executable), pattern)) +} + func normalizeProcessName(processPath string) string { normalizedPath := strings.ReplaceAll(processPath, "\\", "/") if index := strings.LastIndex(normalizedPath, "/"); index >= 0 { diff --git a/cli/azd/internal/runcontext/agentdetect/detect_test.go b/cli/azd/internal/runcontext/agentdetect/detect_test.go index dd395acad3c..02a0188681f 100644 --- a/cli/azd/internal/runcontext/agentdetect/detect_test.go +++ b/cli/azd/internal/runcontext/agentdetect/detect_test.go @@ -327,7 +327,7 @@ func TestMatchProcessToAgent(t *testing.T) { expectedAgent: AgentTypeUnknown, }, { - name: "Agent name in host path does not match", + name: "Copilot name in host path does not match", processInfo: parentProcessInfo{ Name: "pwsh.exe", Executable: `C:\Users\example\copilot-workspace\pwsh.exe`, @@ -335,12 +335,34 @@ func TestMatchProcessToAgent(t *testing.T) { expectedAgent: AgentTypeUnknown, }, { - name: "Agent name substring does not match", + name: "Copilot name substring does not match", processInfo: parentProcessInfo{ - Name: "my-claude-wrapper", + Name: "my-copilot-wrapper", }, expectedAgent: AgentTypeUnknown, }, + { + name: "Claude wrapper remains supported", + processInfo: parentProcessInfo{ + Name: "my-claude-wrapper", + }, + expectedAgent: AgentTypeClaudeCode, + }, + { + name: "Gemini installation path remains supported", + processInfo: parentProcessInfo{ + Name: "node", + Executable: "/usr/local/lib/google-gemini/bin/node", + }, + expectedAgent: AgentTypeGemini, + }, + { + name: "OpenCode versioned executable remains supported", + processInfo: parentProcessInfo{ + Name: "opencode-v1.2.3", + }, + expectedAgent: AgentTypeOpenCode, + }, } for _, tt := range tests { From 80dc2c95461a3f811b240f5138be4033b86a7eed Mon Sep 17 00:00:00 2001 From: Alberto Gimeno <50486+gimenete@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:21:27 +0200 Subject: [PATCH 3/4] chore: retrigger cancelled checks The extension registry check was cancelled by its workflow concurrency guard and cannot be rerun by fork contributors. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> From ab603bab06f91f6b6067d48aa3c8be1521e25492 Mon Sep 17 00:00:00 2001 From: Alberto Gimeno <50486+gimenete@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:25:07 +0200 Subject: [PATCH 4/4] fix: retain parent process match evidence Keep full executable path matching in the executable branch so path-only agent detection reports the path that triggered it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../runcontext/agentdetect/detect_process.go | 18 +++++++++--------- .../runcontext/agentdetect/detect_test.go | 13 +++++++++---- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/cli/azd/internal/runcontext/agentdetect/detect_process.go b/cli/azd/internal/runcontext/agentdetect/detect_process.go index 0f95a1f11a9..9ea5e750d85 100644 --- a/cli/azd/internal/runcontext/agentdetect/detect_process.go +++ b/cli/azd/internal/runcontext/agentdetect/detect_process.go @@ -22,8 +22,8 @@ var processNamePatterns = []struct { }, // GitHub Copilot CLI - installed via npm (@github/copilot) or as gh extension { - patterns: []string{"copilot", "copilot-cli", "gh-copilot", "github-copilot", "github-copilot-cli"}, - agentType: AgentTypeGitHubCopilotCLI, + patterns: []string{"copilot", "copilot-cli", "gh-copilot", "github-copilot", "github-copilot-cli"}, + agentType: AgentTypeGitHubCopilotCLI, // Avoid classifying host applications and installation paths containing the generic "copilot" term. exactMatch: true, }, @@ -92,7 +92,7 @@ func matchProcessToAgent(info parentProcessInfo) AgentInfo { for _, entry := range processNamePatterns { for _, pattern := range entry.patterns { - if processNameMatches(nameLower, info.Executable, pattern, entry.exactMatch) { + if processNameMatches(nameLower, pattern, entry.exactMatch) { return AgentInfo{ Type: entry.agentType, Name: entry.agentType.DisplayName(), @@ -102,7 +102,8 @@ func matchProcessToAgent(info parentProcessInfo) AgentInfo { } } - if processNameMatches(execBaseLower, info.Executable, pattern, entry.exactMatch) { + if processNameMatches(execBaseLower, pattern, entry.exactMatch) || + (!entry.exactMatch && strings.Contains(strings.ToLower(info.Executable), pattern)) { return AgentInfo{ Type: entry.agentType, Name: entry.agentType.DisplayName(), @@ -117,13 +118,12 @@ func matchProcessToAgent(info parentProcessInfo) AgentInfo { return NoAgent() } -func processNameMatches(processName string, executable string, pattern string, exactMatch bool) bool { - if processName == pattern { - return true +func processNameMatches(processName string, pattern string, exactMatch bool) bool { + if exactMatch { + return processName == pattern } - return !exactMatch && - (strings.Contains(processName, pattern) || strings.Contains(strings.ToLower(executable), pattern)) + return strings.Contains(processName, pattern) } func normalizeProcessName(processPath string) string { diff --git a/cli/azd/internal/runcontext/agentdetect/detect_test.go b/cli/azd/internal/runcontext/agentdetect/detect_test.go index 02a0188681f..0e411d016ac 100644 --- a/cli/azd/internal/runcontext/agentdetect/detect_test.go +++ b/cli/azd/internal/runcontext/agentdetect/detect_test.go @@ -252,9 +252,10 @@ func TestDetectFromUserAgent(t *testing.T) { func TestMatchProcessToAgent(t *testing.T) { tests := []struct { - name string - processInfo parentProcessInfo - expectedAgent AgentType + name string + processInfo parentProcessInfo + expectedAgent AgentType + expectedDetails string }{ { name: "Empty process info", @@ -354,7 +355,8 @@ func TestMatchProcessToAgent(t *testing.T) { Name: "node", Executable: "/usr/local/lib/google-gemini/bin/node", }, - expectedAgent: AgentTypeGemini, + expectedAgent: AgentTypeGemini, + expectedDetails: "/usr/local/lib/google-gemini/bin/node", }, { name: "OpenCode versioned executable remains supported", @@ -374,6 +376,9 @@ func TestMatchProcessToAgent(t *testing.T) { if result.Detected { assert.Equal(t, DetectionSourceParentProcess, result.Source) + if tt.expectedDetails != "" { + assert.Equal(t, tt.expectedDetails, result.Details) + } } }) }