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 <noreply@anthropic.com>
This commit is contained in:
Dan Guido
2026-01-19 16:30:47 -05:00
parent 1585b511ed
commit 3f06ce6873
4 changed files with 118 additions and 7 deletions
+2 -3
View File
@@ -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
+35
View File
@@ -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:
+3 -2
View File
@@ -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"
}
+78 -2
View File
@@ -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!")