Skip to content

chore: integrate Claude Code into devcontainer for autonomus development#3166

Open
shumkov wants to merge 6 commits intov3.1-devfrom
chore/devcontainer
Open

chore: integrate Claude Code into devcontainer for autonomus development#3166
shumkov wants to merge 6 commits intov3.1-devfrom
chore/devcontainer

Conversation

@shumkov
Copy link
Collaborator

@shumkov shumkov commented Mar 1, 2026

Issue being fixed or feature implemented

Integrate Claude Code into the Dev Container to enable autonomous AI-assisted
development with full sandbox isolation. This gives developers (and Claude itself) a
ready-to-go environment where Claude Code can build, test, and iterate on Dash Platform
code without manual setup or permission prompts.

What was done?

Claude Code integration

  • Added [ghcr.io/anthropics/devcontainer-features/claude-code](https://github.com/anth
    ropics/devcontainer-features) as a devcontainer feature
  • Host ~/.claude/ config (credentials, skills, plugins) is staged by init-host.sh
    and copied into a persistent Docker volume by post-create.sh, then the staged copy is
    cleaned up
  • bypassPermissions mode is forced in Claude settings so Claude Code runs autonomously
    without prompts
  • ANTHROPIC_API_KEY is forwarded from the host environment as a fallback auth method
  • Persistent named volume for ~/.claude/ survives container rebuilds (conversation
    history, config)

Optional network firewall (.devcontainer/init-firewall.sh)

  • iptables-based outbound firewall restricting Claude Code and other tools to
    whitelisted services only (Anthropic API, npm, crates.io, GitHub, Docker Hub, VS Code
    marketplace)
  • Disabled by default — documented how to enable for stricter sandboxing

Devcontainer modernization

  • Replaced static image reference (ghcr.io/dashpay/platform/devcontainer:0.1.0) with a
    local Dockerfile build
  • Added devcontainer features: Node.js 24, Docker-in-Docker, GitHub CLI, common-utils,
    jq-likes
  • Added VS Code extensions: Claude Code, rust-analyzer, ESLint, Prettier, GitLens,
    Docker, LLDB, TOML
  • Configured named Docker volumes for cargo cache, build target, and shell history
  • Added developer tools: ripgrep, fd-find, fzf, git-delta, vim, nano
  • Bumped wasm-bindgen-cli to 0.2.108, added wasm-pack

Git worktree support

  • init-host.sh resolves the main .git directory and mounts it into the container
  • post-create.sh creates symlinks so git operations work transparently from worktrees

Documentation

  • Added .devcontainer/README.md covering prerequisites, auth options (OAuth + API
    key), VS Code and CLI usage, firewall setup, persistent data, and troubleshooting
  • Updated root README.md with Dev Container as recommended setup method

How Has This Been Tested?

  • Built and opened the devcontainer in VS Code on macOS (arm64) from both main repo and
    a git worktree
  • Verified Claude Code launches with bypassPermissions mode and host credentials
  • Verified git operations (status, commit, push) work inside the container from a
    worktree
  • Verified Docker-in-Docker is functional for dashmate usage
  • Verified persistent volumes survive container rebuilds (cargo cache, Claude config,
    shell history)

Breaking Changes

None. The devcontainer previously pointed to a static image. This replaces it with a
local build — existing users will get the new configuration on next rebuild.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have added or updated relevant unit/integration/functional/e2e tests
  • I have added "!" to the title and described breaking changes in the corresponding
    section if my code contains any
  • I have made corresponding changes to the documentation if needed

For repository code-owners and collaborators only

  • I have assigned this pull request to a milestone

Summary by CodeRabbit

  • New Features

    • Full Dev Container workflow: parameterized build, richer toolchain, workspace mounts, init/post-create setup, firewall and host initialization, and Claude integration for a reproducible sandboxed dev environment.
  • Documentation

    • Added detailed devcontainer README and updated main README with setup and manual instructions.
  • Chores

    • Bumped Node minimum to v24 and wasm-bindgen-cli to 0.2.108; removed legacy devcontainer prebuild workflow; updated gitignore for host-resolved artifacts.

@shumkov shumkov requested a review from QuantumExplorer as a code owner March 1, 2026 14:09
@github-actions github-actions bot added this to the v3.1.0 milestone Mar 1, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

📝 Walkthrough

Walkthrough

Replaces the devcontainer with a build-based setup: new Dockerfile, parameterized devcontainer.json, host/container init scripts (firewall, Claude staging, git worktree handling), post-create setup, docs, env examples, and removal of prior prebuild workflow and build JSON.

Changes

Cohort / File(s) Summary
Dockerfile & toolchain
.devcontainer/Dockerfile
Replaces base image and adds extensive tooling and architecture-aware installs (protoc v32, git-delta, rust/cargo toolchain changes, wasm tooling), TZ/env handling, user/permission and shell history persistence.
Devcontainer config & lifecycle scripts
.devcontainer/devcontainer.json, .devcontainer/post-create.sh, .devcontainer/init-host.sh, .devcontainer/init-firewall.sh
Switches to build-based devcontainer with build args, features, mounts, containerEnv, initialize/postCreate commands; adds host init (Claude staging, selective agents/skills, git worktree resolution), outbound firewall script (iptables/ipset allowlist), and post-create setup (git worktree symlink, Claude config install, perms, history/settings).
Docs & examples
.devcontainer/README.md, .devcontainer/.env.example, README.md
Adds comprehensive devcontainer README, .env example for CLAUDE_AGENTS/CLAUDE_SKILLS, and updates root README with Dev Container and manual setup notes (Node/wasm-bindgen versions).
Ignored & small artifacts
.devcontainer/.claude-config-resolved, .devcontainer/.gitignore, .gitignore
Adds one-line .claude-config-resolved, ignores machine-specific staging/symlink files, and adds .env/staging entries to .devcontainer/.gitignore.
Removed CI / prebuild config
.devcontainer/devcontainer-build.json, .github/workflows/prebuild-devcontainers.yml
Deletes prior devcontainer build JSON and GitHub Actions workflow that built/pushed prebuilt devcontainer images.

Sequence Diagram(s)

sequenceDiagram
    actor Host as Host
    participant InitHost as init-host.sh
    participant Builder as Docker Build
    participant Container as DevContainer
    participant PostCreate as post-create.sh
    participant VSCode as VS Code

    Host->>InitHost: run initializeCommand (pre-create)
    InitHost-->>Host: create staging (.devcontainer/.claude-host-config) and .main-git-resolved

    Host->>Builder: build image (devcontainer.json -> Dockerfile)
    Builder->>Builder: install toolchain (protoc, Rust, wasm, git-delta, cargo tools)
    Builder-->>Container: produce image with vscode user & workspace

    Container->>Container: container start (initialize)
    Container->>PostCreate: run postCreateCommand
    PostCreate->>PostCreate: link host git worktree, set cargo permissions
    PostCreate->>PostCreate: copy Claude staging into /home/vscode/.claude, adjust settings
    PostCreate-->>Container: finalize environment

    VSCode->>Container: attach, extensions & workspace available
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰🛠️ I hopped through Docker, scripts, and keys,
Stashed Claude crumbs and tightened fences with ease.
Rust boots laced, wasm wings prepped to fly,
Firewall guards the carrots, code reaches the sky.
Hop in the container — build, run, and sigh.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'chore: integrate Claude Code into devcontainer for autonomus development' is closely related to the main changes. It accurately describes the primary objective of integrating Claude Code into the devcontainer, though there is a typo ('autonomus' should be 'autonomous'). The title captures the essence of the substantial changes across multiple devcontainer configuration files.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/devcontainer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@shumkov shumkov self-assigned this Mar 1, 2026
@shumkov shumkov moved this to In review / testing in Platform team Mar 1, 2026
@shumkov shumkov changed the title chore: integrate Claude Code into devcontainer chore: integrate Claude Code into devcontainer for autonomus development Mar 1, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
.devcontainer/init-firewall.sh (2)

92-92: Consider using conntrack instead of deprecated state module.

The --state option is deprecated in favor of -m conntrack --ctstate. This is a minor compatibility note for newer iptables versions.

Suggested update
-iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
+iptables -A OUTPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.devcontainer/init-firewall.sh at line 92, Replace the deprecated state
match in the iptables rule: update the rule that currently uses "-m state
--state ESTABLISHED,RELATED -j ACCEPT" (found in the init-firewall script) to
use the conntrack match instead by switching to "-m conntrack --ctstate
ESTABLISHED,RELATED -j ACCEPT" so it uses the modern conntrack module while
preserving the same behavior.

62-68: Consider validating GitHub CIDR ranges before adding to iptables.

The GitHub meta API response is used directly in iptables rules without validation. A compromised or malformed response could inject unexpected rules. While the 2>/dev/null || true on line 112 suppresses errors, explicit validation would be safer.

Suggested validation
 # 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)
+GITHUB_IPS=$(curl -sf --max-time 10 https://api.github.com/meta 2>/dev/null | jq -r '.web[], .api[], .git[], .actions[]' 2>/dev/null | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(/[0-9]+)?$' || true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.devcontainer/init-firewall.sh around lines 62 - 68, The script currently
assigns GITHUB_IPS from the GitHub meta API and feeds those values into firewall
rules; instead validate each entry before applying rules: after GITHUB_IPS is
populated, iterate its items and verify each is a valid IPv4/IPv6 address or
CIDR (use a strict regex or syscalls like ipcalc/ip in the shell) and reject
anything that doesn't match, log rejected/malformed entries, and only pass
sanitized CIDR strings to resolve_and_allow or the iptables commands; implement
this validation where GITHUB_IPS is set and before any use of
resolve_and_allow/iptables so resolve_and_allow and iptables only ever receive
trusted, validated CIDR/IP strings.
.devcontainer/post-create.sh (1)

44-49: Redundant dotfile copy loop — same issue as in init-host.sh.

The cp -a "$HOST_CONFIG"/. "$CLAUDE_DIR"/ on line 44 already copies hidden files. The subsequent loop duplicates this.

Suggested simplification
 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
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.devcontainer/post-create.sh around lines 44 - 49, The duplicate copy is
caused by the initial cp -a "$HOST_CONFIG"/. "$CLAUDE_DIR"/ which already copies
hidden files, followed by the for loop (for item in "$HOST_CONFIG"/.*) that
re-copies dotfiles; remove the entire loop (the for ... do ... done block) and
keep the single cp -a "$HOST_CONFIG"/. "$CLAUDE_DIR"/ command so hidden files
are copied once and behavior matches the init-host.sh approach.
.devcontainer/init-host.sh (1)

14-22: Redundant dotfile copy loop — cp -a dir/. already copies hidden files.

The command on line 16 (cp -a "$HOME/.claude"/. "$CLAUDE_STAGING"/) already copies all contents including dotfiles. The loop on lines 18-22 duplicates this work.

Suggested simplification
 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
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.devcontainer/init-host.sh around lines 14 - 22, The dotfile copy loop is
redundant because the existing cp -a "$HOME/.claude"/. "$CLAUDE_STAGING"/
already copies hidden files; remove the entire for-loop that iterates over
"$HOME/.claude"/.* (the block that computes base and cp -a "$item"
"$CLAUDE_STAGING/$base") and keep the mkdir -p "$CLAUDE_STAGING" and the single
cp -a "$HOME/.claude"/. "$CLAUDE_STAGING"/ 2>/dev/null || true call so
CLAUDE_STAGING population and existing error suppression remain intact.
.devcontainer/Dockerfile (2)

1-2: Consider pinning the base image version for reproducibility.

The :ubuntu tag will pull whatever the latest Ubuntu-based devcontainer image is at build time. For more reproducible builds, consider pinning to a specific version (e.g., :ubuntu-24.04 or using a digest).

That said, for a devcontainer where staying current may be desirable, this is an acceptable trade-off.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.devcontainer/Dockerfile around lines 1 - 2, The Dockerfile uses an unpinned
base image "FROM mcr.microsoft.com/devcontainers/base:ubuntu" which makes builds
non-reproducible; update that FROM line to reference a specific, pinned tag or
digest (for example "mcr.microsoft.com/devcontainers/base:ubuntu-24.04" or a
sha256 digest) so builds are deterministic, and document the chosen pin in the
Dockerfile comment; locate the FROM instruction in the Dockerfile and replace
the ":ubuntu" tag with the chosen version or digest.

64-71: TOML parsing is fragile and may break with different formatting.

The current parsing assumes channel = "version" with spaces around =. TOML allows channel="version" (no spaces), which would cause awk '{print $3}' to fail.

Consider a more robust extraction:

♻️ More robust TOML parsing
 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 '"')" && \
+RUN TOOLCHAIN_VERSION="$(grep -oP 'channel\s*=\s*"\K[^"]+' /tmp/rust-toolchain.toml)" && \
     curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- \

Alternatively, using sed:

TOOLCHAIN_VERSION="$(sed -n 's/^channel[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' /tmp/rust-toolchain.toml)"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.devcontainer/Dockerfile around lines 64 - 71, The current RUN that sets
TOOLCHAIN_VERSION from rust-toolchain.toml using grep/awk is fragile (it depends
on spacing); change the extraction used in the RUN command to a robust
regex-based parser (e.g., sed) that captures the value of channel regardless of
spaces or quoting, so TOOLCHAIN_VERSION is correctly set from
rust-toolchain.toml before invoking the rustup installer; keep the rest of the
RUN flow (invoking sh.rustup.rs with --default-toolchain and --target
wasm32-unknown-unknown and removing /tmp/rust-toolchain.toml) unchanged, and
reference the variables and files exactly as used: TOOLCHAIN_VERSION,
rust-toolchain.toml, and the existing RUN invocation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.devcontainer/.claude-config-resolved:
- Line 1: Remove the machine-specific file .devcontainer/.claude-config-resolved
from the repository and stop tracking it: add an appropriate ignore pattern
(e.g., .devcontainer/.claude-config-resolved or a broader
.devcontainer/*.resolved) to .gitignore, remove the tracked file from git (git
rm --cached or git rm as appropriate), and commit the change so the personal
path (/Users/ivanshumkov/.claude) is not stored in the repo; ensure the
.devcontainer/.claude-config-resolved entry is present in .gitignore to prevent
future commits of that generated, user-specific file.

In @.devcontainer/README.md:
- Around line 108-114: The README statement saying the host's `~/.claude/`
directory is "mounted read-only" is inaccurate because the scripts init-host.sh
and post-create.sh actually copy the config into a staging area and then into a
persistent Docker volume; update the text in .devcontainer/README.md to say the
config is copied into a persistent Docker volume (not mounted), and keep/clarify
the notes that post-create.sh forces bypassPermissions and skips the safety
confirmation prompt and that host-specific path references are copied as-is and
may log harmless warnings in-container.

---

Nitpick comments:
In @.devcontainer/Dockerfile:
- Around line 1-2: The Dockerfile uses an unpinned base image "FROM
mcr.microsoft.com/devcontainers/base:ubuntu" which makes builds
non-reproducible; update that FROM line to reference a specific, pinned tag or
digest (for example "mcr.microsoft.com/devcontainers/base:ubuntu-24.04" or a
sha256 digest) so builds are deterministic, and document the chosen pin in the
Dockerfile comment; locate the FROM instruction in the Dockerfile and replace
the ":ubuntu" tag with the chosen version or digest.
- Around line 64-71: The current RUN that sets TOOLCHAIN_VERSION from
rust-toolchain.toml using grep/awk is fragile (it depends on spacing); change
the extraction used in the RUN command to a robust regex-based parser (e.g.,
sed) that captures the value of channel regardless of spaces or quoting, so
TOOLCHAIN_VERSION is correctly set from rust-toolchain.toml before invoking the
rustup installer; keep the rest of the RUN flow (invoking sh.rustup.rs with
--default-toolchain and --target wasm32-unknown-unknown and removing
/tmp/rust-toolchain.toml) unchanged, and reference the variables and files
exactly as used: TOOLCHAIN_VERSION, rust-toolchain.toml, and the existing RUN
invocation.

In @.devcontainer/init-firewall.sh:
- Line 92: Replace the deprecated state match in the iptables rule: update the
rule that currently uses "-m state --state ESTABLISHED,RELATED -j ACCEPT" (found
in the init-firewall script) to use the conntrack match instead by switching to
"-m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT" so it uses the modern
conntrack module while preserving the same behavior.
- Around line 62-68: The script currently assigns GITHUB_IPS from the GitHub
meta API and feeds those values into firewall rules; instead validate each entry
before applying rules: after GITHUB_IPS is populated, iterate its items and
verify each is a valid IPv4/IPv6 address or CIDR (use a strict regex or syscalls
like ipcalc/ip in the shell) and reject anything that doesn't match, log
rejected/malformed entries, and only pass sanitized CIDR strings to
resolve_and_allow or the iptables commands; implement this validation where
GITHUB_IPS is set and before any use of resolve_and_allow/iptables so
resolve_and_allow and iptables only ever receive trusted, validated CIDR/IP
strings.

In @.devcontainer/init-host.sh:
- Around line 14-22: The dotfile copy loop is redundant because the existing cp
-a "$HOME/.claude"/. "$CLAUDE_STAGING"/ already copies hidden files; remove the
entire for-loop that iterates over "$HOME/.claude"/.* (the block that computes
base and cp -a "$item" "$CLAUDE_STAGING/$base") and keep the mkdir -p
"$CLAUDE_STAGING" and the single cp -a "$HOME/.claude"/. "$CLAUDE_STAGING"/
2>/dev/null || true call so CLAUDE_STAGING population and existing error
suppression remain intact.

In @.devcontainer/post-create.sh:
- Around line 44-49: The duplicate copy is caused by the initial cp -a
"$HOST_CONFIG"/. "$CLAUDE_DIR"/ which already copies hidden files, followed by
the for loop (for item in "$HOST_CONFIG"/.*) that re-copies dotfiles; remove the
entire loop (the for ... do ... done block) and keep the single cp -a
"$HOST_CONFIG"/. "$CLAUDE_DIR"/ command so hidden files are copied once and
behavior matches the init-host.sh approach.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 78184f6 and 82fff6a.

📒 Files selected for processing (9)
  • .devcontainer/.claude-config-resolved
  • .devcontainer/Dockerfile
  • .devcontainer/README.md
  • .devcontainer/devcontainer.json
  • .devcontainer/init-firewall.sh
  • .devcontainer/init-host.sh
  • .devcontainer/post-create.sh
  • .gitignore
  • README.md

@@ -0,0 +1 @@
/Users/ivanshumkov/.claude No newline at end of file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This file should not be committed — it contains a machine-specific path and exposes a developer's username.

This file contains a hardcoded personal path (/Users/ivanshumkov/.claude) which:

  1. Exposes personal information (username)
  2. Is machine-specific and won't work for other developers
  3. Appears to be generated during the init process

This file should be removed from the commit and added to .gitignore:

Proposed .gitignore addition
 # Devcontainer host-resolved symlink (machine-specific)
 .devcontainer/.main-git-resolved
 .devcontainer/.claude-host-config
+.devcontainer/.claude-config-resolved

Then remove this file from the repository:

git rm .devcontainer/.claude-config-resolved
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.devcontainer/.claude-config-resolved at line 1, Remove the machine-specific
file .devcontainer/.claude-config-resolved from the repository and stop tracking
it: add an appropriate ignore pattern (e.g.,
.devcontainer/.claude-config-resolved or a broader .devcontainer/*.resolved) to
.gitignore, remove the tracked file from git (git rm --cached or git rm as
appropriate), and commit the change so the personal path
(/Users/ivanshumkov/.claude) is not stored in the repo; ensure the
.devcontainer/.claude-config-resolved entry is present in .gitignore to prevent
future commits of that generated, user-specific file.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
.devcontainer/devcontainer.json (1)

74-77: Consider pinning the Git feature version for reproducible builds (Line 75).

Using "latest" for the version can cause non-deterministic environment drift across rebuilds. While supported by the feature (default is "os-provided"), pinning to a specific version ensures consistent environments and allows intentional updates.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.devcontainer/devcontainer.json around lines 74 - 77, The devcontainer
feature entry "ghcr.io/devcontainers/features/git:1" currently sets "version":
"latest", which can cause non-deterministic builds; update the "version"
property for that feature to a fixed, explicit tag (e.g., a specific semver or
release tag) instead of "latest" — change the value of the "version" key under
"ghcr.io/devcontainers/features/git:1" to a pinned version string so rebuilds
are reproducible.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In @.devcontainer/devcontainer.json:
- Around line 74-77: The devcontainer feature entry
"ghcr.io/devcontainers/features/git:1" currently sets "version": "latest", which
can cause non-deterministic builds; update the "version" property for that
feature to a fixed, explicit tag (e.g., a specific semver or release tag)
instead of "latest" — change the value of the "version" key under
"ghcr.io/devcontainers/features/git:1" to a pinned version string so rebuilds
are reproducible.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 82fff6a and 044c89d.

📒 Files selected for processing (3)
  • .devcontainer/devcontainer-build.json
  • .devcontainer/devcontainer.json
  • .github/workflows/prebuild-devcontainers.yml
💤 Files with no reviewable changes (2)
  • .devcontainer/devcontainer-build.json
  • .github/workflows/prebuild-devcontainers.yml

@lklimek lklimek self-requested a review March 2, 2026 12:14
# 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should copy whole .claude:

  • projects should have their per-project .claude configuration, settings, etc
  • I have some personal data in.claude like API keys for MCP servers, I don't want some process to copy that
  • credentials should not be put into a docker layer; env var like CLAUDE_CODE_OAUTH_TOKEN should do the job (or secret mounts if env var doesn't work)

shumkov and others added 4 commits March 4, 2026 22:06
…iner

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 <noreply@anthropic.com>
…h option

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
@shumkov shumkov requested a deployment to test-suite-approval March 6, 2026 04:53 — with GitHub Actions Waiting
@shumkov shumkov requested a deployment to test-suite-approval March 6, 2026 04:53 — with GitHub Actions Waiting
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
.devcontainer/init-host.sh (1)

13-22: ⚠️ Potential issue | 🟠 Major

Move Claude auth staging out of the workspace tree.

.devcontainer/devcontainer.json Lines 3-10 set build.context to .., so this staging directory sits inside the Docker build context. Docker uses .dockerignore—not .gitignore—to exclude paths from that context, and the legacy builder sends the full context to the daemon. Keeping host auth files under .devcontainer/ is still the wrong secret boundary even if the Dockerfile never copies them. Please stage these files outside the workspace and mount that host-only path explicitly instead. (containers.dev)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.devcontainer/init-host.sh around lines 13 - 22, The staging directory for
Claude auth is currently set to
CLAUDE_STAGING=".devcontainer/.claude-host-config" which places secrets inside
the workspace/Docker build context; change CLAUDE_STAGING to a host-only path
outside the repo (for example under "$HOME/.claude-host-config" or another
directory outside the repository root), update the mkdir/cp logic to create and
copy credentials into that new path (references: CLAUDE_STAGING variable, the cp
lines that copy "$HOME/.claude/.credentials.json" and "$HOME/.claude.json"), and
ensure whatever devcontainer/docker config mounts that host-only path into the
container instead of relying on files under .devcontainer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.devcontainer/init-host.sh:
- Around line 61-78: The script can leave a stale directory at RESOLVED so
subsequent ln -sfn calls create a symlink inside that directory instead of
replacing it; update the branches that call ln -sfn (the ones using MAIN_GIT and
"$(pwd)/.git") to remove any existing $RESOLVED first (e.g. test for existence
and rm -rf "$RESOLVED") before creating the symlink, and preserve the mkdir -p
"$RESOLVED" fallback branch for the no-git case so the mount still has a
directory when needed.

---

Duplicate comments:
In @.devcontainer/init-host.sh:
- Around line 13-22: The staging directory for Claude auth is currently set to
CLAUDE_STAGING=".devcontainer/.claude-host-config" which places secrets inside
the workspace/Docker build context; change CLAUDE_STAGING to a host-only path
outside the repo (for example under "$HOME/.claude-host-config" or another
directory outside the repository root), update the mkdir/cp logic to create and
copy credentials into that new path (references: CLAUDE_STAGING variable, the cp
lines that copy "$HOME/.claude/.credentials.json" and "$HOME/.claude.json"), and
ensure whatever devcontainer/docker config mounts that host-only path into the
container instead of relying on files under .devcontainer.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 60159fb0-0b2b-478d-bbe6-de1856d474a4

📥 Commits

Reviewing files that changed from the base of the PR and between 044c89d and b5c8168.

📒 Files selected for processing (8)
  • .devcontainer/.env.example
  • .devcontainer/.gitignore
  • .devcontainer/README.md
  • .devcontainer/devcontainer.json
  • .devcontainer/init-host.sh
  • .devcontainer/post-create.sh
  • .gitignore
  • README.md
✅ Files skipped from review due to trivial changes (1)
  • .devcontainer/.env.example
🚧 Files skipped from review as they are similar to previous changes (4)
  • .devcontainer/post-create.sh
  • README.md
  • .devcontainer/README.md
  • .gitignore

Comment on lines +61 to +78
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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n .devcontainer/init-host.sh | sed -n '61,78p'

Repository: dashpay/platform

Length of output: 775


🌐 Web query:

POSIX ln command behavior existing directory symlink -f flag

💡 Result:

POSIX ln when the destination is (or resolves to) an existing directory

POSIX rule: If the last operand names an existing directory, ln switches to its “target_dir” form and creates the link inside that directory, using the basename of the source as the new link name. It does not replace the directory. [1]

Example (POSIX semantics):

  • ln -s /src/path existing_dir
    → creates existing_dir/path (a symlink whose name is path). [1]

What -f does (and does not do) in POSIX

With -f, if the destination path exists, ln performs an action equivalent to calling unlink(destination) before creating the link. If that unlink()-equivalent step fails, ln must diagnose the error and skip that operand. [1]

Implication:

  • Because unlink() does not remove directories, ln -f is not a portable way to “replace an existing directory with a symlink”. In POSIX terms, if the destination is an existing directory, you’re in the “put it in that directory” case, and -f doesn’t change that. [1]

Common non-POSIX behavior you may be seeing (GNU / others)

Many implementations (e.g., GNU coreutils) also treat a destination that is a symlink to a directory as a directory by default—so ln -sf TARGET link_to_dir may create the new link inside the referenced directory rather than replacing the symlink itself. GNU provides -n and/or -T to prevent that and allow replacing the symlink-as-an-entry. [2]

Portable takeaway: POSIX guarantees “existing directory ⇒ create link inside it”; replacing a directory (or reliably replacing a symlink-to-directory itself) is not specified portably by POSIX ln -f alone. [1][2]

Sources: [1] POSIX ln specification (Issue 7 / POSIX.1-2017 text) via ManKier; [2] GNU coreutils ln documentation (-n, -T, directory/symlink-to-directory handling).


🏁 Script executed:

wc -l .devcontainer/init-host.sh

Repository: dashpay/platform

Length of output: 89


Recreate .main-git-resolved instead of reusing mixed dir/symlink state.

Lines 69 and 75 use ln -sfn targeting $RESOLVED. If $RESOLVED already exists as a directory (from a previous run), POSIX ln will create the symlink inside that directory rather than replace it—the directory itself remains and the bind mount points to the wrong location. On subsequent runs, the state doesn't recover. Remove $RESOLVED first to ensure a clean state.

Proposed fix
 RESOLVED=".devcontainer/.main-git-resolved"
+rm -rf "$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"
+        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"
 fi
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.devcontainer/init-host.sh around lines 61 - 78, The script can leave a
stale directory at RESOLVED so subsequent ln -sfn calls create a symlink inside
that directory instead of replacing it; update the branches that call ln -sfn
(the ones using MAIN_GIT and "$(pwd)/.git") to remove any existing $RESOLVED
first (e.g. test for existence and rm -rf "$RESOLVED") before creating the
symlink, and preserve the mkdir -p "$RESOLVED" fallback branch for the no-git
case so the mount still has a directory when needed.

[ -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" ] && \
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

● 🔍 Sensitive Info in ~/.claude.json

  1. Account identity block — email address, display name, account UUID, organization UUID, billing type, and account/subscription creation dates — all in plaintext under oauthAccount
  2. User ID hash — a stable SHA-256 hash that acts as a persistent fingerprint across sessions
  3. Session IDs — dozens of UUIDs across every project, forming a detailed activity timeline
  4. Project paths & usage metrics — full local filesystem paths to every project with per-session token consumption stats, revealing what you work on, how much, and how recently
  5. Feature flags & A/B test assignments — cachedGrowthBookFeatures with ~30+ flags, exposing which experimental features your account is enrolled in

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure you want plugin-dev plugin inside the devcontainer. It's polluting context. I'd prefer using dedicated $PROJECT/.claude/settings.json instead.

It's minor issue. You can ignore.

[ -f "$ENV_FILE" ] && source "$ENV_FILE"

if [ -n "$CLAUDE_AGENTS" ]; then
mkdir -p "$CLAUDE_STAGING/agents"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, per-project agents should be used here. It's minor issue.

done
fi

if [ -n "$CLAUDE_SKILLS" ]; then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above.

It's minor issue.

@@ -0,0 +1,79 @@
#!/bin/bash
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about GITHUB_TOKEN / GH_TOKEN ? Should the container have access to git repos?

I don't know, just asking; my claude does have access there, without token github has quite low hourly request quota so even clean build sometimes fails.

If we want to provide a token, maybe get a short-lived (1 day?) scoped personal access token, read only, api generated?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In review / testing

Development

Successfully merging this pull request may close these issues.

2 participants