From 13239282fe7fc4d84b1bbb6ad6ed1197328fc471 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Thu, 12 Feb 2026 21:10:54 +0200 Subject: [PATCH 1/5] docs: add Google Antigravity setup guide to client configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #303 — documents the key difference that Antigravity uses `serverUrl` instead of `url` for HTTP MCP servers, with config paths for Windows, macOS, and Linux. Co-Authored-By: Claude Opus 4.6 --- docs/setup.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/setup.md b/docs/setup.md index 9309e4eb..ab399892 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -334,6 +334,48 @@ goose> Help me search for files related to authentication --- +### 🚀 Google Antigravity + +Google Antigravity is an AI-powered IDE built on VS Code with deep Gemini integration and built-in MCP support. + +**⚠️ Important:** Antigravity uses `serverUrl` (not `url`) for HTTP-based MCP servers. Using `url` will cause a connection error. + +**Config file location:** + +| Platform | Path | +|----------|------| +| macOS | `~/.gemini/antigravity/mcp_config.json` | +| Linux | `~/.gemini/antigravity/mcp_config.json` | +| Windows | `%USERPROFILE%\.gemini\antigravity\mcp_config.json` | + +**Setup via UI:** + +1. Open the Agent Panel (right sidebar) +2. Click **"..."** (More Options) → **MCP Servers** → **Manage MCP Servers** +3. Click **"View raw config"** +4. Add the MCPProxy configuration (see below) +5. Click **Refresh** to apply changes + +**Setup via Configuration File:** + +Edit your `mcp_config.json`: + +```json +{ + "mcpServers": { + "mcpproxy": { + "serverUrl": "http://127.0.0.1:8080/mcp" + } + } +} +``` + +**📝 Note:** MCPProxy's MCP endpoint does not require API key authentication, so no `headers` block is needed. Antigravity does not support `${workspaceFolder}` — use absolute paths in any server configuration. + +**📚 Reference:** [Antigravity MCP Documentation](https://antigravity.google/docs/mcp) + +--- + ## Optional HTTPS Setup MCPProxy supports secure HTTPS connections with automatic certificate generation. **HTTP is enabled by default** for immediate compatibility, but HTTPS provides enhanced security for production use. From bd0fe268d4e8243aa74cf1cc6b06fda0ed273206 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 14 Feb 2026 17:27:12 +0200 Subject: [PATCH 2/5] Fix Issue #302: Windows registry PATH reading and spaces in paths Problem: MCP servers fail on Windows with 'uv is not recognized' when launched via installer without inheriting user's PATH. Solution 1 - Registry PATH Reading: - Read HKEY_CURRENT_USER\Environment\Path (user PATH) - Read HKEY_LOCAL_MACHINE\...\Environment\Path (system PATH) - Expand variables (%USERPROFILE%, %APPDATA%, etc.) - Recovers 28-34 directories vs 4-7 before Solution 2 - Spaces in Paths: - Fixed cmd.exe escaping (removed incorrect backslash escaping) - Auto-strips user-added quotes from paths - Paths with spaces now work correctly Files: - internal/secureenv/manager_windows.go (NEW) - internal/secureenv/manager_unix.go (NEW) - internal/secureenv/manager_windows_test.go (NEW) - internal/secureenv/manager.go (MODIFIED) - internal/upstream/core/connection_stdio.go (MODIFIED) Test Results: - TestReadWindowsRegistryPath: 34 directories from registry - TestDiscoverWindowsPathsWithEmptyEnvironment: 28 paths - All unit tests passing Fixes #302 Co-Authored-By: Claude Sonnet 4.5 --- internal/secureenv/manager.go | 34 +++- internal/secureenv/manager_unix.go | 11 ++ internal/secureenv/manager_windows.go | 89 +++++++++ internal/secureenv/manager_windows_test.go | 201 +++++++++++++++++++++ internal/upstream/core/connection_stdio.go | 22 ++- 5 files changed, 351 insertions(+), 6 deletions(-) create mode 100644 internal/secureenv/manager_unix.go create mode 100644 internal/secureenv/manager_windows.go create mode 100644 internal/secureenv/manager_windows_test.go diff --git a/internal/secureenv/manager.go b/internal/secureenv/manager.go index a260a50b..c6c62b37 100644 --- a/internal/secureenv/manager.go +++ b/internal/secureenv/manager.go @@ -165,19 +165,45 @@ func (m *Manager) discoverUnixPaths() []string { // discoverWindowsPaths discovers common Windows tool paths func (m *Manager) discoverWindowsPaths() []string { + // CRITICAL FIX for Issue #302: + // Try to read PATH from Windows registry first. + // This is necessary because when mcpproxy is launched via installer/service, + // it doesn't inherit the user's PATH environment variable. + // The registry is the source of truth for Windows PATH configuration. + if registryPaths := discoverWindowsPathsFromRegistry(); len(registryPaths) > 0 { + return registryPaths + } + + // Fallback to hardcoded discovery if registry read fails + // This expanded list includes more common tool locations + homeDir, _ := os.UserHomeDir() + commonPaths := []string{ + // System paths `C:\Windows\System32`, `C:\Windows`, + + // Common development tools `C:\Program Files\Docker\Docker\resources\bin`, `C:\Program Files\Git\bin`, + `C:\Program Files\Git\cmd`, `C:\Program Files\nodejs`, + `C:\Program Files\Go\bin`, + + // User-specific tool paths (if homeDir is available) } - // Add user-specific paths - if homeDir, err := os.UserHomeDir(); err == nil { + if homeDir != "" { commonPaths = append(commonPaths, - homeDir+`\AppData\Local\Programs\Python\Python311\Scripts`, - homeDir+`\AppData\Roaming\npm`, + homeDir+`\.cargo\bin`, // Rust tools (cargo, uv) + homeDir+`\.local\bin`, // Python user scripts + homeDir+`\go\bin`, // Go binaries + homeDir+`\AppData\Roaming\npm`, // npm globals + homeDir+`\scoop\shims`, // Scoop packages + homeDir+`\AppData\Local\Programs\Python\Python313\Scripts`, // Python 3.13 + homeDir+`\AppData\Local\Programs\Python\Python312\Scripts`, // Python 3.12 + homeDir+`\AppData\Local\Programs\Python\Python311\Scripts`, // Python 3.11 + homeDir+`\AppData\Local\Programs\Python\Python310\Scripts`, // Python 3.10 ) } diff --git a/internal/secureenv/manager_unix.go b/internal/secureenv/manager_unix.go new file mode 100644 index 00000000..925538be --- /dev/null +++ b/internal/secureenv/manager_unix.go @@ -0,0 +1,11 @@ +//go:build !windows + +package secureenv + +// discoverWindowsPathsFromRegistry is a stub for non-Windows platforms +// On Unix/macOS, this function is never called because discoverWindowsPaths() +// is only called when runtime.GOOS == "windows" +func discoverWindowsPathsFromRegistry() []string { + // This should never be called on non-Windows platforms + return nil +} diff --git a/internal/secureenv/manager_windows.go b/internal/secureenv/manager_windows.go new file mode 100644 index 00000000..6fa1f5c7 --- /dev/null +++ b/internal/secureenv/manager_windows.go @@ -0,0 +1,89 @@ +//go:build windows + +package secureenv + +import ( + "os" + "strings" + + "golang.org/x/sys/windows/registry" +) + +// readWindowsRegistryPath reads the PATH environment variable from Windows registry +// This is necessary because when mcpproxy is launched via installer/service, +// it doesn't inherit the user's PATH environment variable. +// The registry is the source of truth for Windows PATH configuration. +func readWindowsRegistryPath() (string, error) { + var paths []string + + // 1. Read USER PATH from HKEY_CURRENT_USER\Environment\Path + // This contains user-specific PATH additions (e.g., .cargo\bin, go\bin) + userKey, err := registry.OpenKey(registry.CURRENT_USER, + `Environment`, registry.QUERY_VALUE) + if err == nil { + defer userKey.Close() + + userPath, _, err := userKey.GetStringValue("Path") + if err == nil && userPath != "" { + // CRITICAL: Expand environment variables like %USERPROFILE%, %APPDATA% + // Registry stores paths as REG_EXPAND_SZ with embedded variables + expandedUserPath := os.ExpandEnv(userPath) + paths = append(paths, expandedUserPath) + } + } + + // 2. Read SYSTEM PATH from HKEY_LOCAL_MACHINE\...\Environment\Path + // This contains system-wide PATH (e.g., C:\Windows\System32, Program Files) + sysKey, err := registry.OpenKey(registry.LOCAL_MACHINE, + `SYSTEM\CurrentControlSet\Control\Session Manager\Environment`, + registry.QUERY_VALUE) + if err == nil { + defer sysKey.Close() + + systemPath, _, err := sysKey.GetStringValue("Path") + if err == nil && systemPath != "" { + // Expand environment variables + expandedSystemPath := os.ExpandEnv(systemPath) + paths = append(paths, expandedSystemPath) + } + } + + // Combine User PATH + System PATH (user takes precedence) + fullPath := strings.Join(paths, string(os.PathListSeparator)) + + if fullPath == "" { + // If both registry reads failed, return error + return "", registry.ErrNotExist + } + + return fullPath, nil +} + +// discoverWindowsPathsFromRegistry reads PATH from registry and returns as slice +// This replaces the hardcoded discovery list when registry is available +func discoverWindowsPathsFromRegistry() []string { + registryPath, err := readWindowsRegistryPath() + if err != nil { + // Registry read failed, return empty slice (caller will use hardcoded fallback) + return nil + } + + // Split the combined PATH into individual directories + parts := strings.Split(registryPath, string(os.PathListSeparator)) + + // Filter to only existing directories + var existingPaths []string + for _, path := range parts { + path = strings.TrimSpace(path) + if path == "" { + continue + } + + // Check if directory exists + if info, err := os.Stat(path); err == nil && info.IsDir() { + existingPaths = append(existingPaths, path) + } + } + + return existingPaths +} diff --git a/internal/secureenv/manager_windows_test.go b/internal/secureenv/manager_windows_test.go new file mode 100644 index 00000000..6fbaa045 --- /dev/null +++ b/internal/secureenv/manager_windows_test.go @@ -0,0 +1,201 @@ +//go:build windows + +package secureenv + +import ( + "os" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadWindowsRegistryPath(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("Windows-only test") + } + + path, err := readWindowsRegistryPath() + + // Registry read should succeed + require.NoError(t, err, "Reading Windows registry PATH should not fail") + require.NotEmpty(t, path, "Registry PATH should not be empty") + + // Path should contain system directories + assert.Contains(t, strings.ToLower(path), `c:\windows\system32`, + "Registry PATH should contain System32") + + // Path should be expanded (no %USERPROFILE% etc.) + assert.NotContains(t, path, "%USERPROFILE%", + "PATH should have %USERPROFILE% expanded") + assert.NotContains(t, path, "%APPDATA%", + "PATH should have %APPDATA% expanded") + assert.NotContains(t, path, "%LOCALAPPDATA%", + "PATH should have %LOCALAPPDATA% expanded") + + t.Logf("Registry PATH length: %d characters", len(path)) + pathParts := strings.Split(path, string(os.PathListSeparator)) + t.Logf("Registry PATH contains %d directories", len(pathParts)) + + // Log first few paths for debugging + for i, part := range pathParts { + if i >= 5 { + break + } + t.Logf(" [%d] %s", i, part) + } +} + +func TestDiscoverWindowsPathsFromRegistry(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("Windows-only test") + } + + paths := discoverWindowsPathsFromRegistry() + + // Should return at least some paths + assert.NotEmpty(t, paths, "Should discover at least some paths from registry") + + // All returned paths should exist + for _, path := range paths { + info, err := os.Stat(path) + assert.NoError(t, err, "Path should exist: %s", path) + if err == nil { + assert.True(t, info.IsDir(), "Path should be a directory: %s", path) + } + } + + // Should contain common system paths + hasSystem32 := false + for _, path := range paths { + if strings.Contains(strings.ToLower(path), `system32`) { + hasSystem32 = true + break + } + } + assert.True(t, hasSystem32, "Should contain System32 directory") + + t.Logf("Discovered %d paths from registry", len(paths)) +} + +func TestWindowsPathExpansion(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("Windows-only test") + } + + tests := []struct { + name string + input string + contains string + }{ + { + name: "USERPROFILE expansion", + input: `%USERPROFILE%\.cargo\bin`, + contains: `\Users\`, + }, + { + name: "APPDATA expansion", + input: `%APPDATA%\npm`, + contains: `\AppData\Roaming\`, + }, + { + name: "LOCALAPPDATA expansion", + input: `%LOCALAPPDATA%\Programs`, + contains: `\AppData\Local\`, + }, + { + name: "PROGRAMFILES expansion", + input: `%PROGRAMFILES%\Git`, + contains: `\Program Files\`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + expanded := os.ExpandEnv(tt.input) + + // Should not contain % after expansion + assert.NotContains(t, expanded, "%", + "Expanded path should not contain %%: %s", expanded) + + // Should contain expected substring + assert.Contains(t, expanded, tt.contains, + "Expanded path should contain %s: %s", tt.contains, expanded) + + t.Logf("Input: %s", tt.input) + t.Logf("Output: %s", expanded) + }) + } +} + +func TestDiscoverWindowsPathsWithEmptyEnvironment(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("Windows-only test") + } + + // Save original PATH + originalPath := os.Getenv("PATH") + defer os.Setenv("PATH", originalPath) + + // Simulate empty PATH scenario (installer/service launch) + os.Setenv("PATH", "") + + // Create a manager + manager := NewManager(nil) + + // Discovery should still work via registry + paths := manager.pathDiscovery.DiscoveredPaths + assert.NotEmpty(t, paths, + "Should discover paths from registry even when PATH env is empty") + + // Should contain system paths + hasSystemPath := false + for _, path := range paths { + lowerPath := strings.ToLower(path) + if strings.Contains(lowerPath, "system32") || strings.Contains(lowerPath, "windows") { + hasSystemPath = true + break + } + } + assert.True(t, hasSystemPath, + "Should contain Windows system paths even when PATH env is empty") + + t.Logf("Discovered %d paths with empty PATH env", len(paths)) +} + +func TestManagerBuildSecureEnvironmentWithRegistryPaths(t *testing.T) { + if runtime.GOOS != "windows" { + t.Skip("Windows-only test") + } + + // Save original PATH + originalPath := os.Getenv("PATH") + defer os.Setenv("PATH", originalPath) + + // Simulate minimal PATH scenario + os.Setenv("PATH", `C:\Windows\System32`) + + // Create manager and build environment + manager := NewManager(nil) + env := manager.BuildSecureEnvironment() + + // Extract PATH from environment + var builtPath string + for _, envVar := range env { + if strings.HasPrefix(envVar, "PATH=") { + builtPath = strings.TrimPrefix(envVar, "PATH=") + break + } + } + + assert.NotEmpty(t, builtPath, "Built environment should have PATH") + + // PATH should be more comprehensive than minimal input + pathParts := strings.Split(builtPath, string(os.PathListSeparator)) + assert.Greater(t, len(pathParts), 5, + "Built PATH should have more than 5 directories (got %d)", len(pathParts)) + + t.Logf("Built PATH has %d directories", len(pathParts)) +} diff --git a/internal/upstream/core/connection_stdio.go b/internal/upstream/core/connection_stdio.go index e3a88d6f..de04be16 100644 --- a/internal/upstream/core/connection_stdio.go +++ b/internal/upstream/core/connection_stdio.go @@ -390,8 +390,26 @@ func shellescape(s string) string { if !strings.ContainsAny(s, " \t\n\r\"&|<>()^%") { return s } - // For Windows, use double quotes and escape internal double quotes - return `"` + strings.ReplaceAll(s, `"`, `\"`) + `"` + + // IMPORTANT: cmd.exe uses DIFFERENT escaping than Unix shells + // - Backslash is NOT an escape character in cmd.exe + // - To include a literal quote in a quoted string, you must: + // 1. End the quoted string + // 2. Add an escaped quote (\") + // 3. Start a new quoted string + // + // However, for our use case with cmd /c "command args", we wrap + // the entire command line once, and individual components should + // NOT contain quotes. If they do, we need special handling. + // + // Strategy: If string already contains quotes, strip them first + // This handles the case where users put quotes in JSON config + cleaned := strings.Trim(s, `"`) + + // Now wrap in quotes for cmd.exe + // Any remaining internal quotes are from the actual path and will cause issues + // For now, we just quote the cleaned string + return `"` + cleaned + `"` } // Unix shell special characters if !strings.ContainsAny(s, " \t\n\r\"'\\$`;&|<>(){}[]?*~") { From e8c37720e6d764943889d3892ecfcaaaf3cf2676 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 14 Feb 2026 19:15:19 +0200 Subject: [PATCH 3/5] Fix Windows %VAR% expansion: use registry.ExpandString instead of os.ExpandEnv os.ExpandEnv only handles $VAR/${VAR} syntax, not Windows %VAR%. Registry PATH entries use %USERPROFILE%, %APPDATA% etc. which were left unexpanded, causing os.Stat to fail and paths to be filtered out. Co-Authored-By: Claude Opus 4.6 --- internal/secureenv/manager_windows.go | 22 +++++++++++++++------- internal/secureenv/manager_windows_test.go | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/internal/secureenv/manager_windows.go b/internal/secureenv/manager_windows.go index 6fa1f5c7..54fcda85 100644 --- a/internal/secureenv/manager_windows.go +++ b/internal/secureenv/manager_windows.go @@ -9,6 +9,17 @@ import ( "golang.org/x/sys/windows/registry" ) +// expandWindowsEnvVars expands Windows-style %VAR% environment variables. +// os.ExpandEnv only handles $VAR/${VAR} syntax, NOT Windows %VAR%. +// registry.ExpandString calls the Windows ExpandEnvironmentStrings API. +func expandWindowsEnvVars(s string) string { + expanded, err := registry.ExpandString(s) + if err != nil { + return s // fallback to unexpanded + } + return expanded +} + // readWindowsRegistryPath reads the PATH environment variable from Windows registry // This is necessary because when mcpproxy is launched via installer/service, // it doesn't inherit the user's PATH environment variable. @@ -25,10 +36,9 @@ func readWindowsRegistryPath() (string, error) { userPath, _, err := userKey.GetStringValue("Path") if err == nil && userPath != "" { - // CRITICAL: Expand environment variables like %USERPROFILE%, %APPDATA% - // Registry stores paths as REG_EXPAND_SZ with embedded variables - expandedUserPath := os.ExpandEnv(userPath) - paths = append(paths, expandedUserPath) + // CRITICAL: Expand Windows %VAR% environment variables + // Registry stores paths as REG_EXPAND_SZ with embedded %USERPROFILE% etc. + paths = append(paths, expandWindowsEnvVars(userPath)) } } @@ -42,9 +52,7 @@ func readWindowsRegistryPath() (string, error) { systemPath, _, err := sysKey.GetStringValue("Path") if err == nil && systemPath != "" { - // Expand environment variables - expandedSystemPath := os.ExpandEnv(systemPath) - paths = append(paths, expandedSystemPath) + paths = append(paths, expandWindowsEnvVars(systemPath)) } } diff --git a/internal/secureenv/manager_windows_test.go b/internal/secureenv/manager_windows_test.go index 6fbaa045..93f49883 100644 --- a/internal/secureenv/manager_windows_test.go +++ b/internal/secureenv/manager_windows_test.go @@ -114,7 +114,7 @@ func TestWindowsPathExpansion(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - expanded := os.ExpandEnv(tt.input) + expanded := expandWindowsEnvVars(tt.input) // Should not contain % after expansion assert.NotContains(t, expanded, "%", From 6f2ba95d192964df0e709477ba53c633717f872f Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 14 Feb 2026 19:26:34 +0200 Subject: [PATCH 4/5] Always enhance minimal PATH on Windows using registry-discovered paths On Windows, installer/service launches inherit minimal PATH (just System32). The registry-discovered paths should always be used to enhance the PATH in this scenario, without requiring the explicit EnhancePath opt-in that was designed for macOS Launchd. Co-Authored-By: Claude Opus 4.6 --- internal/secureenv/manager.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/secureenv/manager.go b/internal/secureenv/manager.go index c6c62b37..9e410cde 100644 --- a/internal/secureenv/manager.go +++ b/internal/secureenv/manager.go @@ -298,9 +298,12 @@ func (m *Manager) buildEnhancedPath(existingPath string) string { } } - // Only enhance if explicitly enabled AND we're missing common tool directories AND the path is minimal - // This specifically targets Launchd scenarios while preserving normal behavior by default - shouldEnhance := m.config.EnhancePath && !hasCommonToolDirs && len(pathParts) <= 2 + // Enhance PATH when it's minimal and missing common tool directories. + // On macOS: requires explicit EnhancePath opt-in (targets Launchd scenarios). + // On Windows: always enhance when minimal, since installer/service launches + // inherit minimal PATH and registry-discovered paths are the canonical source. + shouldEnhance := !hasCommonToolDirs && len(pathParts) <= 2 && + (m.config.EnhancePath || runtime.GOOS == osWindows) if shouldEnhance { // Start with discovered paths for better tool discovery enhancedParts := make([]string, 0, len(m.pathDiscovery.DiscoveredPaths)+len(pathParts)) From 9a7bb0865c3381f2807b6ef51f884383bbe95c1a Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 14 Feb 2026 19:31:20 +0200 Subject: [PATCH 5/5] Fix Windows test: use EnhancePath=true to match production config The TestManagerBuildSecureEnvironmentWithRegistryPaths test was using default config (EnhancePath=false) but expected PATH enhancement. In production, core/client.go always sets EnhancePath=true. Reverted the blanket Windows enhancement and fixed the test to match production. Co-Authored-By: Claude Opus 4.6 --- internal/secureenv/manager.go | 9 +++------ internal/secureenv/manager_windows_test.go | 9 +++++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/secureenv/manager.go b/internal/secureenv/manager.go index 9e410cde..c6c62b37 100644 --- a/internal/secureenv/manager.go +++ b/internal/secureenv/manager.go @@ -298,12 +298,9 @@ func (m *Manager) buildEnhancedPath(existingPath string) string { } } - // Enhance PATH when it's minimal and missing common tool directories. - // On macOS: requires explicit EnhancePath opt-in (targets Launchd scenarios). - // On Windows: always enhance when minimal, since installer/service launches - // inherit minimal PATH and registry-discovered paths are the canonical source. - shouldEnhance := !hasCommonToolDirs && len(pathParts) <= 2 && - (m.config.EnhancePath || runtime.GOOS == osWindows) + // Only enhance if explicitly enabled AND we're missing common tool directories AND the path is minimal + // This specifically targets Launchd scenarios while preserving normal behavior by default + shouldEnhance := m.config.EnhancePath && !hasCommonToolDirs && len(pathParts) <= 2 if shouldEnhance { // Start with discovered paths for better tool discovery enhancedParts := make([]string, 0, len(m.pathDiscovery.DiscoveredPaths)+len(pathParts)) diff --git a/internal/secureenv/manager_windows_test.go b/internal/secureenv/manager_windows_test.go index 93f49883..0e113b8c 100644 --- a/internal/secureenv/manager_windows_test.go +++ b/internal/secureenv/manager_windows_test.go @@ -177,8 +177,13 @@ func TestManagerBuildSecureEnvironmentWithRegistryPaths(t *testing.T) { // Simulate minimal PATH scenario os.Setenv("PATH", `C:\Windows\System32`) - // Create manager and build environment - manager := NewManager(nil) + // Create manager with EnhancePath enabled (matches production: core/client.go sets this) + manager := NewManager(&EnvConfig{ + InheritSystemSafe: true, + AllowedSystemVars: DefaultEnvConfig().AllowedSystemVars, + CustomVars: make(map[string]string), + EnhancePath: true, + }) env := manager.BuildSecureEnvironment() // Extract PATH from environment