Skip to content

Commit b094d16

Browse files
authored
Merge pull request #266 from alex-feel/alex-feel-dev
Prevent auto-update when installing specific Claude Code versions
2 parents ccb32f6 + beab951 commit b094d16

File tree

2 files changed

+62
-155
lines changed

2 files changed

+62
-155
lines changed

scripts/install_claude.py

Lines changed: 47 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1753,52 +1753,6 @@ def _download_claude_direct_from_gcs(version: str, target_path: Path) -> bool:
17531753
temp_path.unlink()
17541754

17551755

1756-
def _run_claude_install_setup() -> bool:
1757-
"""Run claude install to configure PATH and shell integration.
1758-
1759-
Executes the install subcommand WITHOUT version argument to avoid
1760-
triggering the version-check bug. This only performs PATH setup.
1761-
1762-
Returns:
1763-
True if setup succeeded, False otherwise.
1764-
1765-
Note:
1766-
Windows uses ~/.local/bin/claude.exe, Unix uses ~/.local/bin/claude.
1767-
"""
1768-
if sys.platform == 'win32':
1769-
native_path = Path.home() / '.local' / 'bin' / 'claude.exe'
1770-
else:
1771-
native_path = Path.home() / '.local' / 'bin' / 'claude'
1772-
1773-
if not native_path.exists():
1774-
error(f'Cannot run install setup - {native_path.name} not found')
1775-
return False
1776-
1777-
info(f'Running {native_path.name} install for PATH setup...')
1778-
1779-
# Run install subcommand without version to avoid triggering the bug
1780-
# The install subcommand configures PATH and shell integration
1781-
result = run_command([str(native_path), 'install'], capture_output=True)
1782-
1783-
if result.returncode == 0:
1784-
success('PATH setup completed successfully')
1785-
return True
1786-
1787-
# Non-zero return may still be okay if PATH is already configured
1788-
warning(f'Install subcommand returned code {result.returncode}')
1789-
if result.stderr:
1790-
info(f'Output: {result.stderr}')
1791-
1792-
# Verify the binary is executable
1793-
version_result = run_command([str(native_path), '--version'], capture_output=True)
1794-
if version_result.returncode == 0:
1795-
info(f'Claude binary is functional: {version_result.stdout.strip()}')
1796-
return True
1797-
1798-
error('Claude binary may not be functional')
1799-
return False
1800-
1801-
18021756
def _ensure_local_bin_in_path_unix() -> bool:
18031757
"""Ensure ~/.local/bin is in PATH on Unix-like systems.
18041758
@@ -1894,10 +1848,11 @@ def install_claude_native_windows(version: str | None = None) -> bool:
18941848

18951849
# Attempt direct download from GCS
18961850
if _download_claude_direct_from_gcs(version, native_target):
1897-
# Run install subcommand for PATH setup (without version argument)
1898-
_run_claude_install_setup()
1899-
1900-
# Ensure ~/.local/bin is in PATH
1851+
# Configure PATH directly without calling 'claude install'.
1852+
# The 'claude install' subcommand triggers auto-update behavior,
1853+
# which would replace the downloaded specific version with the latest.
1854+
# Using ensure_local_bin_in_path_windows() updates PATH via Windows
1855+
# registry and current session without any auto-update risk.
19011856
info('Updating PATH for native installation...')
19021857
ensure_local_bin_in_path_windows()
19031858

@@ -2115,8 +2070,8 @@ def install_claude_native_macos(version: str | None = None) -> bool:
21152070
21162071
Implements a hybrid approach to work around Anthropic's installer bug:
21172072
- For latest version (None or 'latest'): Use native installer with 'latest'
2118-
- For specific versions: Download directly from GCS bucket, then run
2119-
'claude install' for PATH setup (bypasses buggy version check)
2073+
- For specific versions: Download directly from GCS bucket, then configure
2074+
PATH directly via shell profiles (avoids auto-update behavior)
21202075
21212076
Args:
21222077
version: Specific version to install (e.g., "2.0.76"). If None or
@@ -2147,29 +2102,28 @@ def install_claude_native_macos(version: str | None = None) -> bool:
21472102
# Make binary executable
21482103
native_path.chmod(0o755)
21492104

2150-
# Run 'claude install' for PATH setup (without version to avoid bug)
2151-
if _run_claude_install_setup():
2152-
_ensure_local_bin_in_path_unix()
2105+
# Configure PATH directly without calling 'claude install'.
2106+
# The 'claude install' subcommand triggers auto-update behavior,
2107+
# which would replace the downloaded specific version with the latest.
2108+
# Using _ensure_local_bin_in_path_unix() updates shell profiles
2109+
# and current session PATH without any auto-update risk.
2110+
_ensure_local_bin_in_path_unix()
21532111

2154-
time.sleep(1)
2112+
time.sleep(1)
21552113

2156-
# Verify installation
2157-
is_installed, claude_path, source = verify_claude_installation()
2158-
if is_installed and source == 'native':
2159-
success(f'Native installation verified at: {claude_path}')
2160-
return True
2161-
if is_installed:
2162-
warning(f'Claude found but from {source} source at: {claude_path}')
2163-
warning('Direct download did not create expected file at ~/.local/bin/claude')
2164-
error('Installation failed - file not created at expected location')
2165-
return False
2166-
warning('Installation failed - no Claude executable found')
2167-
error('Claude not accessible after direct download')
2114+
# Verify installation
2115+
is_installed, claude_path, source = verify_claude_installation()
2116+
if is_installed and source == 'native':
2117+
success(f'Native installation verified at: {claude_path}')
2118+
return True
2119+
if is_installed:
2120+
warning(f'Claude found but from {source} source at: {claude_path}')
2121+
warning('Direct download did not create expected file at ~/.local/bin/claude')
2122+
error('Installation failed - file not created at expected location')
21682123
return False
2169-
# Install setup failed, but binary exists - try to continue
2170-
warning('PATH setup failed, but binary was downloaded')
2171-
_ensure_local_bin_in_path_unix()
2172-
return native_path.exists()
2124+
warning('Installation failed - no Claude executable found')
2125+
error('Claude not accessible after direct download')
2126+
return False
21732127

21742128
# GCS download failed - fall back to native installer with "latest"
21752129
warning(f'Direct download failed for version {version}, falling back to native installer')
@@ -2266,8 +2220,8 @@ def install_claude_native_linux(version: str | None = None) -> bool:
22662220
22672221
Implements a hybrid approach to work around Anthropic's installer bug:
22682222
- For latest version (None or 'latest'): Use native installer with 'latest'
2269-
- For specific versions: Download directly from GCS bucket, then run
2270-
'claude install' for PATH setup (bypasses buggy version check)
2223+
- For specific versions: Download directly from GCS bucket, then configure
2224+
PATH directly via shell profiles (avoids auto-update behavior)
22712225
22722226
Supports: Ubuntu 20.04+, Debian 10+, and other modern Linux distributions.
22732227
@@ -2303,29 +2257,28 @@ def install_claude_native_linux(version: str | None = None) -> bool:
23032257
# Make binary executable
23042258
native_path.chmod(0o755)
23052259

2306-
# Run 'claude install' for PATH setup (without version to avoid bug)
2307-
if _run_claude_install_setup():
2308-
_ensure_local_bin_in_path_unix()
2260+
# Configure PATH directly without calling 'claude install'.
2261+
# The 'claude install' subcommand triggers auto-update behavior,
2262+
# which would replace the downloaded specific version with the latest.
2263+
# Using _ensure_local_bin_in_path_unix() updates shell profiles
2264+
# and current session PATH without any auto-update risk.
2265+
_ensure_local_bin_in_path_unix()
23092266

2310-
time.sleep(1)
2267+
time.sleep(1)
23112268

2312-
# Verify installation
2313-
is_installed, claude_path, source = verify_claude_installation()
2314-
if is_installed and source == 'native':
2315-
success(f'Native installation verified at: {claude_path}')
2316-
return True
2317-
if is_installed:
2318-
warning(f'Claude found but from {source} source at: {claude_path}')
2319-
warning('Direct download did not create expected file at ~/.local/bin/claude')
2320-
error('Installation failed - file not created at expected location')
2321-
return False
2322-
warning('Installation failed - no Claude executable found')
2323-
error('Claude not accessible after direct download')
2269+
# Verify installation
2270+
is_installed, claude_path, source = verify_claude_installation()
2271+
if is_installed and source == 'native':
2272+
success(f'Native installation verified at: {claude_path}')
2273+
return True
2274+
if is_installed:
2275+
warning(f'Claude found but from {source} source at: {claude_path}')
2276+
warning('Direct download did not create expected file at ~/.local/bin/claude')
2277+
error('Installation failed - file not created at expected location')
23242278
return False
2325-
# Install setup failed, but binary exists - try to continue
2326-
warning('PATH setup failed, but binary was downloaded')
2327-
_ensure_local_bin_in_path_unix()
2328-
return native_path.exists()
2279+
warning('Installation failed - no Claude executable found')
2280+
error('Claude not accessible after direct download')
2281+
return False
23292282

23302283
# GCS download failed - fall back to native installer with "latest"
23312284
warning(f'Direct download failed for version {version}, falling back to native installer')

tests/test_install_claude_additional.py

Lines changed: 15 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -687,55 +687,6 @@ def test_download_claude_direct_from_gcs_ssl_fallback(
687687
mock_replace.assert_called() # Atomic file move
688688

689689

690-
class TestRunClaudeInstallSetup:
691-
"""Test claude.exe install subcommand for PATH setup."""
692-
693-
@patch('install_claude.run_command')
694-
@patch('pathlib.Path.exists', return_value=True)
695-
def test_run_claude_install_setup_success(self, mock_exists, mock_run):
696-
"""Test successful PATH setup via install subcommand."""
697-
# Verify mock configuration
698-
assert mock_exists.return_value is True
699-
mock_run.return_value = subprocess.CompletedProcess([], 0, '', '')
700-
701-
result = install_claude._run_claude_install_setup()
702-
703-
assert result is True
704-
mock_run.assert_called_once()
705-
# Verify install subcommand was called without version
706-
call_args = mock_run.call_args[0][0]
707-
assert 'install' in call_args
708-
assert len(call_args) == 2 # Only [path, 'install'], no version
709-
710-
@patch('pathlib.Path.exists', return_value=False)
711-
def test_run_claude_install_setup_binary_not_found(self, mock_exists):
712-
"""Test PATH setup when binary doesn't exist."""
713-
# Verify mock configuration
714-
assert mock_exists.return_value is False
715-
716-
result = install_claude._run_claude_install_setup()
717-
718-
assert result is False
719-
720-
@patch('install_claude.run_command')
721-
@patch('pathlib.Path.exists', return_value=True)
722-
def test_run_claude_install_setup_nonzero_but_functional(self, mock_exists, mock_run):
723-
"""Test PATH setup with non-zero return but binary is functional."""
724-
# Verify mock configuration
725-
assert mock_exists.return_value is True
726-
# First call (install) returns non-zero
727-
# Second call (--version) succeeds
728-
mock_run.side_effect = [
729-
subprocess.CompletedProcess([], 1, '', 'Some warning'),
730-
subprocess.CompletedProcess([], 0, 'claude 2.0.76', ''),
731-
]
732-
733-
result = install_claude._run_claude_install_setup()
734-
735-
assert result is True
736-
assert mock_run.call_count == 2
737-
738-
739690
class TestHybridInstallApproach:
740691
"""Test the hybrid installation approach for Windows."""
741692

@@ -775,7 +726,6 @@ def test_install_claude_native_windows_latest_uses_installer(
775726

776727
@patch('platform.system', return_value='Windows')
777728
@patch('install_claude._download_claude_direct_from_gcs')
778-
@patch('install_claude._run_claude_install_setup')
779729
@patch('install_claude.ensure_local_bin_in_path_windows')
780730
@patch('install_claude.verify_claude_installation')
781731
@patch('time.sleep')
@@ -784,15 +734,17 @@ def test_install_claude_native_windows_specific_version_uses_gcs(
784734
mock_sleep,
785735
mock_verify,
786736
mock_ensure_path,
787-
mock_setup,
788737
mock_gcs_download,
789738
mock_system,
790739
):
791-
"""Test that specific version uses GCS direct download."""
740+
"""Test that specific version uses GCS direct download.
741+
742+
The implementation configures PATH directly via ensure_local_bin_in_path_windows()
743+
without calling 'claude install', which would trigger auto-update behavior.
744+
"""
792745
# Verify mock configuration
793746
assert mock_system.return_value == 'Windows'
794747
mock_gcs_download.return_value = True
795-
mock_setup.return_value = True
796748
mock_verify.return_value = (True, 'C:\\Users\\Test\\.local\\bin\\claude.exe', 'native')
797749

798750
result = install_claude.install_claude_native_windows(version='2.0.76')
@@ -1043,7 +995,6 @@ def test_install_claude_native_macos_latest_uses_installer(
1043995

1044996
@patch('sys.platform', 'darwin')
1045997
@patch('install_claude._download_claude_direct_from_gcs')
1046-
@patch('install_claude._run_claude_install_setup')
1047998
@patch('install_claude._ensure_local_bin_in_path_unix')
1048999
@patch('install_claude.verify_claude_installation')
10491000
@patch('pathlib.Path.chmod')
@@ -1054,12 +1005,14 @@ def test_install_claude_native_macos_specific_version_uses_gcs(
10541005
mock_chmod,
10551006
mock_verify,
10561007
mock_ensure_path,
1057-
mock_setup,
10581008
mock_gcs_download,
10591009
):
1060-
"""Test that specific version uses GCS direct download on macOS."""
1010+
"""Test that specific version uses GCS direct download on macOS.
1011+
1012+
The implementation configures PATH directly via _ensure_local_bin_in_path_unix()
1013+
without calling 'claude install', which would trigger auto-update behavior.
1014+
"""
10611015
mock_gcs_download.return_value = True
1062-
mock_setup.return_value = True
10631016
mock_verify.return_value = (True, '/Users/Test/.local/bin/claude', 'native')
10641017

10651018
result = install_claude.install_claude_native_macos(version='2.0.76')
@@ -1136,7 +1089,6 @@ def test_install_claude_native_linux_latest_uses_installer(
11361089

11371090
@patch('platform.system', return_value='Linux')
11381091
@patch('install_claude._download_claude_direct_from_gcs')
1139-
@patch('install_claude._run_claude_install_setup')
11401092
@patch('install_claude._ensure_local_bin_in_path_unix')
11411093
@patch('install_claude.verify_claude_installation')
11421094
@patch('pathlib.Path.chmod')
@@ -1147,13 +1099,15 @@ def test_install_claude_native_linux_specific_version_uses_gcs(
11471099
mock_chmod,
11481100
mock_verify,
11491101
mock_ensure_path,
1150-
mock_setup,
11511102
mock_gcs_download,
11521103
mock_platform,
11531104
):
1154-
"""Test that specific version uses GCS direct download on Linux."""
1105+
"""Test that specific version uses GCS direct download on Linux.
1106+
1107+
The implementation configures PATH directly via _ensure_local_bin_in_path_unix()
1108+
without calling 'claude install', which would trigger auto-update behavior.
1109+
"""
11551110
mock_gcs_download.return_value = True
1156-
mock_setup.return_value = True
11571111
mock_verify.return_value = (True, '/home/test/.local/bin/claude', 'native')
11581112

11591113
result = install_claude.install_claude_native_linux(version='2.0.76')

0 commit comments

Comments
 (0)