From 4605a62d90f1542638f0d954835769f5d22f7b16 Mon Sep 17 00:00:00 2001 From: Jeena Date: Wed, 18 Mar 2026 03:53:55 +0000 Subject: [PATCH] Switch to native Claude Code installer and add update command Replace the deprecated npm installation with the native installer. The binary is installed to /usr/local/bin so it survives the home directory bind mount at runtime. Add a 'claude update' subcommand that rebuilds the image with the latest Claude Code binary and removes all existing containers. Disable the in-container auto-updater since the binary lives in the read-only image layer. --- Dockerfile | 11 ++++++++++- README.md | 16 ++++++++++++++++ claude-container.py | 39 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 64 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index f3f49ba..350e2ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,13 +13,22 @@ RUN pacman -Syu --noconfirm \ ripgrep \ nodejs \ npm \ + curl \ sudo && \ groupadd -g ${GID} ${USERNAME} && \ useradd -m -u ${UID} -g ${GID} -s /bin/bash ${USERNAME} && \ echo "${USERNAME} ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers && \ - npm install -g @anthropic-ai/claude-code && \ pacman -Scc --noconfirm +# Install Claude Code using the native installer, then copy the binary +# to a system-wide location so it survives the home directory bind mount +USER ${USERNAME} +WORKDIR /tmp +RUN curl -fsSL https://claude.ai/install.sh | bash && \ + sudo cp ~/.local/bin/claude /usr/local/bin/claude && \ + rm -rf ~/.local/share/claude ~/.local/bin/claude ~/.claude ~/.claude.json \ + ~/.cache/claude + USER ${USERNAME} WORKDIR /home/${USERNAME} diff --git a/README.md b/README.md index e8dbc75..8159a5f 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ the host. - **Hard linking support**: Can hard link files like `~/.gitconfig` to share configurations with containers - Mounts only the current project directory (same absolute path inside container) - **Security boundary**: No access to SSH keys, passwords, or full `$HOME` (intentionally prevents remote code pushes) +- **Easy updates**: `claude update` rebuilds the image with the latest Claude Code - Simple shell function (`claude`) to launch interactively ## Install @@ -55,6 +56,21 @@ The image is built automatically on first use if it does not already exist. Claude Code starts inside the container with the current directory mounted and set as the working directory. +### Updating Claude Code + +To update Claude Code to the latest version: + +``` +claude update +``` + +This rebuilds the Docker image with the latest Claude Code binary and removes +all existing containers. Containers are recreated automatically on the next +run. The persistent home directory is not affected. + +The in-container auto-updater is disabled because Claude Code is installed in +the image layer. Use `claude update` to get new versions. + ## Sharing host config files via hard links The container home at `~/.local/share/claude-container/container-home/` is mounted diff --git a/claude-container.py b/claude-container.py index 9170646..f237892 100755 --- a/claude-container.py +++ b/claude-container.py @@ -41,6 +41,9 @@ class ClaudeContainer: return self._get_xdg_data_home() / 'claude-container' / 'container-home' def run(self, args: list[str]) -> None: + if args and args[0] == "update": + self.update() + return self.container_home_path.mkdir(parents=True, exist_ok=True) if self.container_exists() and self.container_running(): logger.error(f"Project '{self.project_path.name}' already has a running Claude container.") @@ -86,6 +89,40 @@ class ClaudeContainer: str(self.docker_context_dir), ]) + def update(self) -> None: + logger.info("Updating Claude Code...") + logger.info("Removing existing containers...") + result = subprocess.run( + ["docker", "ps", "-a", "--filter", "name=^cc-", "--format", "{{.Names}}"], + capture_output=True, + text=True, + ) + for name in result.stdout.strip().splitlines(): + if name: + logger.info(f" Removing container '{name}'") + subprocess.run( + ["docker", "rm", "-f", name], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if self.image_exists(): + logger.info(f"Removing image '{self.IMAGE}'...") + subprocess.run( + ["docker", "rmi", self.IMAGE], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + logger.info("Rebuilding image with latest Claude Code...") + self._run([ + "docker", "build", "--no-cache", + "--build-arg", f"USERNAME={self.host_username}", + "--build-arg", f"UID={self.host_uid}", + "--build-arg", f"GID={self.host_gid}", + "-t", self.IMAGE, + str(self.docker_context_dir), + ]) + logger.info("Update complete. Containers will be recreated on next run.") + def create_container(self) -> None: logger.info(f"Creating container '{self.container_name}'") self._run([ @@ -118,7 +155,7 @@ class ClaudeContainer: ) def exec_claude(self, args: list[str]) -> None: - env_args = [] + env_args = ["-e", "DISABLE_AUTOUPDATER=1"] for var in ("ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"): val = os.environ.get(var) if val: