[BUG] [SECURITY] Claude binary distribution auto-installs itself to ~/.local without asking for user permission
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
The issue
New binary distribution of CC produces unexpected side effects which cannot be (easily) suppressed.
In particular, it
- reports error saying that despite native installer is used,
~/.local/bin/claudeis missing;
- it then installs itself to
~/.local, without asking for a permission to do so, and without providing any configuration of this behavior.
Why it's bad
Security related impact
Manifesting with unwanted behavior without obtaining a user consent raises security concerns.
It cannot be trivially suppressed
If I'm not missing anything, claude --help doesn't specify a flag or environment variable to disable this behavior.
It violates principle of least astonishment
By calling claude --help one may discover install functionality and reasonably assume that this is (the only) entry point to trigger this behavior.
Despite its lacking meaningful config options<sup>1</sup> which is separate concern, at least it implies users's clear intent to install the software, which is not the case with silent installation in background.
It's potentially breaking change of the system state
No legitimate software shall assume it may silently introduce itself into users's PATH because this has a potential of breaking user setups.
It's not compatible with environments enforcing isolation of the installed software
I'm primarily talking about nix, and in this case - primarily about FS-level isolation.
However not only nix provides such a functionality.
---
<sup>1</sup> Having just distribution flavor and --force is quite a limited choice of specifying installation behavior
Example
CC's auto installing itself to ~/.local is not compatible with nix way of installing it.
The below are
- Claude Code nix derivation
- manifest fetcher for Claude Code installation which adopts it for usage by nix derivation
claude-code.nix
fetch-claude-code-manifest.py
<details><summary>Or click here to expand</summary>
```claude-code.nix
{
fetchurl,
makeWrapper,
stdenvNoCC,
}: let
meta-info = builtins.fromJSON (builtins.readFile ./meta-info.json);
inherit (meta-info) platforms version;
platform = let
inherit (stdenvNoCC.hostPlatform) darwinArch linuxArch;
in
if stdenvNoCC.isDarwin
then "darwin-${darwinArch}"
else if stdenvNoCC.isLinux
then "linux-${linuxArch}"
else "unknown";
in
assert (platform != "unknown");
stdenvNoCC.mkDerivation {
pname = "claude-code";
inherit version;
src = fetchurl {
inherit (platforms."${platform}") url sha256;
};
dontUnpack = true;
nativeBuildInputs = [makeWrapper];
installPhase = ''
runHook preInstall
install -Dm755 $src $out/bin/claude
runHook postInstall
'';
# TODO: find a way to disable automatic installation to ~/.local
postInstall = ''
wrapProgram $out/bin/claude \
--set DISABLE_AUTOUPDATER 1 \
--unset DEV
'';
}
```fetch-claude-code-manifest.py
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = ["requests"]
# ///
import dataclasses as d
import json
import logging
import re
import sys
import requests
logging.basicConfig(stream=sys.stderr, level=logging.INFO)
logger = logging.getLogger("fetch-meta")
gcs_bucket = "https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases"
_version_re = re.compile(r"^\d+\.\d+\.\d+$")
@d.dataclass(frozen=True, slots=True)
class PlatformEntry:
checksum: d.InitVar[str]
size: int
url: str = ""
sha256: str = d.field(init=False)
def __post_init__(self, checksum: str) -> None:
object.__setattr__(self, "sha256", checksum)
@d.dataclass(frozen=True, slots=True)
class Manifest:
version: str
build_date: str
platforms: dict[str, PlatformEntry]
def fetch_latest_version() -> str:
url = f"{gcs_bucket}/latest"
logger.debug(f"fetching latest version from {url}")
r = requests.get(url)
r.raise_for_status()
version = r.text.strip()
if not _version_re.match(version):
raise ValueError(f"invalid version format: {version!r}")
logger.debug(f"latest version: {version}")
return version
def manifest_with_urls() -> Manifest:
version = fetch_latest_version()
url = f"{gcs_bucket}/{version}/manifest.json"
logger.debug(f"fetching manifest from {url}")
r = requests.get(url)
r.raise_for_status()
data: dict = r.json()
logger.debug(f"raw manifest: {data}")
platforms = {
platform.replace("-x64", "-x86_64"): PlatformEntry(
url=f"{gcs_bucket}/{version}/{platform}/claude",
**entry,
)
for platform, entry in data["platforms"].items()
}
return Manifest(
version=data["version"],
build_date=data["buildDate"],
platforms=platforms,
)
if __name__ == "__main__":
try:
logger.info("Getting meta info...")
manifest = manifest_with_urls()
print(json.dumps(d.asdict(manifest), indent=2))
except Exception as e:
logger.error(str(e))
</details>
What Should Happen?
- Despite I acknowledge that CC installing itself to ~/.local might be acceptable behavior for users who don't have a strong opinion about it and just "want things to work", it MUST request explicit user consent to do so
- This behavior MUST be suppressible via, ideally, an environment variable or in any other reasonable way
Error Messages/Logs
Not applicable
Steps to Reproduce
Given the artifacts above:
1.
nix shell nixpkgs#uv
chmod +x ./fetch-claude-code-manifest.py
./fetch-claude-code-manifest.py | tee meta-info.json
nix-build claude-code.nix
./result/bin/claude
- Observe the described behavior
Claude Model
None
Is this a regression?
Yes, this worked in a previous version
Last Working Version
npm-based distributions
Claude Code Version
2.1.23
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Other
Additional Information
_No response_
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗