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: