From 3f06ce68731f755221bc5233b3f9e4a2cb26e0b6 Mon Sep 17 00:00:00 2001 From: Dan Guido Date: Mon, 19 Jan 2026 16:30:47 -0500 Subject: [PATCH] Improve uv install, fix gitconfig, add security docs - Use COPY from ghcr.io/astral-sh/uv:latest instead of curl install - Fix gitconfig bug when host config is mounted read-only - Add global gitignore for common patterns (.claude/, .DS_Store, etc.) - Use GIT_CONFIG_GLOBAL env var for container-local git settings - Add Security Model section to README explaining sandbox boundaries - Change postCreateCommand to use uv run Co-Authored-By: Claude Opus 4.5 --- Dockerfile | 5 ++- README.md | 35 +++++++++++++++++++++ devcontainer.json | 5 +-- post_install.py | 80 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 118 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 1ba3346..d18b2ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -59,9 +59,8 @@ RUN ARCH=$(dpkg --print-architecture) && \ dpkg -i /tmp/git-delta.deb && \ rm /tmp/git-delta.deb -# Install uv (Python package manager) -ENV UV_INSTALL_DIR="/usr/local/bin" -RUN curl -LsSf https://astral.sh/uv/install.sh | sh +# Install uv (Python package manager) via multi-stage copy +COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv ARG USERNAME=ubuntu diff --git a/README.md b/README.md index 7e7e4c9..5461015 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,41 @@ The following data persists across container rebuilds: Your host `~/.gitconfig` is mounted read-only for git identity. +## Security Model + +This devcontainer provides **filesystem isolation** but not complete sandboxing. + +### What IS sandboxed + +- **Filesystem**: Container has its own filesystem; your host files outside the mounted workspace are inaccessible +- **Processes**: Container processes are isolated from host processes +- **Package installations**: npm/pip installs stay in the container + +### What is NOT sandboxed + +- **Network**: Full outbound network access (can make API calls, download packages, etc.) +- **Git identity**: Your `~/.gitconfig` is mounted read-only +- **Docker socket**: Not mounted by default, but can be added if needed + +### The `bypassPermissions` setting + +This container auto-configures Claude Code with `bypassPermissions` mode, which: + +- Allows Claude to run commands without confirmation prompts +- Is appropriate here because the container itself is the sandbox +- Would be risky on a host machine but is safe in this isolated environment + +### Network isolation (optional) + +For stricter security, you can enable network restrictions using the included iptables tools: + +```bash +# Example: Block all outbound except specific hosts +sudo iptables -A OUTPUT -d api.anthropic.com -j ACCEPT +sudo iptables -A OUTPUT -d github.com -j ACCEPT +sudo iptables -A OUTPUT -j DROP +``` + ## Auto-Configuration On container creation, `post_install.py` automatically: diff --git a/devcontainer.json b/devcontainer.json index bba75e9..f12c2be 100644 --- a/devcontainer.json +++ b/devcontainer.json @@ -50,9 +50,10 @@ "containerEnv": { "NODE_OPTIONS": "--max-old-space-size=4096", "CLAUDE_CONFIG_DIR": "/home/ubuntu/.claude", - "POWERLEVEL9K_DISABLE_GITSTATUS": "true" + "POWERLEVEL9K_DISABLE_GITSTATUS": "true", + "GIT_CONFIG_GLOBAL": "/home/ubuntu/.gitconfig.local" }, "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated", "workspaceFolder": "/workspace", - "postCreateCommand": "python3 /opt/post_install.py" + "postCreateCommand": "uv run /opt/post_install.py" } diff --git a/post_install.py b/post_install.py index bf649d2..095e18f 100644 --- a/post_install.py +++ b/post_install.py @@ -107,8 +107,10 @@ def setup_git_config(): """Set up git delta as the default pager if not already configured.""" gitconfig = Path.home() / ".gitconfig" - # Only add delta config if .gitconfig doesn't exist or doesn't have pager config - if not gitconfig.exists(): + # Skip if .gitconfig exists (likely mounted from host) + if gitconfig.exists(): + print(f"[post_install] Git config exists (mounted from host): {gitconfig}") + else: config = """\ [core] pager = delta @@ -131,6 +133,79 @@ def setup_git_config(): print(f"[post_install] Git config created: {gitconfig}") +def setup_global_gitignore(): + """Set up global gitignore and local git config. + + Since ~/.gitconfig is mounted read-only from host, we create a local + config file that includes the host config and adds container-specific + settings like core.excludesfile. + + GIT_CONFIG_GLOBAL env var (set in devcontainer.json) points git to this + local config as the "global" config. + """ + home = Path.home() + gitignore = home / ".gitignore_global" + local_gitconfig = home / ".gitconfig.local" + host_gitconfig = home / ".gitconfig" + + # Create global gitignore with common patterns + patterns = """\ +# Claude Code +.claude/ + +# macOS +.DS_Store +.AppleDouble +.LSOverride +._* + +# Python +*.pyc +*.pyo +__pycache__/ +*.egg-info/ +.eggs/ +*.egg +.venv/ +venv/ +.mypy_cache/ +.ruff_cache/ + +# Node +node_modules/ +.npm/ + +# Editors +*.swp +*.swo +*~ +.idea/ +.vscode/ +*.sublime-* + +# Misc +*.log +.env.local +.env.*.local +""" + gitignore.write_text(patterns) + print(f"[post_install] Global gitignore created: {gitignore}") + + # Create local git config that includes host config and sets excludesfile + local_config = f"""\ +# Container-local git config +# Includes host config (mounted read-only) and adds container settings + +[include] + path = {host_gitconfig} + +[core] + excludesfile = {gitignore} +""" + local_gitconfig.write_text(local_config) + print(f"[post_install] Local git config created: {local_gitconfig}") + + def main(): """Run all post-install configuration.""" print("[post_install] Starting post-install configuration...") @@ -139,6 +214,7 @@ def main(): setup_tmux_config() fix_directory_ownership() setup_git_config() + setup_global_gitignore() print("[post_install] Configuration complete!")