Add Claude Code container management scripts

Scripts to run Claude Code inside an Arch Linux Docker container
that mirrors the local development environment while limiting
access to sensitive host files.

Includes per-project container isolation, a shared persistent home
directory, and a shell alias for launching Claude interactively
from any project directory.
This commit is contained in:
Jeena 2026-03-05 12:07:20 +00:00
commit a9a645abca
6 changed files with 284 additions and 0 deletions

173
claude-container.py Executable file
View file

@ -0,0 +1,173 @@
#!/usr/bin/env python3
import hashlib
import logging
import os
import subprocess
import sys
import time
from pathlib import Path
logging.basicConfig(
level=logging.INFO,
format='%(message)s',
stream=sys.stderr
)
logger = logging.getLogger(__name__)
class ClaudeContainer:
IMAGE = "claude-container:latest"
PROJECT_ID_LEN = 12
START_TIMEOUT = 3.0
def __init__(self):
self.project_path = Path.cwd().resolve()
self.project_id = self._project_id(self.project_path)
self.host_username = os.environ.get('USER', 'dev')
self.host_uid = os.getuid()
self.host_gid = os.getgid()
self.container_name = f"cc-{self.project_path.name}-{self.project_id}"
self.docker_context_dir = Path(__file__).resolve().parent
def _get_xdg_data_home(self) -> Path:
xdg_data = os.environ.get('XDG_DATA_HOME')
if xdg_data:
return Path(xdg_data)
return Path.home() / '.local' / 'share'
@property
def container_home_path(self) -> Path:
return self._get_xdg_data_home() / 'claude-container' / 'container-home'
def run(self, args: list[str]) -> None:
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.")
logger.error(f"Container name: {self.container_name}")
logger.error("Wait for the current instance to finish or manually stop it with:")
logger.error(f" docker stop {self.container_name}")
sys.exit(1)
try:
relative_path = self.project_path.relative_to(Path.home())
(self.container_home_path / relative_path).mkdir(parents=True, exist_ok=True)
except ValueError:
pass
if not self.image_exists():
self.build_image()
if not self.container_exists():
self.create_container()
if not self.start_container():
logger.warning("Recreating container due to failed start")
self.remove_container()
self.create_container()
if not self.start_container():
raise RuntimeError("Container failed to start after recreation")
try:
self.exec_claude(args)
finally:
self.stop_container()
def image_exists(self) -> bool:
return subprocess.run(
["docker", "image", "inspect", self.IMAGE],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode == 0
def build_image(self) -> None:
logger.info(f"Building image '{self.IMAGE}' with user {self.host_username} ({self.host_uid}:{self.host_gid})")
self._run([
"docker", "build",
"--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),
])
def create_container(self) -> None:
logger.info(f"Creating container '{self.container_name}'")
self._run([
"docker", "create",
"--name", self.container_name,
"--user", f"{self.host_uid}:{self.host_gid}",
"--volume", f"{self.project_path}:{self.project_path}",
"--volume", f"{self.container_home_path}:/home/{self.host_username}",
self.IMAGE,
"sh", "-c", "sleep infinity",
])
def start_container(self) -> bool:
subprocess.run(
["docker", "start", self.container_name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
return self.wait_until_running()
def stop_container(self) -> None:
if self.container_running():
self._run(["docker", "stop", "-t", "0", self.container_name])
def remove_container(self) -> None:
subprocess.run(
["docker", "rm", "-f", self.container_name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
def exec_claude(self, args: list[str]) -> None:
env_args = []
for var in ("ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"):
val = os.environ.get(var)
if val:
env_args += ["-e", f"{var}={val}"]
subprocess.run([
"docker", "exec",
"-it",
*env_args,
"-w", str(self.project_path),
self.container_name,
"claude",
*args,
], check=True)
def container_exists(self) -> bool:
return subprocess.run(
["docker", "inspect", self.container_name],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
).returncode == 0
def container_running(self) -> bool:
result = subprocess.run(
["docker", "inspect", "-f", "{{.State.Running}}", self.container_name],
capture_output=True,
text=True,
)
return result.returncode == 0 and result.stdout.strip() == "true"
def wait_until_running(self) -> bool:
deadline = time.time() + self.START_TIMEOUT
while time.time() < deadline:
if self.container_running():
return True
time.sleep(0.1)
return False
@staticmethod
def _project_id(path: Path) -> str:
return hashlib.sha256(str(path).encode()).hexdigest()[:12]
@staticmethod
def _run(cmd: list[str]) -> None:
subprocess.run(cmd, check=True)
def main() -> None:
ClaudeContainer().run(sys.argv[1:])
if __name__ == "__main__":
main()