Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Container config lives at `~/.config/claude-isolated/home/` (override with `CLAU
./tests/test-lifecycle.sh
```

Requires Podman running. Builds image, starts a container, verifies tools (python3, uv, claude, git, zellij), checks workspace, then stops and cleans up.
Requires Podman or Docker. Builds image, starts a container, verifies tools (python3, uv, claude, git, zellij), checks workspace, then stops and cleans up.

## Conventions

Expand All @@ -43,4 +43,5 @@ Requires Podman running. Builds image, starts a container, verifies tools (pytho
- Image tag is `claude-isolated:latest`
- No nodejs/npm — Claude Code installed via `claude.ai/install.sh`
- No zellij — Claude runs directly in the container
- Supports both Podman and Docker (auto-detected, prefers Podman)
- Requires git 2.48+ in the container (for relative worktree paths); installed from Debian sid
30 changes: 21 additions & 9 deletions src/claude_isolated/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Run Claude Code in isolated Podman containers."""
"""Run Claude Code in isolated containers (Podman or Docker)."""

import argparse
import contextlib
Expand All @@ -15,6 +15,17 @@
IMAGE = "claude-isolated:latest"


def runtime() -> str:
for cmd in ("podman", "docker"):
if shutil.which(cmd):
return cmd
print("Neither podman nor docker found in PATH", file=sys.stderr)
sys.exit(1)


RUNTIME = runtime()


def generate_name() -> str:
if shutil.which("random-name"):
return f"claude-isolated-{subprocess.check_output(['random-name'], text=True).strip()}"
Expand Down Expand Up @@ -111,7 +122,8 @@ def cmd_init(args: argparse.Namespace) -> None:

def cmd_build(args: argparse.Namespace, silent: bool = False) -> None:
kwargs = {"stdout": subprocess.DEVNULL, "stderr": subprocess.DEVNULL} if silent else {}
subprocess.run(["podman", "build", "-t", IMAGE, str(PROJECT_ROOT)], check=True, **kwargs)
containerfile = str(PROJECT_ROOT / "Containerfile")
subprocess.run([RUNTIME, "build", "-f", containerfile, "-t", IMAGE, str(PROJECT_ROOT)], check=True, **kwargs)
if not silent:
print(f"Image built: {IMAGE}")

Expand Down Expand Up @@ -146,7 +158,7 @@ def cmd_start(args: argparse.Namespace) -> None:
# TODO: consider re-adding per-file required checks

run_args = [
"podman", "run", "-it",
RUNTIME, "run", "-it",
"--name", name,
*[flag for src, dest, mode in volumes for flag in ("-v", f"{src}:{dest}:{mode}")],
]
Expand All @@ -166,19 +178,19 @@ def cmd_start(args: argparse.Namespace) -> None:
subprocess.run(run_args)
finally:
# Ensure the container gets stopped and removed on exit (including on Ctrl+C or SIGHUP, which happens when closing a terminal tab for example).
subprocess.run(["podman", "stop", "-t", "10", name],
subprocess.run([RUNTIME, "stop", "-t", "10", name],
capture_output=True)
subprocess.run(["podman", "rm", name],
subprocess.run([RUNTIME, "rm", name],
capture_output=True)



def cmd_stop(args: argparse.Namespace) -> None:
stop = subprocess.run(["podman", "stop", "-t", "2", args.name], capture_output=True)
stop = subprocess.run([RUNTIME, "stop", "-t", "2", args.name], capture_output=True)
if stop.returncode != 0:
print(f"Failed to stop container: {args.name}", file=sys.stderr)
sys.exit(1)
rm = subprocess.run(["podman", "rm", args.name], capture_output=True)
rm = subprocess.run([RUNTIME, "rm", args.name], capture_output=True)
if rm.returncode != 0:
print(f"Stopped but failed to remove container: {args.name}", file=sys.stderr)
sys.exit(1)
Expand All @@ -187,7 +199,7 @@ def cmd_stop(args: argparse.Namespace) -> None:

def cmd_ls(args: argparse.Namespace) -> None:
subprocess.run([
"podman", "ps",
RUNTIME, "ps",
"--filter", f"ancestor={IMAGE}",
"--format", "table {{.Names}}\t{{.Status}}\t{{.Created}}",
], check=True)
Expand All @@ -206,7 +218,7 @@ def main() -> None:
cmd_start(argparse.Namespace(prompt=prompt))
return

parser = argparse.ArgumentParser(description="Run Claude Code in isolated Podman containers")
parser = argparse.ArgumentParser(description="Run Claude Code in isolated containers")
sub = parser.add_subparsers(dest="command")

sub.add_parser("init", help="Create config directory from template")
Expand Down