fix(backend): isolate PYTHONPATH to prevent pollution of external projects (ACS-251) (#1065)

* fix(backend): isolate PYTHONPATH to prevent pollution of external projects (ACS-251)

When Auto-Claude's backend runs (using Python 3.12), it inherits a PYTHONPATH
environment variable that can cause cryptic failures when agents work on
external projects requiring different Python versions.

The Claude Agent SDK merges os.environ with the env dict we provide when
spawning subprocesses. This means any PYTHONPATH set in the parent process
is inherited by agent subprocesses, causing import failures and version
mismatches in external projects.

Solution:
- Explicitly set PYTHONPATH to empty string in get_sdk_env_vars()
- This overrides any inherited PYTHONPATH from the parent process
- Agent subprocesses now have clean Python environments

This fixes the root cause of ACS-251, removing the need for workarounds
in agent prompt files.

Refs: ACS-251

* test: address PR review feedback on test_auth.py

- Remove unused 'os' import
- Remove redundant sys.path setup (already in conftest.py)
- Fix misleading test name: returns_empty_dict -> pythonpath_is_always_set_in_result
- Move platform import inside test functions to avoid mid-file imports
- Make Windows tests platform-independent using platform.system() mocks
- Add new test for non-Windows platforms (test_on_non_windows_git_bash_not_added)

All 9 tests now pass on all platforms.

Addresses review comments on PR #1065

* test: remove unused pytest import and unnecessary mock

- Remove unused 'pytest' import (not used in test file)
- Remove unnecessary _find_git_bash_path mock in test_on_non_windows_git_bash_not_added
  (when platform.system() returns "Linux", _find_git_bash_path is never called)

All 9 tests still pass.

Addresses additional review comments on PR #1065

* test: address Auto Claude PR Review feedback on test_auth.py

- Add shebang line (#!/usr/bin/env python3)
- Move imports to module level (platform, get_sdk_env_vars)
- Remove duplicate test (test_empty_pythonpath_overrides_parent_value)
- Remove unnecessary conftest.py comment
- Apply ruff formatting

Addresses LOW priority findings from Auto Claude PR Review.

---------

Co-authored-by: StillKnotKnown <stillknotknown@users.noreply.github.com>
This commit is contained in:
StillKnotKnown
2026-01-15 19:52:14 +02:00
committed by GitHub
parent 5fb7574b7b
commit 18d9b6cfc1
2 changed files with 139 additions and 0 deletions
+10
View File
@@ -321,6 +321,16 @@ def get_sdk_env_vars() -> dict[str, str]:
if bash_path:
env["CLAUDE_CODE_GIT_BASH_PATH"] = bash_path
# Explicitly unset PYTHONPATH in SDK subprocess environment to prevent
# pollution of agent subprocess environments. This fixes ACS-251 where
# external projects with different Python versions would fail due to
# inheriting Auto-Claude's PYTHONPATH (which points to Python 3.12 packages).
#
# The SDK merges os.environ with the env dict we provide, so setting
# PYTHONPATH to an empty string here overrides any inherited value.
# The empty string ensures Python doesn't add any extra paths to sys.path.
env["PYTHONPATH"] = ""
return env
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
"""
Tests for core/auth module.
Tests authentication token management and SDK environment variable handling,
including the PYTHONPATH isolation fix for ACS-251.
"""
import platform
from unittest.mock import patch
from core.auth import get_sdk_env_vars
class TestGetSdkEnvVars:
"""Tests for get_sdk_env_vars() function."""
def test_pythonpath_is_always_set_in_result(self, monkeypatch):
"""
PYTHONPATH should always be present in result, even when empty.
When no SDK env vars are set, PYTHONPATH is still explicitly set to
empty string to override any inherited value from the parent process.
"""
# Clear all SDK env vars
for var in [
"ANTHROPIC_BASE_URL",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_MODEL",
"NO_PROXY",
"DISABLE_TELEMETRY",
]:
monkeypatch.delenv(var, raising=False)
result = get_sdk_env_vars()
# PYTHONPATH should always be present, even if empty
assert "PYTHONPATH" in result
assert result["PYTHONPATH"] == ""
def test_includes_anthropic_base_url_when_set(self, monkeypatch):
"""Should include ANTHROPIC_BASE_URL when set."""
monkeypatch.setenv("ANTHROPIC_BASE_URL", "https://api.example.com")
result = get_sdk_env_vars()
assert result.get("ANTHROPIC_BASE_URL") == "https://api.example.com"
def test_includes_anthropic_auth_token_when_set(self, monkeypatch):
"""Should include ANTHROPIC_AUTH_TOKEN when set."""
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", "sk-test-token")
result = get_sdk_env_vars()
assert result.get("ANTHROPIC_AUTH_TOKEN") == "sk-test-token"
def test_pythonpath_isolation_from_parent_process(self, monkeypatch):
"""
Test ACS-251 fix: PYTHONPATH from parent process should be overridden.
This ensures that Auto-Claude's PYTHONPATH (which may point to Python 3.12
packages) doesn't pollute agent subprocess environments, preventing
failures when working on external projects with different Python versions.
"""
# Simulate parent process having a PYTHONPATH set
monkeypatch.setenv(
"PYTHONPATH",
"/path/to/auto-claude/backend:/path/to/python3.12/site-packages",
)
result = get_sdk_env_vars()
# PYTHONPATH should be explicitly overridden to empty string
# This prevents the SDK from inheriting the parent's PYTHONPATH
assert "PYTHONPATH" in result
assert result["PYTHONPATH"] == ""
def test_skips_empty_env_vars(self, monkeypatch):
"""Should not include env vars with empty values."""
monkeypatch.setenv("ANTHROPIC_BASE_URL", "")
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", "")
result = get_sdk_env_vars()
# Empty vars should not be in result (except PYTHONPATH which is explicitly set)
assert "ANTHROPIC_BASE_URL" not in result
assert "ANTHROPIC_AUTH_TOKEN" not in result
# PYTHONPATH should still be present as explicit override
assert result["PYTHONPATH"] == ""
def test_on_windows_auto_detects_git_bash_path(self, monkeypatch):
"""On Windows, should auto-detect git-bash path if not set."""
# Mock platform.system to simulate Windows for cross-platform testing
with patch.object(platform, "system", return_value="Windows"):
# Mock _find_git_bash_path to return a path
with patch(
"core.auth._find_git_bash_path",
return_value="C:/Program Files/Git/bin/bash.exe",
):
monkeypatch.delenv("CLAUDE_CODE_GIT_BASH_PATH", raising=False)
result = get_sdk_env_vars()
assert (
result.get("CLAUDE_CODE_GIT_BASH_PATH")
== "C:/Program Files/Git/bin/bash.exe"
)
def test_preserves_existing_git_bash_path_on_windows(self, monkeypatch):
"""On Windows, should preserve existing CLAUDE_CODE_GIT_BASH_PATH."""
# Mock platform.system to simulate Windows for cross-platform testing
with patch.object(platform, "system", return_value="Windows"):
monkeypatch.setenv("CLAUDE_CODE_GIT_BASH_PATH", "C:/Custom/bash.exe")
result = get_sdk_env_vars()
assert result.get("CLAUDE_CODE_GIT_BASH_PATH") == "C:/Custom/bash.exe"
def test_on_non_windows_git_bash_not_added(self, monkeypatch):
"""On non-Windows platforms, CLAUDE_CODE_GIT_BASH_PATH should not be auto-added."""
# Mock platform.system to simulate Linux for cross-platform testing
# When platform is not Windows, _find_git_bash_path is never called
with patch.object(platform, "system", return_value="Linux"):
monkeypatch.delenv("CLAUDE_CODE_GIT_BASH_PATH", raising=False)
result = get_sdk_env_vars()
# Should not have CLAUDE_CODE_GIT_BASH_PATH
assert "CLAUDE_CODE_GIT_BASH_PATH" not in result