mirror of
https://github.com/trailofbits/skills
synced 2026-06-21 14:12:00 +00:00
Sync skill with claude-code-devcontainer repo (#151)
* sync skill with claude-code-devcontainer repo Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * devcontainer-setup: apply ruff format and shfmt The pre-commit hook was failing on these two files in CI: ruff joins the multi-line f-strings in post_install.py, and shfmt with the repo's '-i 2 -ci' flags adjusts case-body indentation in install.sh. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Dan Guido <dan@trailofbits.com>
This commit is contained in:
@@ -87,7 +87,7 @@
|
||||
},
|
||||
{
|
||||
"name": "devcontainer-setup",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "Create pre-configured devcontainers with Claude Code and language-specific tooling",
|
||||
"author": {
|
||||
"name": "Alexis Challande",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "devcontainer-setup",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"description": "Create pre-configured devcontainers with Claude Code and language-specific tooling",
|
||||
"author": {
|
||||
"name": "Alexis Challande",
|
||||
|
||||
@@ -8,8 +8,9 @@ Create pre-configured devcontainers with Claude Code and language-specific tooli
|
||||
- **Multi-language support**: Python 3.13, Node 22, Rust, Go
|
||||
- **Modern CLI tools**: ripgrep, fd, fzf, tmux, git-delta, ast-grep
|
||||
- **Session persistence**: command history, GitHub CLI auth, Claude config survive rebuilds
|
||||
- **Sandboxing**: bubblewrap and socat for Claude Code sandboxing support
|
||||
- **Network isolation**: iptables/ipset with NET_ADMIN capability for restricting outbound traffic
|
||||
- **Tailscale integration**: Secure networking via devcontainer feature
|
||||
- **Token forwarding**: `CLAUDE_CODE_OAUTH_TOKEN` and `ANTHROPIC_API_KEY` forwarded to container
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -40,6 +41,12 @@ devc up Start the devcontainer
|
||||
devc rebuild Rebuild container (preserves persistent volumes)
|
||||
devc down Stop the container
|
||||
devc shell Open zsh shell in container
|
||||
devc exec <cmd> Execute a command in the running container
|
||||
devc upgrade Upgrade Claude Code to latest version
|
||||
devc mount <h> <c> Add a bind mount to the container
|
||||
devc sync Sync sessions from devcontainers to host
|
||||
devc cp <c> <h> Copy files from container to host
|
||||
devc destroy [-f] Remove container, volumes, and image
|
||||
```
|
||||
|
||||
## Supported Languages
|
||||
@@ -58,10 +65,13 @@ Multi-language projects automatically get all detected configurations merged.
|
||||
The devcontainer provides **filesystem isolation** with **network isolation** capabilities:
|
||||
|
||||
- Container filesystem is isolated from host
|
||||
- `.devcontainer/` mounted read-only inside the container to prevent escape
|
||||
- Your `~/.gitconfig` is mounted read-only
|
||||
- SYS_ADMIN capability blocked by `devc` CLI to protect read-only mounts
|
||||
- Persistent volumes preserve auth across rebuilds
|
||||
- iptables/ipset with NET_ADMIN/NET_RAW capabilities for restricting network access
|
||||
- NPM security settings: scripts disabled, 24-hour package release delay
|
||||
- SSH commit signing supported via `gpg.ssh.program` configuration
|
||||
|
||||
## Reference Material
|
||||
|
||||
|
||||
@@ -90,10 +90,13 @@ Then apply language-specific modifications below.
|
||||
The base template includes:
|
||||
|
||||
- **Claude Code** with marketplace plugins (anthropics/skills, trailofbits/skills, trailofbits/skills-curated)
|
||||
- **Sandboxing** via bubblewrap and socat
|
||||
- **Python 3.13** via uv (fast binary download)
|
||||
- **Node 22** via fnm (Fast Node Manager)
|
||||
- **ast-grep** for AST-based code search
|
||||
- **Network isolation tools** (iptables, ipset) with NET_ADMIN capability
|
||||
- **Security mounts**: `.devcontainer/` mounted read-only to prevent container escape
|
||||
- **Token forwarding**: `CLAUDE_CODE_OAUTH_TOKEN` and `ANTHROPIC_API_KEY` via `remoteEnv`
|
||||
- **Modern CLI tools**: ripgrep, fd, fzf, tmux, git-delta
|
||||
|
||||
---
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
# {{PROJECT_NAME}} Devcontainer
|
||||
# Based on Microsoft devcontainer image for better devcontainer integration
|
||||
ARG UV_VERSION=0.10.0
|
||||
FROM ghcr.io/astral-sh/uv:${UV_VERSION}@sha256:78a7ff97cd27b7124a5f3c2aefe146170793c56a1e03321dd31a289f6d82a04f AS uv
|
||||
FROM mcr.microsoft.com/devcontainers/base:ubuntu-24.04@sha256:d94c97dd9cacf183d0a6fd12a8e87b526e9e928307674ae9c94139139c0c6eae
|
||||
FROM ghcr.io/astral-sh/uv:0.10@sha256:10902f58a1606787602f303954cea099626a4adb02acbac4c69920fe9d278f82 AS uv
|
||||
FROM mcr.microsoft.com/devcontainers/base:ubuntu24.04@sha256:4bcb1b466771b1ba1ea110e2a27daea2f6093f9527fb75ee59703ec89b5561cb
|
||||
|
||||
ARG TZ
|
||||
ENV TZ="$TZ"
|
||||
@@ -44,7 +43,7 @@ RUN ARCH=$(dpkg --print-architecture) && \
|
||||
COPY --from=uv /uv /usr/local/bin/uv
|
||||
|
||||
# Install fzf from GitHub releases (newer than apt, includes built-in shell integration)
|
||||
ARG FZF_VERSION=0.67.0
|
||||
ARG FZF_VERSION=0.70.0
|
||||
RUN ARCH=$(dpkg --print-architecture) && \
|
||||
case "${ARCH}" in \
|
||||
amd64) FZF_ARCH="linux_amd64" ;; \
|
||||
|
||||
@@ -63,6 +63,10 @@
|
||||
"PYTHONDONTWRITEBYTECODE": "1",
|
||||
"PIP_DISABLE_PIP_VERSION_CHECK": "1"
|
||||
},
|
||||
"remoteEnv": {
|
||||
"CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN:}",
|
||||
"ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY:}"
|
||||
},
|
||||
"initializeCommand": "test -f \"$HOME/.gitconfig\" || touch \"$HOME/.gitconfig\"",
|
||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
|
||||
"workspaceFolder": "/workspace",
|
||||
|
||||
@@ -37,6 +37,9 @@ Commands:
|
||||
exec <cmd> Execute a command in the running container
|
||||
upgrade Upgrade Claude Code to latest version
|
||||
mount <host> <cont> Add a mount to the devcontainer (recreates container)
|
||||
sync [project] [--trusted] Sync sessions from devcontainers to host
|
||||
cp <cont> <host> Copy files/directories from container to host
|
||||
destroy [-f] Remove container, volumes, and image for current project
|
||||
help Show this help message
|
||||
|
||||
Examples:
|
||||
@@ -49,6 +52,11 @@ Examples:
|
||||
devc exec ls -la # Run command in container
|
||||
devc upgrade # Upgrade Claude Code to latest
|
||||
devc mount ~/data /data # Add mount to container
|
||||
devc sync # Sync sessions from all devcontainers
|
||||
devc sync crypto # Sync only matching devcontainer
|
||||
devc cp /some/file ./out # Copy a path from container to host
|
||||
devc destroy # Remove all project Docker resources
|
||||
devc destroy -f # Skip confirmation prompt
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -332,6 +340,410 @@ cmd_mount() {
|
||||
log_success "Mount added: $host_path → $container_path"
|
||||
}
|
||||
|
||||
cmd_sync() {
|
||||
local filter=""
|
||||
local trusted=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--trusted)
|
||||
trusted=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
filter="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local host_projects="${HOME}/.claude/projects"
|
||||
|
||||
if [[ "$trusted" == false ]]; then
|
||||
log_warn "This copies files from devcontainers to your host filesystem."
|
||||
log_warn "Only proceed if you trust the container contents."
|
||||
log_info "Use --trusted to skip this prompt."
|
||||
read -p "Continue? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
log_info "Aborted."
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Discover all devcontainers (running + stopped) by label.
|
||||
local container_ids
|
||||
container_ids=$(docker ps -a -q \
|
||||
--filter "label=devcontainer.local_folder" 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$container_ids" ]]; then
|
||||
log_error "No devcontainers found (running or stopped)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# List discovered devcontainers.
|
||||
log_info "Discovered devcontainers:"
|
||||
local matched_any=false
|
||||
while IFS= read -r cid; do
|
||||
local name folder status
|
||||
name=$(sync_get_project_name "$cid")
|
||||
folder=$(docker inspect --format \
|
||||
'{{index .Config.Labels "devcontainer.local_folder"}}' "$cid")
|
||||
status=$(docker inspect --format '{{.State.Status}}' "$cid")
|
||||
|
||||
if [[ -n "$filter" ]]; then
|
||||
if ! echo "$name" | grep -qi "$filter"; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
matched_any=true
|
||||
echo " - ${name} (${status}) ${folder}"
|
||||
done <<<"$container_ids"
|
||||
|
||||
if [[ "$matched_any" == false ]]; then
|
||||
log_error "No devcontainers matching '${filter}'."
|
||||
echo ""
|
||||
echo "Available:"
|
||||
while IFS= read -r cid; do
|
||||
local name status
|
||||
name=$(sync_get_project_name "$cid")
|
||||
status=$(docker inspect --format '{{.State.Status}}' "$cid")
|
||||
echo " - ${name} (${status})"
|
||||
done <<<"$container_ids"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Sync matching containers.
|
||||
while IFS= read -r cid; do
|
||||
local name
|
||||
name=$(sync_get_project_name "$cid")
|
||||
|
||||
if [[ -n "$filter" ]]; then
|
||||
if ! echo "$name" | grep -qi "$filter"; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
sync_one_container "$cid" "$host_projects"
|
||||
echo ""
|
||||
done <<<"$container_ids"
|
||||
|
||||
log_success "Run '/insights' in Claude Code to include these sessions."
|
||||
}
|
||||
|
||||
# Extract project name from devcontainer.local_folder label.
|
||||
sync_get_project_name() {
|
||||
local folder
|
||||
folder=$(docker inspect --format \
|
||||
'{{index .Config.Labels "devcontainer.local_folder"}}' "$1")
|
||||
basename "$folder"
|
||||
}
|
||||
|
||||
# Resolve the Claude projects dir inside a container without
|
||||
# docker exec (works on stopped containers too).
|
||||
# Reads CLAUDE_CONFIG_DIR from container env, falls back to
|
||||
# /home/<user>/.claude.
|
||||
sync_get_claude_projects_dir() {
|
||||
local cid="$1"
|
||||
local claude_dir
|
||||
|
||||
claude_dir=$(docker inspect --format '{{json .Config.Env}}' "$cid" |
|
||||
tr ',' '\n' | tr -d '[]"' |
|
||||
grep '^CLAUDE_CONFIG_DIR=' |
|
||||
cut -d= -f2- || true)
|
||||
|
||||
if [[ -n "$claude_dir" ]]; then
|
||||
echo "${claude_dir}/projects"
|
||||
return
|
||||
fi
|
||||
|
||||
local user
|
||||
user=$(docker inspect --format '{{.Config.User}}' "$cid")
|
||||
if [[ -z "$user" || "$user" == "root" ]]; then
|
||||
echo "/root/.claude/projects"
|
||||
else
|
||||
echo "/home/${user}/.claude/projects"
|
||||
fi
|
||||
}
|
||||
|
||||
sync_one_container() {
|
||||
local cid="$1"
|
||||
local host_projects="$2"
|
||||
local project_name status claude_dir folder
|
||||
|
||||
project_name=$(sync_get_project_name "$cid")
|
||||
folder=$(docker inspect --format \
|
||||
'{{index .Config.Labels "devcontainer.local_folder"}}' "$cid")
|
||||
status=$(docker inspect --format '{{.State.Status}}' "$cid")
|
||||
claude_dir=$(sync_get_claude_projects_dir "$cid")
|
||||
|
||||
log_info "=== ${project_name} (${status}) ==="
|
||||
echo " Host path: ${folder}"
|
||||
echo " Container: ${cid:0:12}"
|
||||
|
||||
# docker cp works on both running and stopped containers.
|
||||
local tmpdir
|
||||
tmpdir=$(mktemp -d)
|
||||
|
||||
if ! docker cp "${cid}:${claude_dir}/." "$tmpdir/" 2>/dev/null; then
|
||||
echo " No sessions found, skipping."
|
||||
rm -rf "$tmpdir"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local session_count
|
||||
session_count=$(find "$tmpdir" -name '*.jsonl' | wc -l | tr -d ' ')
|
||||
|
||||
if [[ "$session_count" -eq 0 ]]; then
|
||||
echo " No sessions found, skipping."
|
||||
rm -rf "$tmpdir"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo " Sessions: ${session_count}"
|
||||
|
||||
local total_copied=0
|
||||
|
||||
# Sync each project key subdirectory.
|
||||
for key_path in "$tmpdir"/*/; do
|
||||
[[ ! -d "$key_path" ]] && continue
|
||||
local key dest_key
|
||||
key=$(basename "$key_path")
|
||||
|
||||
if [[ "$key" == "-workspace" ]]; then
|
||||
dest_key="-devcontainer-${project_name}"
|
||||
else
|
||||
dest_key="${key}"
|
||||
fi
|
||||
|
||||
local dest_dir="${host_projects}/${dest_key}"
|
||||
mkdir -p "$dest_dir"
|
||||
|
||||
local copied=0
|
||||
while IFS= read -r -d '' file; do
|
||||
local rel="${file#"$key_path"}"
|
||||
local dest_file="${dest_dir}/${rel}"
|
||||
mkdir -p "$(dirname "$dest_file")"
|
||||
|
||||
if [[ ! -e "$dest_file" ]] ||
|
||||
[[ "$file" -nt "$dest_file" ]]; then
|
||||
cp -p "$file" "$dest_file"
|
||||
copied=$((copied + 1))
|
||||
fi
|
||||
done < <(find "$key_path" -type f -print0)
|
||||
|
||||
if [[ "$copied" -gt 0 ]]; then
|
||||
echo " Synced ${copied} file(s) -> ${dest_key}"
|
||||
fi
|
||||
total_copied=$((total_copied + copied))
|
||||
done
|
||||
|
||||
# Handle .jsonl files directly in projects/ (no subdirectory).
|
||||
local orphan_copied=0
|
||||
local dest_dir="${host_projects}/-devcontainer-${project_name}"
|
||||
mkdir -p "$dest_dir"
|
||||
|
||||
while IFS= read -r -d '' file; do
|
||||
local name
|
||||
name=$(basename "$file")
|
||||
local dest_file="${dest_dir}/${name}"
|
||||
|
||||
if [[ ! -e "$dest_file" ]] ||
|
||||
[[ "$file" -nt "$dest_file" ]]; then
|
||||
cp -p "$file" "$dest_file"
|
||||
orphan_copied=$((orphan_copied + 1))
|
||||
fi
|
||||
done < <(find "$tmpdir" -maxdepth 1 -name '*.jsonl' -print0)
|
||||
|
||||
if [[ "$orphan_copied" -gt 0 ]]; then
|
||||
echo " Synced ${orphan_copied} file(s) -> -devcontainer-${project_name}"
|
||||
total_copied=$((total_copied + orphan_copied))
|
||||
fi
|
||||
|
||||
rm -rf "$tmpdir"
|
||||
|
||||
echo " Total: ${total_copied} file(s) synced."
|
||||
}
|
||||
|
||||
cmd_cp() {
|
||||
local container_path="${1:-}"
|
||||
local host_path="${2:-}"
|
||||
|
||||
if [[ -z "$container_path" ]] || [[ -z "$host_path" ]]; then
|
||||
log_error "Usage: devc cp <container_path> <host_path>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local workspace_folder
|
||||
workspace_folder="$(get_workspace_folder)"
|
||||
|
||||
# Find the running container
|
||||
local label="devcontainer.local_folder=$workspace_folder"
|
||||
local container_id
|
||||
container_id=$(docker ps -q --filter "label=$label" 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$container_id" ]]; then
|
||||
log_error "No running devcontainer found for $workspace_folder"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "Copying $container_path → $host_path"
|
||||
docker cp "$container_id:$container_path" "$host_path"
|
||||
log_success "Copied $container_path → $host_path"
|
||||
}
|
||||
|
||||
# Discovers all Docker resources associated with the current workspace.
|
||||
# Sets global variables: CONTAINER_ID, CONTAINER_STATUS, VOLUMES (array), IMAGE, IMAGE_UID
|
||||
discover_resources() {
|
||||
local workspace_folder="$1"
|
||||
local label="devcontainer.local_folder=$workspace_folder"
|
||||
|
||||
CONTAINER_ID=""
|
||||
CONTAINER_STATUS=""
|
||||
VOLUMES=()
|
||||
IMAGE=""
|
||||
IMAGE_UID=""
|
||||
|
||||
# Find container (any state: running, stopped, created, etc.)
|
||||
CONTAINER_ID=$(docker ps -aq --filter "label=$label" 2>/dev/null | head -1)
|
||||
|
||||
if [[ -z "$CONTAINER_ID" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Get container status
|
||||
CONTAINER_STATUS=$(docker inspect "$CONTAINER_ID" --format '{{.State.Status}}' 2>/dev/null || true)
|
||||
|
||||
# Get volumes (docker volumes only, not bind mounts)
|
||||
while IFS= read -r vol; do
|
||||
[[ -n "$vol" ]] && VOLUMES+=("$vol")
|
||||
done < <(docker inspect "$CONTAINER_ID" --format '{{json .Mounts}}' 2>/dev/null |
|
||||
jq -r '.[] | select(.Type == "volume") | .Name' 2>/dev/null)
|
||||
|
||||
# Get image and its -uid variant
|
||||
IMAGE=$(docker inspect "$CONTAINER_ID" --format '{{.Config.Image}}' 2>/dev/null || true)
|
||||
if [[ -n "$IMAGE" ]]; then
|
||||
if [[ "$IMAGE" == *-uid ]]; then
|
||||
IMAGE_UID="$IMAGE"
|
||||
IMAGE="${IMAGE%-uid}"
|
||||
else
|
||||
IMAGE_UID="${IMAGE}-uid"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
print_destroy_summary() {
|
||||
echo ""
|
||||
log_warn "The following resources will be permanently removed:"
|
||||
echo ""
|
||||
|
||||
if [[ -n "$CONTAINER_ID" ]]; then
|
||||
local container_name
|
||||
container_name=$(docker inspect "$CONTAINER_ID" --format '{{.Name}}' 2>/dev/null | sed 's|^/||')
|
||||
echo " Container: ${container_name:-$CONTAINER_ID}"
|
||||
if [[ "$CONTAINER_STATUS" == "running" ]]; then
|
||||
echo " (currently running -- will be force-stopped)"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ${#VOLUMES[@]} -gt 0 ]]; then
|
||||
echo " Volumes:"
|
||||
for vol in "${VOLUMES[@]}"; do
|
||||
echo " $vol"
|
||||
done
|
||||
fi
|
||||
|
||||
if [[ -n "$IMAGE" ]]; then
|
||||
echo " Image: $IMAGE"
|
||||
if docker image inspect "$IMAGE_UID" &>/dev/null; then
|
||||
echo " $IMAGE_UID"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
}
|
||||
|
||||
cmd_destroy() {
|
||||
local force=false
|
||||
|
||||
# Parse flags
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-f | --force)
|
||||
force=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
local workspace_folder
|
||||
workspace_folder="$(get_workspace_folder "${1:-}")"
|
||||
|
||||
discover_resources "$workspace_folder"
|
||||
|
||||
# No resources found (idempotent behavior)
|
||||
if [[ -z "$CONTAINER_ID" ]]; then
|
||||
log_info "No devcontainer found for $workspace_folder"
|
||||
return 0
|
||||
fi
|
||||
|
||||
print_destroy_summary
|
||||
|
||||
# Running container warning
|
||||
if [[ "$CONTAINER_STATUS" == "running" && "$force" != true ]]; then
|
||||
log_warn "Container is currently running!"
|
||||
read -p "Force-stop the running container? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
log_info "Aborted."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Main confirmation prompt
|
||||
if [[ "$force" != true ]]; then
|
||||
read -p "Destroy these resources? [y/N] " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
log_info "Aborted."
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Deletion, in order: stop, remove container, volumes, images
|
||||
if [[ -n "$CONTAINER_ID" && "$CONTAINER_STATUS" == "running" ]]; then
|
||||
log_info "Stopping container..."
|
||||
docker stop "$CONTAINER_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [[ -n "$CONTAINER_ID" ]]; then
|
||||
log_info "Removing container..."
|
||||
docker rm -f "$CONTAINER_ID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
for vol in "${VOLUMES[@]}"; do
|
||||
log_info "Removing volume: $vol"
|
||||
docker volume rm -f "$vol" >/dev/null 2>&1 || true
|
||||
done
|
||||
|
||||
if [[ -n "$IMAGE" ]]; then
|
||||
log_info "Removing image: $IMAGE"
|
||||
docker rmi -f "$IMAGE" >/dev/null 2>&1 || true
|
||||
if docker image inspect "$IMAGE_UID" &>/dev/null 2>&1; then
|
||||
log_info "Removing image: $IMAGE_UID"
|
||||
docker rmi -f "$IMAGE_UID" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
log_success "All resources destroyed for $workspace_folder"
|
||||
}
|
||||
|
||||
cmd_self_install() {
|
||||
local install_dir="$HOME/.local/bin"
|
||||
local install_path="$install_dir/devc"
|
||||
@@ -406,6 +818,9 @@ main() {
|
||||
down)
|
||||
cmd_down "$@"
|
||||
;;
|
||||
destroy)
|
||||
cmd_destroy "$@"
|
||||
;;
|
||||
shell)
|
||||
cmd_shell
|
||||
;;
|
||||
@@ -419,6 +834,12 @@ main() {
|
||||
mount)
|
||||
cmd_mount "$@"
|
||||
;;
|
||||
sync)
|
||||
cmd_sync "$@"
|
||||
;;
|
||||
cp)
|
||||
cmd_cp "$@"
|
||||
;;
|
||||
self-install)
|
||||
cmd_self_install
|
||||
;;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"""Post-install configuration for Claude Code devcontainer.
|
||||
|
||||
Runs on container creation to set up:
|
||||
- Onboarding bypass (when CLAUDE_CODE_OAUTH_TOKEN is set)
|
||||
- Claude settings (bypassPermissions mode)
|
||||
- Tmux configuration (200k history, mouse support)
|
||||
- Directory ownership fixes for mounted volumes
|
||||
@@ -15,9 +16,83 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def setup_onboarding_bypass():
|
||||
"""Bypass the interactive onboarding wizard when CLAUDE_CODE_OAUTH_TOKEN is set.
|
||||
|
||||
Runs `claude -p` to seed ~/.claude.json with auth state. The subprocess
|
||||
writes the config file during startup before the API call completes, so
|
||||
a timeout is expected and acceptable. After the subprocess finishes (or
|
||||
times out), we check whether ~/.claude.json was populated and only then
|
||||
set hasCompletedOnboarding.
|
||||
|
||||
Workaround for https://github.com/anthropics/claude-code/issues/8938.
|
||||
"""
|
||||
token = os.environ.get("CLAUDE_CODE_OAUTH_TOKEN", "").strip()
|
||||
if not token:
|
||||
print(
|
||||
"[post_install] No CLAUDE_CODE_OAUTH_TOKEN set, skipping onboarding bypass",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
|
||||
# When `CLAUDE_CONFIG_DIR` is set, as is done in `devcontainer.json`,
|
||||
# `claude` unexpectedly looks for `.claude.json` in *that* folder,
|
||||
# instead of in `~`, contradicting the documentation.
|
||||
# See https://github.com/anthropics/claude-code/issues/3833#issuecomment-3694918874
|
||||
claude_json_dir = Path(os.environ.get("CLAUDE_CONFIG_DIR", Path.home()))
|
||||
claude_json = claude_json_dir / ".claude.json"
|
||||
|
||||
print("[post_install] Running claude -p to populate auth state...", file=sys.stderr)
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["claude", "-p", "ok"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
print(
|
||||
f"[post_install] claude -p exited {result.returncode}: {result.stderr.strip()}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
print(
|
||||
"[post_install] claude -p timed out (expected on cold start)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
except (FileNotFoundError, OSError) as e:
|
||||
print(
|
||||
f"[post_install] Warning: could not run claude ({e}) — onboarding bypass skipped",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
|
||||
if not claude_json.exists():
|
||||
print(
|
||||
f"[post_install] Warning: {claude_json} not created by claude -p — "
|
||||
"onboarding bypass skipped",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
|
||||
config: dict = {}
|
||||
try:
|
||||
config = json.loads(claude_json.read_text())
|
||||
except json.JSONDecodeError as e:
|
||||
print(
|
||||
f"[post_install] Warning: {claude_json} has invalid JSON ({e}), starting fresh",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
config["hasCompletedOnboarding"] = True
|
||||
|
||||
claude_json.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
||||
print(f"[post_install] Onboarding bypass configured: {claude_json}", file=sys.stderr)
|
||||
|
||||
|
||||
def setup_claude_settings():
|
||||
"""Configure Claude Code with bypassPermissions enabled."""
|
||||
claude_dir = Path.home() / ".claude"
|
||||
claude_dir = Path(os.environ.get("CLAUDE_CONFIG_DIR", Path.home() / ".claude"))
|
||||
claude_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
settings_file = claude_dir / "settings.json"
|
||||
@@ -199,6 +274,9 @@ node_modules/
|
||||
|
||||
[diff]
|
||||
colorMoved = default
|
||||
|
||||
[gpg "ssh"]
|
||||
program = /usr/bin/ssh-keygen
|
||||
"""
|
||||
local_gitconfig.write_text(local_config, encoding="utf-8")
|
||||
print(f"[post_install] Local git config created: {local_gitconfig}", file=sys.stderr)
|
||||
@@ -208,6 +286,7 @@ def main():
|
||||
"""Run all post-install configuration."""
|
||||
print("[post_install] Starting post-install configuration...", file=sys.stderr)
|
||||
|
||||
setup_onboarding_bypass()
|
||||
setup_claude_settings()
|
||||
setup_tmux_config()
|
||||
fix_directory_ownership()
|
||||
|
||||
Reference in New Issue
Block a user