From 82fff6a7399ab2cb02158dec2672a1a17a751d2a Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Sun, 1 Mar 2026 20:05:11 +0700 Subject: [PATCH 01/10] chore: integrate claude into devcontainer --- .devcontainer/.claude-config-resolved | 1 + .devcontainer/Dockerfile | 101 ++++++++++------ .devcontainer/README.md | 164 ++++++++++++++++++++++++++ .devcontainer/devcontainer.json | 96 ++++++++++++++- .devcontainer/init-firewall.sh | 134 +++++++++++++++++++++ .devcontainer/init-host.sh | 49 ++++++++ .devcontainer/post-create.sh | 86 ++++++++++++++ .gitignore | 4 + README.md | 23 +++- 9 files changed, 622 insertions(+), 36 deletions(-) create mode 120000 .devcontainer/.claude-config-resolved create mode 100644 .devcontainer/README.md create mode 100755 .devcontainer/init-firewall.sh create mode 100755 .devcontainer/init-host.sh create mode 100755 .devcontainer/post-create.sh diff --git a/.devcontainer/.claude-config-resolved b/.devcontainer/.claude-config-resolved new file mode 120000 index 00000000000..11300fd7e21 --- /dev/null +++ b/.devcontainer/.claude-config-resolved @@ -0,0 +1 @@ +/Users/ivanshumkov/.claude \ No newline at end of file diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 3d82e9f34ae..7e63c7995b7 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,8 +1,11 @@ -# Use the official VS Code base image for dev containers +# Dash Platform Dev Container with Claude Code sandbox support FROM mcr.microsoft.com/devcontainers/base:ubuntu -# Install dependencies -RUN apt-get update && apt-get install -y \ +ARG TZ=UTC +ENV TZ="$TZ" + +# System dependencies for Rust, RocksDB, and protobuf +RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ libssl-dev \ pkg-config \ @@ -13,58 +16,90 @@ RUN apt-get update && apt-get install -y \ gnupg \ lsb-release \ software-properties-common \ - unzip + unzip \ + libsnappy-dev \ + # Developer tools + less \ + procps \ + fzf \ + man-db \ + ripgrep \ + fd-find \ + nano \ + vim \ + && apt-get clean && rm -rf /var/lib/apt/lists/* -# Switch to clang -RUN rm /usr/bin/cc && ln -s /usr/bin/clang /usr/bin/cc +# Use clang as default C compiler +RUN rm -f /usr/bin/cc && ln -s /usr/bin/clang /usr/bin/cc -# Install protoc - protobuf compiler (pin to 32.0) -# Alpine/system protoc may be outdated; install from releases +# Install protoc (pinned to 32.0) ARG TARGETARCH ARG PROTOC_VERSION=32.0 -RUN if [[ "$TARGETARCH" == "arm64" ]] ; then export PROTOC_ARCH=aarch_64; else export PROTOC_ARCH=x86_64; fi; \ - curl -Ls https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip \ +RUN if [ "$TARGETARCH" = "arm64" ]; then export PROTOC_ARCH=aarch_64; else export PROTOC_ARCH=x86_64; fi; \ + curl -Ls "https://github.com/protocolbuffers/protobuf/releases/download/v${PROTOC_VERSION}/protoc-${PROTOC_VERSION}-linux-${PROTOC_ARCH}.zip" \ -o /tmp/protoc.zip && \ unzip -qd /opt/protoc /tmp/protoc.zip && \ rm /tmp/protoc.zip && \ ln -s /opt/protoc/bin/protoc /usr/bin/ -# Remove duplicate install; single install above is sufficient +# Install git-delta for better diffs +ARG GIT_DELTA_VERSION=0.18.2 +RUN ARCH=$(dpkg --print-architecture) && \ + curl -L "https://github.com/dandavison/delta/releases/download/${GIT_DELTA_VERSION}/git-delta_${GIT_DELTA_VERSION}_${ARCH}.deb" \ + -o /tmp/git-delta.deb && \ + dpkg -i /tmp/git-delta.deb && \ + rm /tmp/git-delta.deb + +# Persist bash/zsh history +RUN mkdir -p /commandhistory && \ + touch /commandhistory/.bash_history /commandhistory/.zsh_history && \ + chown -R vscode:vscode /commandhistory -# Switch to vscode user +# Switch to vscode user for Rust and cargo tools USER vscode ENV CARGO_HOME=/home/vscode/.cargo ENV PATH=$CARGO_HOME/bin:$PATH -# TODO: It doesn't sharing PATH between stages, so we need "source $HOME/.cargo/env" everywhere -COPY rust-toolchain.toml . -RUN TOOLCHAIN_VERSION="$(grep channel rust-toolchain.toml | awk '{print $3}' | tr -d '"')" && \ +# Install Rust toolchain from rust-toolchain.toml +COPY --chown=vscode:vscode rust-toolchain.toml /tmp/rust-toolchain.toml +RUN TOOLCHAIN_VERSION="$(grep channel /tmp/rust-toolchain.toml | awk '{print $3}' | tr -d '"')" && \ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- \ -y \ --default-toolchain "${TOOLCHAIN_VERSION}" \ - --target wasm32-unknown-unknown + --target wasm32-unknown-unknown && \ + rm /tmp/rust-toolchain.toml -# Download and install cargo-binstall -ENV BINSTALL_VERSION=1.10.11 +# Install cargo-binstall for fast binary installs +ARG BINSTALL_VERSION=1.10.11 RUN set -ex; \ - if [ "$TARGETARCH" = "amd64" ]; then \ + if [ "$(uname -m)" = "x86_64" ]; then \ CARGO_BINSTALL_ARCH="x86_64-unknown-linux-musl"; \ - elif [ "$TARGETARCH" = "arm64" ] || [ "$TARGETARCH" = "aarch64" ]; then \ + elif [ "$(uname -m)" = "aarch64" ]; then \ CARGO_BINSTALL_ARCH="aarch64-unknown-linux-musl"; \ else \ - echo "Unsupported architecture: $TARGETARCH"; exit 1; \ + echo "Unsupported architecture: $(uname -m)"; exit 1; \ fi; \ - DOWNLOAD_URL="https://github.com/cargo-bins/cargo-binstall/releases/download/v${BINSTALL_VERSION}/cargo-binstall-${CARGO_BINSTALL_ARCH}.tgz"; \ - curl -L --fail --show-error "$DOWNLOAD_URL" -o /tmp/cargo-binstall.tgz; \ - tar -xzf /tmp/cargo-binstall.tgz -C /tmp cargo-binstall; \ - chmod +x /tmp/cargo-binstall; \ - /tmp/cargo-binstall -y --force cargo-binstall; \ - rm /tmp/cargo-binstall; \ - cargo binstall -V + curl -L --fail --show-error \ + "https://github.com/cargo-bins/cargo-binstall/releases/download/v${BINSTALL_VERSION}/cargo-binstall-${CARGO_BINSTALL_ARCH}.tgz" \ + -o /tmp/cargo-binstall.tgz && \ + tar -xzf /tmp/cargo-binstall.tgz -C /tmp cargo-binstall && \ + chmod +x /tmp/cargo-binstall && \ + /tmp/cargo-binstall -y --force cargo-binstall && \ + rm /tmp/cargo-binstall.tgz /tmp/cargo-binstall + +# Install wasm tools +RUN cargo binstall wasm-bindgen-cli@0.2.108 --locked \ + --no-discover-github-token --disable-telemetry --no-track --no-confirm && \ + cargo binstall wasm-pack --locked \ + --no-discover-github-token --disable-telemetry --no-track --no-confirm + +# Prepare directories (need root for /workspace) +USER root +RUN mkdir -p /home/vscode/.claude /workspace && \ + chown -R vscode:vscode /home/vscode/.claude /workspace +USER vscode -RUN cargo binstall wasm-bindgen-cli@0.2.103 --locked \ - --no-discover-github-token \ - --disable-telemetry \ - --no-track \ - --no-confirm +ENV SHELL=/bin/zsh +ENV EDITOR=nano +ENV VISUAL=nano diff --git a/.devcontainer/README.md b/.devcontainer/README.md new file mode 100644 index 00000000000..6665110e5f7 --- /dev/null +++ b/.devcontainer/README.md @@ -0,0 +1,164 @@ +# Dev Container + +Sandboxed development environment for Dash Platform with Claude Code pre-configured for autonomous work. + +## What's Included + +- **Rust 1.92** with `wasm32-unknown-unknown` target +- **Node.js 24** with yarn 4.12.0 (via corepack) +- **Docker-in-Docker** for dashmate +- **Claude Code** with `bypassPermissions` mode +- protoc 32.0, wasm-bindgen-cli 0.2.108, wasm-pack, cargo-binstall +- Developer tools: git-delta, ripgrep, fd, fzf + +## Prerequisites + +### SSH keys (for git push/pull) + +VS Code forwards your host's SSH agent into the container automatically. Make sure your key is loaded: + +```bash +ssh-add --apple-use-keychain ~/.ssh/id_rsa # macOS +ssh-add ~/.ssh/id_rsa # Linux +``` + +Without this, `git push`/`git pull` will fail with `Permission denied (publickey)`. + +### Claude Code authentication + +Authenticate using **one or both** methods. OAuth login must be done on the **host** — it does not work inside the container (the OAuth callback can't reach localhost in the container). + +### Option A: OAuth (recommended) + +Run on your **host machine** before opening the devcontainer: + +```bash +claude login +``` + +Your `~/.claude/` config (credentials, skills, plugins) is automatically copied into the container on each rebuild. If tokens expire, re-run `claude login` on the host and rebuild. + +### Option B: API Key + +```bash +export ANTHROPIC_API_KEY=sk-ant-... +``` + +Set this in your shell profile so it's available when VS Code launches. + +## Usage with VS Code + +1. Install the [Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension +2. Open this repository in VS Code +3. Press `Ctrl+Shift+P` (or `Cmd+Shift+P` on macOS) and select **Dev Containers: Reopen in Container** +4. Wait for the build (first time takes a while — Rust toolchain, etc.) +5. Claude Code is ready in the integrated terminal: + + ```bash + claude # runs with full permissions, no prompts + ``` + +### Personal extensions + +The `devcontainer.json` includes shared team extensions (rust-analyzer, eslint, Claude Code, etc.). To add your own extensions to every dev container, set this in your **host** VS Code settings (`Cmd+,` → search "defaultExtensions"): + +```json +{ + "dev.containers.defaultExtensions": [ + "github.copilot", + "vscodevim.vim" + ] +} +``` + +## Usage with CLI (no VS Code) + +You can use the [devcontainer CLI](https://github.com/devcontainers/cli) directly: + +```bash +# Install the CLI +npm install -g @devcontainers/cli + +# Build the container +devcontainer build --workspace-folder . + +# Start and enter the container +devcontainer up --workspace-folder . +devcontainer exec --workspace-folder . bash + +# Run Claude Code directly +devcontainer exec --workspace-folder . claude --dangerously-skip-permissions +``` + +Or use Docker Compose / `docker exec` if you prefer: + +```bash +# Build +devcontainer build --workspace-folder . + +# Start in background +devcontainer up --workspace-folder . + +# Run Claude in headless mode (for CI/automation) +devcontainer exec --workspace-folder . claude -p "run the test suite" --dangerously-skip-permissions +``` + +## Authentication Details + +Your host's `~/.claude/` directory is mounted read-only into the container. On first create, the `post-create.sh` script: + +1. Copies your entire `~/.claude/` config (credentials, skills, plugins, etc.) into a persistent Docker volume +2. Forces `bypassPermissions` mode on top of your settings +3. Skips the safety confirmation prompt + +Host config items that reference host-specific paths (MCP servers, hooks, etc.) are copied as-is. They will log warnings if the referenced binaries don't exist in the container — this is harmless. + +## Network Firewall (optional) + +By default, the container has unrestricted network access. To enable a restrictive firewall that only allows whitelisted services, add the following to `devcontainer.json`: + +```jsonc +"runArgs": ["--cap-add=NET_ADMIN", "--cap-add=NET_RAW"], +"postStartCommand": "sudo /usr/local/bin/init-firewall.sh", +"waitFor": "postStartCommand" +``` + +You'll also need to add `iptables ipset iproute2 dnsutils` to the `apt-get install` in the Dockerfile and uncomment the firewall COPY/sudoers block. See `init-firewall.sh` for the domain whitelist. + +## Persistent Data + +These items survive container rebuilds (stored in Docker named volumes): + +- `~/.cargo/registry` and `~/.cargo/git` — Rust dependency cache +- `target/` — Rust build artifacts +- `~/.claude/` — Claude Code config, credentials, conversation history +- `/commandhistory/` — shell history + +## Troubleshooting + +### Git worktrees + +Git worktrees are supported automatically. The `init-host.sh` script (runs on the host) detects whether you opened a worktree or the main repo and mounts the main `.git` directory into the container. The `post-create.sh` script creates the necessary symlinks so git resolves the worktree paths correctly. Commits and pushes from inside the container work as expected. + +### Claude says "not authenticated" + +- Check that `ANTHROPIC_API_KEY` is set in your host shell, or +- Run `claude login` on your host before opening the devcontainer, or +- Run `claude login` inside the container + +### MCP server warnings at Claude startup + +- Expected if your host config has MCP servers referencing macOS binaries. Harmless — Claude works fine without them. + +### `yarn install` fails + +- Run `corepack enable` first (should be done by `post-create.sh`) + +### Docker commands fail inside the container + +- Docker-in-Docker starts automatically. If it didn't, check `docker info`. + +### Firewall too restrictive (if enabled) + +- Edit `.devcontainer/init-firewall.sh` to add domains +- Or temporarily flush rules: `sudo iptables -F OUTPUT` diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 586571ba2b1..8821a66c08a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,4 +1,98 @@ { "name": "Dash Platform Dev Container", - "image": "ghcr.io/dashpay/platform/devcontainer:0.1.0" + "build": { + "dockerfile": "Dockerfile", + "context": "..", + "args": { + "TZ": "${localEnv:TZ:UTC}", + "GIT_DELTA_VERSION": "0.18.2" + } + }, + "init": true, + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.defaultProfile.linux": "zsh", + "editor.formatOnSave": true + }, + "extensions": [ + "anthropic.claude-code", + "arcanis.vscode-zipfs", + "chrmarti.regex", + "davidanson.vscode-markdownlint", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode", + "vadimcn.vscode-lldb", + "rust-lang.rust-analyzer", + "tamasfe.even-better-toml", + "ms-azuretools.vscode-docker", + "eamodio.gitlens" + ] + } + }, + "remoteUser": "vscode", + "mounts": [ + { + "source": "devcontainer-platform-cargo-registry-${devcontainerId}", + "target": "/home/vscode/.cargo/registry", + "type": "volume" + }, + { + "source": "devcontainer-platform-cargo-git-${devcontainerId}", + "target": "/home/vscode/.cargo/git", + "type": "volume" + }, + { + "source": "devcontainer-platform-target-${devcontainerId}", + "target": "${containerWorkspaceFolder}/target", + "type": "volume" + }, + { + "source": "devcontainer-platform-claude-config-${devcontainerId}", + "target": "/home/vscode/.claude", + "type": "volume" + }, + { + "source": "devcontainer-platform-bashhistory-${devcontainerId}", + "target": "/commandhistory", + "type": "volume" + }, + { + "source": "${localWorkspaceFolder}/.devcontainer/.main-git-resolved", + "target": "/workspace/.host-main-git", + "type": "bind" + } + ], + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "installZsh": "true", + "username": "vscode", + "userUid": "1000", + "userGid": "1000", + "upgradePackages": "true" + }, + "ghcr.io/devcontainers/features/git:1": { + "version": "latest", + "ppa": "false" + }, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/node:1": { + "version": "24" + }, + "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}, + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "dockerDashComposeVersion": "v2" + }, + "ghcr.io/anthropics/devcontainer-features/claude-code:1": {} + }, + "containerEnv": { + "ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}", + "CLAUDE_CONFIG_DIR": "/home/vscode/.claude", + "NODE_OPTIONS": "--max-old-space-size=4096", + "DEVCONTAINER": "true" + }, + "workspaceMount": "source=${localWorkspaceFolder},target=/workspace/platform,type=bind,consistency=delegated", + "workspaceFolder": "/workspace/platform", + "initializeCommand": "bash .devcontainer/init-host.sh", + "postCreateCommand": "bash .devcontainer/post-create.sh" } diff --git a/.devcontainer/init-firewall.sh b/.devcontainer/init-firewall.sh new file mode 100755 index 00000000000..96e12ae0be5 --- /dev/null +++ b/.devcontainer/init-firewall.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# Network firewall for Claude Code devcontainer sandbox. +# Restricts outbound traffic to only necessary services. +# Based on Anthropic's official init-firewall.sh pattern. +set -euo pipefail + +# Skip if not running as root +if [ "$(id -u)" -ne 0 ]; then + echo "ERROR: init-firewall.sh must run as root (use sudo)" + exit 1 +fi + +# Skip if iptables is not available +if ! command -v iptables &>/dev/null; then + echo "WARNING: iptables not found, skipping firewall setup" + exit 0 +fi + +echo "Configuring devcontainer firewall..." + +# Flush existing rules +iptables -F OUTPUT 2>/dev/null || true +ipset destroy allowed_hosts 2>/dev/null || true + +# Create ipset for allowed hosts +ipset create allowed_hosts hash:ip hashsize 4096 + +# --- Resolve and allow domains --- + +# NOTE: DNS resolution is point-in-time. CDN-backed services rotate IPs. +# The ESTABLISHED,RELATED rule helps for long-lived connections. +# This script re-runs on every container start (postStartCommand). +resolve_and_allow() { + local domain="$1" + local ips + ips=$(dig +short "$domain" A 2>/dev/null | grep -E '^[0-9]+\.' || true) + for ip in $ips; do + ipset add allowed_hosts "$ip" 2>/dev/null || true + done +} + +# Claude / Anthropic API +resolve_and_allow "api.anthropic.com" +resolve_and_allow "sentry.io" +resolve_and_allow "statsig.anthropic.com" +resolve_and_allow "statsig.com" +resolve_and_allow "featuregates.org" +resolve_and_allow "prodregistryv2.org" + +# npm registry +resolve_and_allow "registry.npmjs.org" +resolve_and_allow "registry.yarnpkg.com" + +# Rust / crates.io +resolve_and_allow "crates.io" +resolve_and_allow "static.crates.io" +resolve_and_allow "index.crates.io" +resolve_and_allow "static.rust-lang.org" +resolve_and_allow "sh.rustup.rs" + +# GitHub (dynamic IP ranges - added as CIDR rules below since ipset doesn't support /16 etc.) +GITHUB_IPS=$(curl -s https://api.github.com/meta 2>/dev/null | jq -r '.web[], .api[], .git[], .actions[]' 2>/dev/null || true) +resolve_and_allow "github.com" +resolve_and_allow "api.github.com" +resolve_and_allow "raw.githubusercontent.com" +resolve_and_allow "objects.githubusercontent.com" +resolve_and_allow "codeload.github.com" +resolve_and_allow "ghcr.io" + +# VS Code marketplace +resolve_and_allow "marketplace.visualstudio.com" +resolve_and_allow "vscode.blob.core.windows.net" +resolve_and_allow "update.code.visualstudio.com" +resolve_and_allow "az764295.vo.msecnd.net" + +# Protobuf releases (via GitHub, already covered) + +# Docker Hub (for dashmate Docker-in-Docker) +resolve_and_allow "registry-1.docker.io" +resolve_and_allow "auth.docker.io" +resolve_and_allow "production.cloudflare.docker.com" + +# Dash-specific +resolve_and_allow "testnet.platform-explorer.com" + +# --- Apply iptables rules --- + +# Allow loopback +iptables -A OUTPUT -o lo -j ACCEPT + +# Allow established/related connections +iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT + +# Allow DNS (UDP/TCP 53) +iptables -A OUTPUT -p udp --dport 53 -j ACCEPT +iptables -A OUTPUT -p tcp --dport 53 -j ACCEPT + +# Allow SSH +iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT + +# Allow private networks: Docker-in-Docker (172.x), dashmate local nodes, host services. +# Broad ranges are intentional — dashmate orchestrates multiple containers on Docker networks. +iptables -A OUTPUT -d 172.16.0.0/12 -j ACCEPT +iptables -A OUTPUT -d 10.0.0.0/8 -j ACCEPT +iptables -A OUTPUT -d 192.168.0.0/16 -j ACCEPT + +# Allow resolved hosts +iptables -A OUTPUT -m set --match-set allowed_hosts dst -j ACCEPT + +# Allow GitHub CIDR ranges directly +for cidr in $GITHUB_IPS; do + iptables -A OUTPUT -d "$cidr" -j ACCEPT 2>/dev/null || true +done + +# Default deny all other outbound +iptables -A OUTPUT -j REJECT --reject-with icmp-port-unreachable + +echo "Firewall configured. Verifying..." + +# Verify: allowed domain should work +if curl -sf --max-time 5 -o /dev/null "https://api.github.com" 2>/dev/null; then + echo " [OK] api.github.com is reachable" +else + echo " [WARN] api.github.com is not reachable - firewall may be too restrictive" +fi + +# Verify: blocked domain should fail +if curl -sf --max-time 3 -o /dev/null "https://example.com" 2>/dev/null; then + echo " [WARN] example.com is reachable - firewall may not be working" +else + echo " [OK] example.com is blocked" +fi + +echo "Firewall setup complete." diff --git a/.devcontainer/init-host.sh b/.devcontainer/init-host.sh new file mode 100755 index 00000000000..4ff8cfb24ec --- /dev/null +++ b/.devcontainer/init-host.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Runs on the HOST before container creation. +# Resolves git worktree paths so git works inside the container. +set -euo pipefail + +test -f "$HOME/.gitconfig" || touch "$HOME/.gitconfig" +mkdir -p "$HOME/.claude" + +# Copy Claude config into the workspace so post-create.sh can access it. +# Bind mounts of ~/.claude fail on some Docker Desktop setups (virtiofs issues). +# The workspace mount is reliable, so we copy here and clean up in post-create. +CLAUDE_STAGING=".devcontainer/.claude-host-config" +rm -rf "$CLAUDE_STAGING" +if [ -d "$HOME/.claude" ] && [ "$(ls -A "$HOME/.claude" 2>/dev/null)" ]; then + mkdir -p "$CLAUDE_STAGING" + cp -a "$HOME/.claude"/. "$CLAUDE_STAGING"/ 2>/dev/null || true + # Also copy dotfiles + for item in "$HOME/.claude"/.*; do + base="$(basename "$item")" + [ "$base" = "." ] || [ "$base" = ".." ] && continue + cp -a "$item" "$CLAUDE_STAGING/$base" 2>/dev/null || true + done + # Also copy ~/.claude.json (onboarding state, outside ~/.claude/) + [ -f "$HOME/.claude.json" ] && cp -a "$HOME/.claude.json" "$CLAUDE_STAGING/.claude.json.root" 2>/dev/null || true +fi + +# Resolve main .git directory for worktree support. +# Docker follows symlinks in bind mount sources, so we create a symlink +# at a known path that always points to the real .git directory. +# This way the same devcontainer.json works for both worktrees and main repo. +RESOLVED=".devcontainer/.main-git-resolved" + +if [ -f .git ]; then + # Worktree: .git file contains "gitdir: /path/to/main/.git/worktrees/name" + GITDIR=$(sed 's/gitdir: //' .git) + # Strip /worktrees/name to get the main .git directory + MAIN_GIT="${GITDIR%/worktrees/*}" + if [ -d "$MAIN_GIT" ]; then + ln -sfn "$MAIN_GIT" "$RESOLVED" + else + mkdir -p "$RESOLVED" + fi +elif [ -d .git ]; then + # Main repo: just point to our own .git + ln -sfn "$(pwd)/.git" "$RESOLVED" +else + # No git at all — empty dir so the mount doesn't fail + mkdir -p "$RESOLVED" +fi diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh new file mode 100755 index 00000000000..7ba4aa43366 --- /dev/null +++ b/.devcontainer/post-create.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Post-create setup for Dash Platform devcontainer with Claude Code. +# Runs once after the container is created. +set -euo pipefail + +WORKSPACE="/workspace/platform" + +echo "=== Dash Platform devcontainer post-create setup ===" + +# --- Git worktree support --- +# Git worktrees use a .git FILE pointing to an absolute host path like +# /Users/you/.../platform/.git/worktrees/v3. That path doesn't exist inside +# the container. init-host.sh mounted the main .git at /workspace/.host-main-git. +# We create a symlink from the absolute host path so git can follow it. +if [ -f "$WORKSPACE/.git" ] && [ -d "/workspace/.host-main-git" ]; then + GITDIR=$(sed 's/gitdir: //' "$WORKSPACE/.git") + if [ ! -d "$GITDIR" ]; then + # e.g. /Users/you/Projects/dashpay/platform/.git + MAIN_GIT_HOST_PATH="$(dirname "$(dirname "$GITDIR")")" + sudo mkdir -p "$(dirname "$MAIN_GIT_HOST_PATH")" + sudo ln -sfn /workspace/.host-main-git "$MAIN_GIT_HOST_PATH" + echo "Git worktree: linked $MAIN_GIT_HOST_PATH -> /workspace/.host-main-git" + fi +fi + +# --- Git configuration --- +git config --global --add safe.directory "$WORKSPACE" + +# --- Cargo permissions --- +sudo chown -R vscode:vscode /home/vscode/.cargo "$WORKSPACE/target" 2>/dev/null || true + +# --- Enable corepack for yarn --- +corepack enable 2>/dev/null || true + +# --- Claude Code: copy config staged by init-host.sh, then override for sandbox --- +# init-host.sh copies ~/.claude into .devcontainer/.claude-host-config/ on the host. +# The workspace bind mount makes it available here. We copy into the persistent +# volume, then clean up the staging copy (it contains credentials). +CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-/home/vscode/.claude}" +HOST_CONFIG="$WORKSPACE/.devcontainer/.claude-host-config" + +if [ -d "$HOST_CONFIG" ] && [ "$(ls -A "$HOST_CONFIG" 2>/dev/null)" ]; then + echo "Copying Claude config from host..." + cp -a "$HOST_CONFIG"/. "$CLAUDE_DIR"/ 2>/dev/null || true + for item in "$HOST_CONFIG"/.*; do + basename="$(basename "$item")" + [ "$basename" = "." ] || [ "$basename" = ".." ] && continue + cp -a "$item" "$CLAUDE_DIR/$basename" 2>/dev/null || true + done + chmod 600 "$CLAUDE_DIR/.credentials.json" 2>/dev/null || true + # Place ~/.claude.json (onboarding state) at the correct path + if [ -f "$HOST_CONFIG/.claude.json.root" ]; then + cp -a "$HOST_CONFIG/.claude.json.root" /home/vscode/.claude.json + chown vscode:vscode /home/vscode/.claude.json + fi + # Clean up staged credentials from the workspace + rm -rf "$HOST_CONFIG" + echo "Host Claude config copied." +else + mkdir -p "$CLAUDE_DIR" + echo "No host Claude config found. Use ANTHROPIC_API_KEY or 'claude login'." +fi + +chown -R vscode:vscode "$CLAUDE_DIR" + +# Force bypassPermissions on top of whatever settings came from host +SETTINGS_FILE="$CLAUDE_DIR/settings.json" +if [ -f "$SETTINGS_FILE" ]; then + TMP=$(mktemp) + jq '.permissions.defaultMode = "bypassPermissions" | .skipDangerousModePermissionPrompt = true' \ + "$SETTINGS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$SETTINGS_FILE" || \ + echo '{"permissions":{"defaultMode":"bypassPermissions"},"skipDangerousModePermissionPrompt":true}' > "$SETTINGS_FILE" +else + echo '{"permissions":{"defaultMode":"bypassPermissions"},"skipDangerousModePermissionPrompt":true}' > "$SETTINGS_FILE" +fi +chown vscode:vscode "$SETTINGS_FILE" + +# --- Shell history (idempotent) --- +grep -q 'HISTFILE=/commandhistory/.zsh_history' /home/vscode/.zshrc 2>/dev/null || \ + echo 'export HISTFILE=/commandhistory/.zsh_history' >> /home/vscode/.zshrc +grep -q 'HISTFILE=/commandhistory/.bash_history' /home/vscode/.bashrc 2>/dev/null || \ + echo 'export HISTFILE=/commandhistory/.bash_history' >> /home/vscode/.bashrc + +echo "=== Post-create setup complete ===" +echo "Claude Code is configured with bypassPermissions mode." +echo "Set ANTHROPIC_API_KEY in your host environment before opening this devcontainer." diff --git a/.gitignore b/.gitignore index 897406169e4..eeebcc78224 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,10 @@ # ignore VSCode project specific files .vscode +# Devcontainer host-resolved symlink (machine-specific) +.devcontainer/.main-git-resolved +.devcontainer/.claude-host-config + # Env file .env diff --git a/README.md b/README.md index 62be4cc3d0c..6031fa70064 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,28 @@ this repository may be used on the following networks: ### How to build and set up a node from the code in this repo? +#### Using Dev Container (recommended) + +The easiest way to get started is with a [Dev Container](.devcontainer/README.md) +which provides a pre-configured environment with all dependencies and +[Claude Code](https://claude.ai/code) sandboxed for autonomous development. + +Open this repo in VS Code and select **Dev Containers: Reopen in Container**, or +use the CLI: + +```bash +npm install -g @devcontainers/cli +devcontainer up --workspace-folder . +devcontainer exec --workspace-folder . bash +``` + +See [.devcontainer/README.md](.devcontainer/README.md) for full details. + +#### Manual setup + - Clone the repo - Install prerequisites: - - [node.js](https://nodejs.org/) v20 + - [node.js](https://nodejs.org/) v24 - [docker](https://docs.docker.com/get-docker/) v20.10+ - [rust](https://www.rust-lang.org/tools/install) v1.92+, with wasm32 target (`rustup target add wasm32-unknown-unknown`) - [protoc - protobuf compiler](https://github.com/protocolbuffers/protobuf/releases) v32.0+ @@ -60,7 +79,7 @@ this repository may be used on the following networks: in terminal run `echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.zshrc` or `echo 'export PATH="/opt/homebrew/opt/llvm/bin:$PATH"' >> ~/.bash_profile` depending on your default shell. You can find your default shell with `echo $SHELL` - Reload your shell with `source ~/.zshrc` or `source ~/.bash_profile` - - `cargo install wasm-bindgen-cli@0.2.103` + - `cargo install wasm-bindgen-cli@0.2.108` - *double-check that wasm-bindgen-cli version above matches wasm-bindgen version in Cargo.lock file* - *Depending on system, additional packages may need to be installed as a prerequisite for wasm-bindgen-cli. If anything is missing, installation will error and prompt what packages are missing (i.e. clang, llvm, libssl-dev)* - essential build tools - example for Debian/Ubuntu: `apt install -y build-essential libssl-dev pkg-config clang cmake llvm` From 044c89d7c1158e8445fb84fc4110558d219c674c Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Mon, 2 Mar 2026 18:12:54 +0700 Subject: [PATCH 02/10] feat: cleanup --- .devcontainer/devcontainer-build.json | 73 -------------------- .devcontainer/devcontainer.json | 3 +- .github/workflows/prebuild-devcontainers.yml | 60 ---------------- 3 files changed, 2 insertions(+), 134 deletions(-) delete mode 100644 .devcontainer/devcontainer-build.json delete mode 100644 .github/workflows/prebuild-devcontainers.yml diff --git a/.devcontainer/devcontainer-build.json b/.devcontainer/devcontainer-build.json deleted file mode 100644 index df88d659340..00000000000 --- a/.devcontainer/devcontainer-build.json +++ /dev/null @@ -1,73 +0,0 @@ -{ - "name": "Dash Platform Dev Container", - "build": { - "dockerfile": "Dockerfile", - "context": ".." - }, - "customizations": { - "vscode": { - "settings": {}, - "extensions": [ - "arcanis.vscode-zipfs", - "chrmarti.regex", - "davidanson.vscode-markdownlint", - "ms-vscode.cmake-tools", - "dbaeumer.vscode-eslint", - "esbenp.prettier-vscode", - "vadimcn.vscode-lldb", - "rust-lang.rust-analyzer", - "tamasfe.even-better-toml", - "zhangyue.rust-mod-generator", - "ms-azuretools.vscode-docker" - ] - } - }, - "remoteUser": "vscode", - "mounts": [ - { - "source": "devcontainer-platform-cargo-registry-index-${devcontainerId}", - "target": "/home/vscode/.cargo/registry", - "type": "volume" - }, - { - "source": "devcontainer-platform-cargo-registry-cache-${devcontainerId}", - "target": "/home/vscode/.cargo/registry/cache", - "type": "volume" - }, - { - "source": "devcontainer-platform-cargo-git-db-${devcontainerId}", - "target": "/home/vscode/.cargo/git/db", - "type": "volume" - }, - { - "source": "devcontainer-platform-target-${devcontainerId}", - "target": "${containerWorkspaceFolder}/target", - "type": "volume" - } - ], - "features": { - "ghcr.io/devcontainers/features/common-utils:2": { - "installZsh": "true", - "username": "vscode", - "userUid": "1000", - "userGid": "1000", - "upgradePackages": "true" - }, - "ghcr.io/devcontainers/features/git:1": { - "version": "latest", - "ppa": "false" - }, - "ghcr.io/devcontainers/features/github-cli:1": {}, - "ghcr.io/devcontainers/features/node:1": { - "version": 20, - "installYarnUsingApt": false - }, - "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {}, - "ghcr.io/devcontainers/features/docker-in-docker:2": {}, - "ghcr.io/schlich/devcontainer-features/starship:0": {}, - }, - "postCreateCommand": { - "git-safe": "git config --global --add safe.directory ${containerWorkspaceFolder}", - "cargo-permissions": "sudo chown -R vscode:vscode /home/vscode/.cargo ${containerWorkspaceFolder}/target" - } -} diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 8821a66c08a..1e98f040a92 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -83,7 +83,8 @@ "ghcr.io/devcontainers/features/docker-in-docker:2": { "dockerDashComposeVersion": "v2" }, - "ghcr.io/anthropics/devcontainer-features/claude-code:1": {} + "ghcr.io/anthropics/devcontainer-features/claude-code:1": {}, + "ghcr.io/schlich/devcontainer-features/starship:0": {} }, "containerEnv": { "ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}", diff --git a/.github/workflows/prebuild-devcontainers.yml b/.github/workflows/prebuild-devcontainers.yml deleted file mode 100644 index dcf2dd5a49f..00000000000 --- a/.github/workflows/prebuild-devcontainers.yml +++ /dev/null @@ -1,60 +0,0 @@ -name: Prebuild Dev Containers - -on: - push: - paths: - - '.devcontainer/**' - - '.github/workflows/prebuild-devcontainers.yml' - - rust-toolchain.toml - - Dockerfile - branches: - - master - workflow_dispatch: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - name: Build and push devcontainer - runs-on: ubuntu-24.04 - timeout-minutes: 240 - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Setup Node.JS - uses: actions/setup-node@v4 - with: - node-version: "24" - - - name: Install skopeo - run: | - sudo apt-get update - sudo apt-get install -y skopeo - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Setup Docker buildx - uses: docker/setup-buildx-action@v3 - with: - use: true - - - name: Login to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: dashpay - password: ${{ secrets.GHCR_TOKEN }} - - - name: Build and push Platform devcontainer - uses: devcontainers/ci@v0.3 - with: - imageName: ghcr.io/dashpay/platform/devcontainer - imageTag: 0.1.0 - platform: linux/amd64,linux/arm64 - configFile: .devcontainer/devcontainer-build.json - push: always - cacheFrom: ghcr.io/dashpay/platform/devcontainer From cc56191d0a4e2a993caa6827c74bb25fe99e074b Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 4 Mar 2026 22:06:22 +0700 Subject: [PATCH 03/10] chore(devcontainer): sync host plugins, agents, and skills into container Copy enabledPlugins from host settings.json automatically (just IDs, no secrets). Add .env/.env.example config for users to list specific agents and skills to copy from ~/.claude/. Harden init-host.sh to only stage credentials and explicitly listed extras instead of copying the entire ~/.claude/ directory. Remove unused bash history volume. Co-Authored-By: Claude Opus 4.6 --- .devcontainer/.env.example | 6 +++ .devcontainer/.gitignore | 3 ++ .devcontainer/README.md | 56 ++++++++++++++++------- .devcontainer/devcontainer.json | 5 --- .devcontainer/init-host.sh | 56 +++++++++++++++++------ .devcontainer/post-create.sh | 78 +++++++++++++++++++-------------- 6 files changed, 138 insertions(+), 66 deletions(-) create mode 100644 .devcontainer/.env.example create mode 100644 .devcontainer/.gitignore diff --git a/.devcontainer/.env.example b/.devcontainer/.env.example new file mode 100644 index 00000000000..d46c177e4a2 --- /dev/null +++ b/.devcontainer/.env.example @@ -0,0 +1,6 @@ +# Claude Code: agents and skills to copy from ~/.claude/ into the container. +# Copy this file to .env and customize. Plugins are always synced automatically. +# +# Comma-separated names (agents without .md extension, skills as directory names) +CLAUDE_AGENTS= +CLAUDE_SKILLS= diff --git a/.devcontainer/.gitignore b/.devcontainer/.gitignore new file mode 100644 index 00000000000..f1228114391 --- /dev/null +++ b/.devcontainer/.gitignore @@ -0,0 +1,3 @@ +.env +.claude-host-config/ +.main-git-resolved diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 6665110e5f7..72c8322b8ce 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -36,7 +36,7 @@ Run on your **host machine** before opening the devcontainer: claude login ``` -Your `~/.claude/` config (credentials, skills, plugins) is automatically copied into the container on each rebuild. If tokens expire, re-run `claude login` on the host and rebuild. +Your OAuth credentials (`~/.claude/.credentials.json`) and enabled plugins are copied into the container. Optionally, agents and skills listed in `.devcontainer/.env` are also copied. No conversation history, project memories, or host settings are transferred. If tokens expire, re-run `claude login` on the host and rebuild. ### Option B: API Key @@ -103,15 +103,45 @@ devcontainer up --workspace-folder . devcontainer exec --workspace-folder . claude -p "run the test suite" --dangerously-skip-permissions ``` -## Authentication Details +## Claude Code customization -Your host's `~/.claude/` directory is mounted read-only into the container. On first create, the `post-create.sh` script: +### Plugins -1. Copies your entire `~/.claude/` config (credentials, skills, plugins, etc.) into a persistent Docker volume -2. Forces `bypassPermissions` mode on top of your settings -3. Skips the safety confirmation prompt +Enabled plugins from your host `~/.claude/settings.json` are automatically synced into the container. No configuration needed — plugin IDs contain no secrets. -Host config items that reference host-specific paths (MCP servers, hooks, etc.) are copied as-is. They will log warnings if the referenced binaries don't exist in the container — this is harmless. +### Agents & skills + +To copy personal agents or skills from `~/.claude/` into the container, create a `.env` file: + +```bash +cp .devcontainer/.env.example .devcontainer/.env +``` + +Edit `.env` with comma-separated names: + +```bash +# Agents from ~/.claude/agents/ (without .md extension) +CLAUDE_AGENTS=blockchain-security-auditor,rust-engineer + +# Skills from ~/.claude/skills/ (directory names) +CLAUDE_SKILLS=my-custom-skill +``` + +The `.env` file is gitignored — each developer configures their own. + +### Project-level settings + +The project's `.claude/` directory (containing `settings.local.json` and `skills/`) is automatically available inside the container via the workspace bind mount. No extra configuration needed. + +## Security Model + +Claude Code runs with `bypassPermissions` inside the container — it can read, write, and execute anything. The container is the sandbox boundary. To minimize exposure: + +- **Only OAuth credentials** are copied from the host (`~/.claude/.credentials.json`). No conversation history, project memories, settings, hooks, scripts, or debug logs are transferred. +- **Enabled plugins** (just plugin IDs) are synced from host settings. Optionally, listed agents/skills (markdown files only) are copied. +- **A clean `settings.json`** is generated inside the container with `bypassPermissions` and enabled plugins — your host's permission allowlists, MCP server configs, and hooks are not copied. +- **No shell history** is persisted or shared with the container. +- **The `.git` directory** is mounted read-write (required for commits/pushes). This is the main trust boundary — Claude can push code. ## Network Firewall (optional) @@ -131,8 +161,7 @@ These items survive container rebuilds (stored in Docker named volumes): - `~/.cargo/registry` and `~/.cargo/git` — Rust dependency cache - `target/` — Rust build artifacts -- `~/.claude/` — Claude Code config, credentials, conversation history -- `/commandhistory/` — shell history +- `~/.claude/` — Claude Code credentials, settings, and optionally agents/skills from host ## Troubleshooting @@ -142,13 +171,8 @@ Git worktrees are supported automatically. The `init-host.sh` script (runs on th ### Claude says "not authenticated" -- Check that `ANTHROPIC_API_KEY` is set in your host shell, or -- Run `claude login` on your host before opening the devcontainer, or -- Run `claude login` inside the container - -### MCP server warnings at Claude startup - -- Expected if your host config has MCP servers referencing macOS binaries. Harmless — Claude works fine without them. +- Ensure `ANTHROPIC_API_KEY` is set in your host shell, **or** +- Run `claude login` on your host and rebuild the container ### `yarn install` fails diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1e98f040a92..0ce3f0ddf7b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -52,11 +52,6 @@ "target": "/home/vscode/.claude", "type": "volume" }, - { - "source": "devcontainer-platform-bashhistory-${devcontainerId}", - "target": "/commandhistory", - "type": "volume" - }, { "source": "${localWorkspaceFolder}/.devcontainer/.main-git-resolved", "target": "/workspace/.host-main-git", diff --git a/.devcontainer/init-host.sh b/.devcontainer/init-host.sh index 4ff8cfb24ec..63214220c04 100755 --- a/.devcontainer/init-host.sh +++ b/.devcontainer/init-host.sh @@ -6,22 +6,52 @@ set -euo pipefail test -f "$HOME/.gitconfig" || touch "$HOME/.gitconfig" mkdir -p "$HOME/.claude" -# Copy Claude config into the workspace so post-create.sh can access it. -# Bind mounts of ~/.claude fail on some Docker Desktop setups (virtiofs issues). -# The workspace mount is reliable, so we copy here and clean up in post-create. +# Stage ONLY the minimum Claude config needed for authentication. +# We deliberately skip: conversation history, project memories, debug logs, +# shell snapshots, plans, scripts, plugins cache, and other data that could +# leak information from other projects into the sandboxed container. CLAUDE_STAGING=".devcontainer/.claude-host-config" rm -rf "$CLAUDE_STAGING" -if [ -d "$HOME/.claude" ] && [ "$(ls -A "$HOME/.claude" 2>/dev/null)" ]; then +if [ -d "$HOME/.claude" ]; then mkdir -p "$CLAUDE_STAGING" - cp -a "$HOME/.claude"/. "$CLAUDE_STAGING"/ 2>/dev/null || true - # Also copy dotfiles - for item in "$HOME/.claude"/.*; do - base="$(basename "$item")" - [ "$base" = "." ] || [ "$base" = ".." ] && continue - cp -a "$item" "$CLAUDE_STAGING/$base" 2>/dev/null || true - done - # Also copy ~/.claude.json (onboarding state, outside ~/.claude/) - [ -f "$HOME/.claude.json" ] && cp -a "$HOME/.claude.json" "$CLAUDE_STAGING/.claude.json.root" 2>/dev/null || true + # Credentials (OAuth tokens) — required for authentication + [ -f "$HOME/.claude/.credentials.json" ] && \ + cp -a "$HOME/.claude/.credentials.json" "$CLAUDE_STAGING/.credentials.json" 2>/dev/null || true + # Onboarding state — prevents setup wizard + [ -f "$HOME/.claude.json" ] && \ + cp -a "$HOME/.claude.json" "$CLAUDE_STAGING/.claude.json.root" 2>/dev/null || true + + # Plugins: always copy (just IDs, no secrets) + if [ -f "$HOME/.claude/settings.json" ]; then + jq '{enabledPlugins: .enabledPlugins}' "$HOME/.claude/settings.json" \ + > "$CLAUDE_STAGING/enabled-plugins.json" 2>/dev/null || true + fi + + # Agents & skills: copy only what the user listed in .env + CLAUDE_AGENTS="" + CLAUDE_SKILLS="" + ENV_FILE=".devcontainer/.env" + [ -f "$ENV_FILE" ] && source "$ENV_FILE" + + if [ -n "$CLAUDE_AGENTS" ]; then + mkdir -p "$CLAUDE_STAGING/agents" + IFS=',' read -ra AGENT_LIST <<< "$CLAUDE_AGENTS" + for agent in "${AGENT_LIST[@]}"; do + agent=$(echo "$agent" | xargs) + src="$HOME/.claude/agents/${agent}.md" + [ -f "$src" ] && cp -a "$src" "$CLAUDE_STAGING/agents/" 2>/dev/null || true + done + fi + + if [ -n "$CLAUDE_SKILLS" ]; then + mkdir -p "$CLAUDE_STAGING/skills" + IFS=',' read -ra SKILL_LIST <<< "$CLAUDE_SKILLS" + for skill in "${SKILL_LIST[@]}"; do + skill=$(echo "$skill" | xargs) + src="$HOME/.claude/skills/${skill}" + [ -d "$src" ] && cp -a "$src" "$CLAUDE_STAGING/skills/" 2>/dev/null || true + done + fi fi # Resolve main .git directory for worktree support. diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 7ba4aa43366..d63a69dffda 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -32,54 +32,68 @@ sudo chown -R vscode:vscode /home/vscode/.cargo "$WORKSPACE/target" 2>/dev/null # --- Enable corepack for yarn --- corepack enable 2>/dev/null || true -# --- Claude Code: copy config staged by init-host.sh, then override for sandbox --- -# init-host.sh copies ~/.claude into .devcontainer/.claude-host-config/ on the host. -# The workspace bind mount makes it available here. We copy into the persistent -# volume, then clean up the staging copy (it contains credentials). +# --- Claude Code: copy config staged by init-host.sh --- +# init-host.sh stages credentials, plugin list, and optionally agents/skills. +# We copy into the persistent volume, create a minimal settings.json with +# plugins merged in, then clean up the staging copy. CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-/home/vscode/.claude}" HOST_CONFIG="$WORKSPACE/.devcontainer/.claude-host-config" +mkdir -p "$CLAUDE_DIR" + if [ -d "$HOST_CONFIG" ] && [ "$(ls -A "$HOST_CONFIG" 2>/dev/null)" ]; then echo "Copying Claude config from host..." - cp -a "$HOST_CONFIG"/. "$CLAUDE_DIR"/ 2>/dev/null || true - for item in "$HOST_CONFIG"/.*; do - basename="$(basename "$item")" - [ "$basename" = "." ] || [ "$basename" = ".." ] && continue - cp -a "$item" "$CLAUDE_DIR/$basename" 2>/dev/null || true - done - chmod 600 "$CLAUDE_DIR/.credentials.json" 2>/dev/null || true - # Place ~/.claude.json (onboarding state) at the correct path + # OAuth credentials + if [ -f "$HOST_CONFIG/.credentials.json" ]; then + cp -a "$HOST_CONFIG/.credentials.json" "$CLAUDE_DIR/.credentials.json" + chmod 600 "$CLAUDE_DIR/.credentials.json" + fi + # Onboarding state (prevents setup wizard) if [ -f "$HOST_CONFIG/.claude.json.root" ]; then cp -a "$HOST_CONFIG/.claude.json.root" /home/vscode/.claude.json chown vscode:vscode /home/vscode/.claude.json fi - # Clean up staged credentials from the workspace - rm -rf "$HOST_CONFIG" - echo "Host Claude config copied." + echo "Host Claude credentials copied." else - mkdir -p "$CLAUDE_DIR" - echo "No host Claude config found. Use ANTHROPIC_API_KEY or 'claude login'." + echo "No host Claude credentials found. Use ANTHROPIC_API_KEY or 'claude login'." fi -chown -R vscode:vscode "$CLAUDE_DIR" - -# Force bypassPermissions on top of whatever settings came from host +# Write a clean settings.json with bypassPermissions (no host settings leak) SETTINGS_FILE="$CLAUDE_DIR/settings.json" -if [ -f "$SETTINGS_FILE" ]; then +cat > "$SETTINGS_FILE" <<'SETTINGS' +{ + "permissions": { + "defaultMode": "bypassPermissions" + }, + "skipDangerousModePermissionPrompt": true +} +SETTINGS + +# Merge host's enabledPlugins into settings (plugin IDs only, no secrets) +if [ -f "$HOST_CONFIG/enabled-plugins.json" ]; then TMP=$(mktemp) - jq '.permissions.defaultMode = "bypassPermissions" | .skipDangerousModePermissionPrompt = true' \ - "$SETTINGS_FILE" > "$TMP" 2>/dev/null && mv "$TMP" "$SETTINGS_FILE" || \ - echo '{"permissions":{"defaultMode":"bypassPermissions"},"skipDangerousModePermissionPrompt":true}' > "$SETTINGS_FILE" -else - echo '{"permissions":{"defaultMode":"bypassPermissions"},"skipDangerousModePermissionPrompt":true}' > "$SETTINGS_FILE" + jq -s '.[0] * .[1]' "$SETTINGS_FILE" "$HOST_CONFIG/enabled-plugins.json" \ + > "$TMP" 2>/dev/null && mv "$TMP" "$SETTINGS_FILE" || true fi -chown vscode:vscode "$SETTINGS_FILE" -# --- Shell history (idempotent) --- -grep -q 'HISTFILE=/commandhistory/.zsh_history' /home/vscode/.zshrc 2>/dev/null || \ - echo 'export HISTFILE=/commandhistory/.zsh_history' >> /home/vscode/.zshrc -grep -q 'HISTFILE=/commandhistory/.bash_history' /home/vscode/.bashrc 2>/dev/null || \ - echo 'export HISTFILE=/commandhistory/.bash_history' >> /home/vscode/.bashrc +# Copy host agent definitions +if [ -d "$HOST_CONFIG/agents" ] && [ "$(ls -A "$HOST_CONFIG/agents" 2>/dev/null)" ]; then + mkdir -p "$CLAUDE_DIR/agents" + cp -a "$HOST_CONFIG/agents/"* "$CLAUDE_DIR/agents/" + echo "Host Claude agents copied." +fi + +# Copy host skill definitions +if [ -d "$HOST_CONFIG/skills" ] && [ "$(ls -A "$HOST_CONFIG/skills" 2>/dev/null)" ]; then + mkdir -p "$CLAUDE_DIR/skills" + cp -a "$HOST_CONFIG/skills/"* "$CLAUDE_DIR/skills/" + echo "Host Claude skills copied." +fi + +chown -R vscode:vscode "$CLAUDE_DIR" + +# Clean up staged config from the workspace +rm -rf "$HOST_CONFIG" echo "=== Post-create setup complete ===" echo "Claude Code is configured with bypassPermissions mode." From 4b338b20649582f2262deaa22b409b2ba885c4de Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 4 Mar 2026 22:09:24 +0700 Subject: [PATCH 04/10] docs(devcontainer): add claude login --print-link as in-container auth option Co-Authored-By: Claude Opus 4.6 --- .devcontainer/README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 72c8322b8ce..bfffc4e78f4 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -26,7 +26,7 @@ Without this, `git push`/`git pull` will fail with `Permission denied (publickey ### Claude Code authentication -Authenticate using **one or both** methods. OAuth login must be done on the **host** — it does not work inside the container (the OAuth callback can't reach localhost in the container). +Authenticate using **one or both** methods. ### Option A: OAuth (recommended) @@ -38,6 +38,12 @@ claude login Your OAuth credentials (`~/.claude/.credentials.json`) and enabled plugins are copied into the container. Optionally, agents and skills listed in `.devcontainer/.env` are also copied. No conversation history, project memories, or host settings are transferred. If tokens expire, re-run `claude login` on the host and rebuild. +You can also log in from inside the container using the print-link flow (no browser redirect needed): + +```bash +claude login --print-link +``` + ### Option B: API Key ```bash @@ -172,7 +178,8 @@ Git worktrees are supported automatically. The `init-host.sh` script (runs on th ### Claude says "not authenticated" - Ensure `ANTHROPIC_API_KEY` is set in your host shell, **or** -- Run `claude login` on your host and rebuild the container +- Run `claude login` on your host and rebuild the container, **or** +- Run `claude login --print-link` inside the container (no browser redirect needed) ### `yarn install` fails From 7953b1da91a1b9ceba4727dcdc756a1ad8b86a42 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Wed, 4 Mar 2026 22:10:22 +0700 Subject: [PATCH 05/10] chore(devcontainer): forward CLAUDE_CODE_OAUTH_TOKEN into container Add CLAUDE_CODE_OAUTH_TOKEN to containerEnv so it's automatically forwarded from the host environment, same as ANTHROPIC_API_KEY. Co-Authored-By: Claude Opus 4.6 --- .devcontainer/README.md | 10 +++++++++- .devcontainer/devcontainer.json | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.devcontainer/README.md b/.devcontainer/README.md index bfffc4e78f4..d40d6deba90 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -44,7 +44,15 @@ You can also log in from inside the container using the print-link flow (no brow claude login --print-link ``` -### Option B: API Key +### Option B: OAuth Token + +```bash +export CLAUDE_CODE_OAUTH_TOKEN= +``` + +Set this in your shell profile. The token is forwarded into the container automatically. + +### Option C: API Key ```bash export ANTHROPIC_API_KEY=sk-ant-... diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 0ce3f0ddf7b..91676502a43 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -83,6 +83,7 @@ }, "containerEnv": { "ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}", + "CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}", "CLAUDE_CONFIG_DIR": "/home/vscode/.claude", "NODE_OPTIONS": "--max-old-space-size=4096", "DEVCONTAINER": "true" From 9b1bee6acdc663cdf97a57997445bb8b93da0125 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Tue, 10 Mar 2026 22:26:39 +0700 Subject: [PATCH 06/10] chore(devcontainer): fix symlink bug, remove tracked personal file, add GH_TOKEN - Remove .claude-config-resolved from git (contained personal path) and add it to .gitignore to prevent future commits - Fix ln -sfn symlink bug: rm -rf $RESOLVED before ln -s to avoid creating symlink inside stale directory on subsequent runs - Pass GH_TOKEN/GITHUB_TOKEN from host env so gh CLI works in container Co-Authored-By: Claude Sonnet 4.6 --- .devcontainer/.claude-config-resolved | 1 - .devcontainer/.gitignore | 1 + .devcontainer/devcontainer.json | 2 ++ .devcontainer/init-host.sh | 5 +++-- 4 files changed, 6 insertions(+), 3 deletions(-) delete mode 120000 .devcontainer/.claude-config-resolved diff --git a/.devcontainer/.claude-config-resolved b/.devcontainer/.claude-config-resolved deleted file mode 120000 index 11300fd7e21..00000000000 --- a/.devcontainer/.claude-config-resolved +++ /dev/null @@ -1 +0,0 @@ -/Users/ivanshumkov/.claude \ No newline at end of file diff --git a/.devcontainer/.gitignore b/.devcontainer/.gitignore index f1228114391..abe7781720c 100644 --- a/.devcontainer/.gitignore +++ b/.devcontainer/.gitignore @@ -1,3 +1,4 @@ .env .claude-host-config/ .main-git-resolved +.claude-config-resolved diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 91676502a43..37ba4c16f6a 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -85,6 +85,8 @@ "ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}", "CLAUDE_CODE_OAUTH_TOKEN": "${localEnv:CLAUDE_CODE_OAUTH_TOKEN}", "CLAUDE_CONFIG_DIR": "/home/vscode/.claude", + "GH_TOKEN": "${localEnv:GH_TOKEN}", + "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}", "NODE_OPTIONS": "--max-old-space-size=4096", "DEVCONTAINER": "true" }, diff --git a/.devcontainer/init-host.sh b/.devcontainer/init-host.sh index 63214220c04..1d916d777cd 100755 --- a/.devcontainer/init-host.sh +++ b/.devcontainer/init-host.sh @@ -59,6 +59,7 @@ fi # at a known path that always points to the real .git directory. # This way the same devcontainer.json works for both worktrees and main repo. RESOLVED=".devcontainer/.main-git-resolved" +rm -rf "$RESOLVED" if [ -f .git ]; then # Worktree: .git file contains "gitdir: /path/to/main/.git/worktrees/name" @@ -66,13 +67,13 @@ if [ -f .git ]; then # Strip /worktrees/name to get the main .git directory MAIN_GIT="${GITDIR%/worktrees/*}" if [ -d "$MAIN_GIT" ]; then - ln -sfn "$MAIN_GIT" "$RESOLVED" + ln -s "$MAIN_GIT" "$RESOLVED" else mkdir -p "$RESOLVED" fi elif [ -d .git ]; then # Main repo: just point to our own .git - ln -sfn "$(pwd)/.git" "$RESOLVED" + ln -s "$(pwd)/.git" "$RESOLVED" else # No git at all — empty dir so the mount doesn't fail mkdir -p "$RESOLVED" From abaf2feb1a5babaf5dc80d2e251ff5f1d5b27129 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 12 Mar 2026 12:04:35 +0700 Subject: [PATCH 07/10] chore(devcontainer): remove ~/.claude.json from container staging ~/.claude.json contains sensitive data: email, account UUID, session IDs, project paths with token usage stats, and feature flag assignments. It should not be copied into the devcontainer where Claude runs in bypass mode. Authentication already works via .credentials.json alone. Co-Authored-By: Claude Sonnet 4.6 --- .devcontainer/init-host.sh | 4 ---- .devcontainer/post-create.sh | 5 ----- 2 files changed, 9 deletions(-) diff --git a/.devcontainer/init-host.sh b/.devcontainer/init-host.sh index 1d916d777cd..28cbb55b3ac 100755 --- a/.devcontainer/init-host.sh +++ b/.devcontainer/init-host.sh @@ -17,10 +17,6 @@ if [ -d "$HOME/.claude" ]; then # Credentials (OAuth tokens) — required for authentication [ -f "$HOME/.claude/.credentials.json" ] && \ cp -a "$HOME/.claude/.credentials.json" "$CLAUDE_STAGING/.credentials.json" 2>/dev/null || true - # Onboarding state — prevents setup wizard - [ -f "$HOME/.claude.json" ] && \ - cp -a "$HOME/.claude.json" "$CLAUDE_STAGING/.claude.json.root" 2>/dev/null || true - # Plugins: always copy (just IDs, no secrets) if [ -f "$HOME/.claude/settings.json" ]; then jq '{enabledPlugins: .enabledPlugins}' "$HOME/.claude/settings.json" \ diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index d63a69dffda..e86fb4327be 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -48,11 +48,6 @@ if [ -d "$HOST_CONFIG" ] && [ "$(ls -A "$HOST_CONFIG" 2>/dev/null)" ]; then cp -a "$HOST_CONFIG/.credentials.json" "$CLAUDE_DIR/.credentials.json" chmod 600 "$CLAUDE_DIR/.credentials.json" fi - # Onboarding state (prevents setup wizard) - if [ -f "$HOST_CONFIG/.claude.json.root" ]; then - cp -a "$HOST_CONFIG/.claude.json.root" /home/vscode/.claude.json - chown vscode:vscode /home/vscode/.claude.json - fi echo "Host Claude credentials copied." else echo "No host Claude credentials found. Use ANTHROPIC_API_KEY or 'claude login'." From f0698487275bc4939dbc84b98f4292b7ac225381 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 12 Mar 2026 12:09:48 +0700 Subject: [PATCH 08/10] chore(devcontainer): make agent/skill host copy opt-in, remove auto plugin sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove automatic syncing of all host enabledPlugins — plugins should be defined per-project in the repo's .claude/settings.json - Keep opt-in CLAUDE_AGENTS and CLAUDE_SKILLS in .env for users who want to bring specific personal agents/skills into the container - Credentials (.credentials.json) are still always copied from the host Co-Authored-By: Claude Sonnet 4.6 --- .devcontainer/init-host.sh | 12 +++--------- .devcontainer/post-create.sh | 33 +++++++++++---------------------- 2 files changed, 14 insertions(+), 31 deletions(-) diff --git a/.devcontainer/init-host.sh b/.devcontainer/init-host.sh index 28cbb55b3ac..e1436914ca5 100755 --- a/.devcontainer/init-host.sh +++ b/.devcontainer/init-host.sh @@ -6,10 +6,9 @@ set -euo pipefail test -f "$HOME/.gitconfig" || touch "$HOME/.gitconfig" mkdir -p "$HOME/.claude" -# Stage ONLY the minimum Claude config needed for authentication. -# We deliberately skip: conversation history, project memories, debug logs, -# shell snapshots, plans, scripts, plugins cache, and other data that could -# leak information from other projects into the sandboxed container. +# Stage credentials and optionally agents/skills from the host. +# Plugins, agents, and skills are NOT copied automatically — list what you +# want explicitly in .devcontainer/.env (CLAUDE_AGENTS, CLAUDE_SKILLS). CLAUDE_STAGING=".devcontainer/.claude-host-config" rm -rf "$CLAUDE_STAGING" if [ -d "$HOME/.claude" ]; then @@ -17,11 +16,6 @@ if [ -d "$HOME/.claude" ]; then # Credentials (OAuth tokens) — required for authentication [ -f "$HOME/.claude/.credentials.json" ] && \ cp -a "$HOME/.claude/.credentials.json" "$CLAUDE_STAGING/.credentials.json" 2>/dev/null || true - # Plugins: always copy (just IDs, no secrets) - if [ -f "$HOME/.claude/settings.json" ]; then - jq '{enabledPlugins: .enabledPlugins}' "$HOME/.claude/settings.json" \ - > "$CLAUDE_STAGING/enabled-plugins.json" 2>/dev/null || true - fi # Agents & skills: copy only what the user listed in .env CLAUDE_AGENTS="" diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index e86fb4327be..25eebe662e4 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -33,29 +33,25 @@ sudo chown -R vscode:vscode /home/vscode/.cargo "$WORKSPACE/target" 2>/dev/null corepack enable 2>/dev/null || true # --- Claude Code: copy config staged by init-host.sh --- -# init-host.sh stages credentials, plugin list, and optionally agents/skills. -# We copy into the persistent volume, create a minimal settings.json with -# plugins merged in, then clean up the staging copy. +# Credentials are always copied. Agents and skills are copied only if listed +# in .devcontainer/.env (CLAUDE_AGENTS, CLAUDE_SKILLS). Plugins are not copied +# automatically — use the project's .claude/settings.json instead. CLAUDE_DIR="${CLAUDE_CONFIG_DIR:-/home/vscode/.claude}" HOST_CONFIG="$WORKSPACE/.devcontainer/.claude-host-config" mkdir -p "$CLAUDE_DIR" -if [ -d "$HOST_CONFIG" ] && [ "$(ls -A "$HOST_CONFIG" 2>/dev/null)" ]; then - echo "Copying Claude config from host..." - # OAuth credentials - if [ -f "$HOST_CONFIG/.credentials.json" ]; then - cp -a "$HOST_CONFIG/.credentials.json" "$CLAUDE_DIR/.credentials.json" - chmod 600 "$CLAUDE_DIR/.credentials.json" - fi +if [ -f "$HOST_CONFIG/.credentials.json" ]; then + echo "Copying Claude credentials from host..." + cp -a "$HOST_CONFIG/.credentials.json" "$CLAUDE_DIR/.credentials.json" + chmod 600 "$CLAUDE_DIR/.credentials.json" echo "Host Claude credentials copied." else echo "No host Claude credentials found. Use ANTHROPIC_API_KEY or 'claude login'." fi -# Write a clean settings.json with bypassPermissions (no host settings leak) -SETTINGS_FILE="$CLAUDE_DIR/settings.json" -cat > "$SETTINGS_FILE" <<'SETTINGS' +# Write a clean settings.json with bypassPermissions +cat > "$CLAUDE_DIR/settings.json" <<'SETTINGS' { "permissions": { "defaultMode": "bypassPermissions" @@ -64,21 +60,14 @@ cat > "$SETTINGS_FILE" <<'SETTINGS' } SETTINGS -# Merge host's enabledPlugins into settings (plugin IDs only, no secrets) -if [ -f "$HOST_CONFIG/enabled-plugins.json" ]; then - TMP=$(mktemp) - jq -s '.[0] * .[1]' "$SETTINGS_FILE" "$HOST_CONFIG/enabled-plugins.json" \ - > "$TMP" 2>/dev/null && mv "$TMP" "$SETTINGS_FILE" || true -fi - -# Copy host agent definitions +# Copy host agent definitions (opt-in via CLAUDE_AGENTS in .env) if [ -d "$HOST_CONFIG/agents" ] && [ "$(ls -A "$HOST_CONFIG/agents" 2>/dev/null)" ]; then mkdir -p "$CLAUDE_DIR/agents" cp -a "$HOST_CONFIG/agents/"* "$CLAUDE_DIR/agents/" echo "Host Claude agents copied." fi -# Copy host skill definitions +# Copy host skill definitions (opt-in via CLAUDE_SKILLS in .env) if [ -d "$HOST_CONFIG/skills" ] && [ "$(ls -A "$HOST_CONFIG/skills" 2>/dev/null)" ]; then mkdir -p "$CLAUDE_DIR/skills" cp -a "$HOST_CONFIG/skills/"* "$CLAUDE_DIR/skills/" From 1772527b6c20299bee0cad6f100c9778f2c5072f Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Thu, 12 Mar 2026 13:24:17 +0700 Subject: [PATCH 09/10] chore(devcontainer): update docs to reflect new plugin/agent/skill config model - Plugins are no longer auto-synced from host; document settings.local.json approach instead (gitignored by Claude Code, no unknown field risk) - Agents/skills remain opt-in via CLAUDE_AGENTS/CLAUDE_SKILLS in .env - Update security model section and .env.example accordingly Co-Authored-By: Claude Sonnet 4.6 --- .devcontainer/.env.example | 7 +++++-- .devcontainer/README.md | 22 +++++++++++++++------- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/.devcontainer/.env.example b/.devcontainer/.env.example index d46c177e4a2..f9b6e58b5a6 100644 --- a/.devcontainer/.env.example +++ b/.devcontainer/.env.example @@ -1,5 +1,8 @@ -# Claude Code: agents and skills to copy from ~/.claude/ into the container. -# Copy this file to .env and customize. Plugins are always synced automatically. +# Personal Claude Code agents and skills to copy from your host ~/.claude/ into the container. +# Copy this file to .env and customize. This file is gitignored. +# +# For plugins, use .claude/settings.local.json inside the container instead +# (auto-gitignored by Claude Code). # # Comma-separated names (agents without .md extension, skills as directory names) CLAUDE_AGENTS= diff --git a/.devcontainer/README.md b/.devcontainer/README.md index d40d6deba90..967a836e4a0 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -36,7 +36,7 @@ Run on your **host machine** before opening the devcontainer: claude login ``` -Your OAuth credentials (`~/.claude/.credentials.json`) and enabled plugins are copied into the container. Optionally, agents and skills listed in `.devcontainer/.env` are also copied. No conversation history, project memories, or host settings are transferred. If tokens expire, re-run `claude login` on the host and rebuild. +Your OAuth credentials (`~/.claude/.credentials.json`) are copied into the container. Optionally, personal agents and skills listed in `.devcontainer/.env` are also copied. No conversation history, project memories, settings, or plugins are transferred. If tokens expire, re-run `claude login` on the host and rebuild. You can also log in from inside the container using the print-link flow (no browser redirect needed): @@ -121,11 +121,19 @@ devcontainer exec --workspace-folder . claude -p "run the test suite" --dangerou ### Plugins -Enabled plugins from your host `~/.claude/settings.json` are automatically synced into the container. No configuration needed — plugin IDs contain no secrets. +Plugins are **not** copied from your host. Use `.claude/settings.local.json` inside the container to enable personal plugins — this file is automatically gitignored by Claude Code: + +```json +{ + "enabledPlugins": { + "my-plugin@my-marketplace": true + } +} +``` ### Agents & skills -To copy personal agents or skills from `~/.claude/` into the container, create a `.env` file: +Personal agents and skills are **not** copied automatically. To bring specific ones from your `~/.claude/` into the container, create a `.env` file: ```bash cp .devcontainer/.env.example .devcontainer/.env @@ -145,15 +153,15 @@ The `.env` file is gitignored — each developer configures their own. ### Project-level settings -The project's `.claude/` directory (containing `settings.local.json` and `skills/`) is automatically available inside the container via the workspace bind mount. No extra configuration needed. +The project's `.claude/` directory is available inside the container via the workspace bind mount. Project-level agents (`.claude/agents/`) and skills (`.claude/skills/`) are automatically loaded by Claude Code. ## Security Model Claude Code runs with `bypassPermissions` inside the container — it can read, write, and execute anything. The container is the sandbox boundary. To minimize exposure: -- **Only OAuth credentials** are copied from the host (`~/.claude/.credentials.json`). No conversation history, project memories, settings, hooks, scripts, or debug logs are transferred. -- **Enabled plugins** (just plugin IDs) are synced from host settings. Optionally, listed agents/skills (markdown files only) are copied. -- **A clean `settings.json`** is generated inside the container with `bypassPermissions` and enabled plugins — your host's permission allowlists, MCP server configs, and hooks are not copied. +- **Only OAuth credentials** are copied from the host (`~/.claude/.credentials.json`). No conversation history, project memories, settings, plugins, hooks, scripts, or debug logs are transferred. +- **Agents/skills** are only copied if explicitly listed in `.devcontainer/.env` — nothing personal leaks in by default. +- **A clean `settings.json`** is generated inside the container with `bypassPermissions` — your host's permission allowlists, MCP server configs, and hooks are not copied. - **No shell history** is persisted or shared with the container. - **The `.git` directory** is mounted read-write (required for commits/pushes). This is the main trust boundary — Claude can push code. From 1aa58103dccbd9e01c5e7afeaedce92a3e49f325 Mon Sep 17 00:00:00 2001 From: Ivan Shumkov Date: Fri, 13 Mar 2026 19:40:48 +0700 Subject: [PATCH 10/10] chore(devcontainer): address reviewer feedback - Replace `source .env` with safe key-value parser (SEC-001: prevents arbitrary shell code execution from a tampered .env file) - Add `gh auth setup-git` in post-create when GH_TOKEN/GITHUB_TOKEN is set, enabling HTTPS git push/pull without SSH keys - Update README: document GH_TOKEN as the preferred GitHub auth method, fix firewall setup instructions (provide explicit Dockerfile changes instead of referencing a nonexistent commented-out block) - Remove unused /commandhistory setup from Dockerfile (no volume mount backs it; README correctly states history is not persisted) Co-Authored-By: Claude Sonnet 4.6 --- .devcontainer/Dockerfile | 5 ----- .devcontainer/README.md | 23 ++++++++++++++++++----- .devcontainer/init-host.sh | 9 ++++++++- .devcontainer/post-create.sh | 7 +++++++ 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 7e63c7995b7..9e6925cda48 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -50,11 +50,6 @@ RUN ARCH=$(dpkg --print-architecture) && \ dpkg -i /tmp/git-delta.deb && \ rm /tmp/git-delta.deb -# Persist bash/zsh history -RUN mkdir -p /commandhistory && \ - touch /commandhistory/.bash_history /commandhistory/.zsh_history && \ - chown -R vscode:vscode /commandhistory - # Switch to vscode user for Rust and cargo tools USER vscode diff --git a/.devcontainer/README.md b/.devcontainer/README.md index 967a836e4a0..2343e7d0821 100644 --- a/.devcontainer/README.md +++ b/.devcontainer/README.md @@ -13,17 +13,21 @@ Sandboxed development environment for Dash Platform with Claude Code pre-configu ## Prerequisites -### SSH keys (for git push/pull) +### GitHub access (for git push/pull) -VS Code forwards your host's SSH agent into the container automatically. Make sure your key is loaded: +The easiest option is to export `GH_TOKEN` (or `GITHUB_TOKEN`) on your host. The devcontainer forwards it and configures HTTPS git automatically — no SSH key required: + +```bash +export GH_TOKEN=ghp_... # add to your shell profile +``` + +Alternatively, VS Code forwards your host's SSH agent into the container. Make sure your key is loaded: ```bash ssh-add --apple-use-keychain ~/.ssh/id_rsa # macOS ssh-add ~/.ssh/id_rsa # Linux ``` -Without this, `git push`/`git pull` will fail with `Permission denied (publickey)`. - ### Claude Code authentication Authenticate using **one or both** methods. @@ -175,7 +179,16 @@ By default, the container has unrestricted network access. To enable a restricti "waitFor": "postStartCommand" ``` -You'll also need to add `iptables ipset iproute2 dnsutils` to the `apt-get install` in the Dockerfile and uncomment the firewall COPY/sudoers block. See `init-firewall.sh` for the domain whitelist. +You'll also need to make two changes to the `Dockerfile`: + +1. Add `iptables ipset iproute2 dnsutils` to the `apt-get install` block. +2. After the apt-get block, add: + ```dockerfile + COPY init-firewall.sh /usr/local/bin/init-firewall.sh + RUN echo "vscode ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/firewall + ``` + +See `init-firewall.sh` for the domain whitelist. ## Persistent Data diff --git a/.devcontainer/init-host.sh b/.devcontainer/init-host.sh index e1436914ca5..d96af4b778f 100755 --- a/.devcontainer/init-host.sh +++ b/.devcontainer/init-host.sh @@ -21,7 +21,14 @@ if [ -d "$HOME/.claude" ]; then CLAUDE_AGENTS="" CLAUDE_SKILLS="" ENV_FILE=".devcontainer/.env" - [ -f "$ENV_FILE" ] && source "$ENV_FILE" + if [ -f "$ENV_FILE" ]; then + while IFS='=' read -r key value; do + case "$key" in + CLAUDE_AGENTS) CLAUDE_AGENTS="$value" ;; + CLAUDE_SKILLS) CLAUDE_SKILLS="$value" ;; + esac + done < <(grep -E '^(CLAUDE_AGENTS|CLAUDE_SKILLS)=' "$ENV_FILE") + fi if [ -n "$CLAUDE_AGENTS" ]; then mkdir -p "$CLAUDE_STAGING/agents" diff --git a/.devcontainer/post-create.sh b/.devcontainer/post-create.sh index 25eebe662e4..05700dd1f7d 100755 --- a/.devcontainer/post-create.sh +++ b/.devcontainer/post-create.sh @@ -26,6 +26,13 @@ fi # --- Git configuration --- git config --global --add safe.directory "$WORKSPACE" +# Configure git to use HTTPS with GH_TOKEN/GITHUB_TOKEN if available. +# This allows git push/pull and gh CLI to work without SSH keys. +if [ -n "${GH_TOKEN:-}" ] || [ -n "${GITHUB_TOKEN:-}" ]; then + gh auth setup-git 2>/dev/null || true + echo "GitHub auth configured via GH_TOKEN (HTTPS git operations enabled)." +fi + # --- Cargo permissions --- sudo chown -R vscode:vscode /home/vscode/.cargo "$WORKSPACE/target" 2>/dev/null || true