Skip to content

Commit 84de11e

Browse files
Merge pull request #4 from runtimeverification/raoul/devcontainer
Add devcontainer for sandboxed Claude Code development
2 parents d6fd0d7 + 32db092 commit 84de11e

7 files changed

Lines changed: 756 additions & 2 deletions

File tree

.devcontainer/Dockerfile

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# Microsoft's official devcontainer base (Ubuntu 24.04), pinned by digest for
2+
# reproducibility. It ships a non-root `vscode` user (UID 1000) with passwordless
3+
# sudo plus the common dev tools (git, curl, sudo, ca-certificates, openssh-client),
4+
# so no manual user creation is needed here.
5+
FROM mcr.microsoft.com/devcontainers/base:ubuntu24.04@sha256:4bcb1b466771b1ba1ea110e2a27daea2f6093f9527fb75ee59703ec89b5561cb
6+
7+
ENV DEBIAN_FRONTEND=noninteractive \
8+
LANG=C.UTF-8 \
9+
LC_ALL=C.UTF-8
10+
11+
# The base already provides git/sudo/ca-certificates/curl/openssh-client; we only
12+
# strictly need xz-utils for the Nix installer tarball. The rest are listed
13+
# defensively (apt is idempotent and skips already-present packages).
14+
RUN apt-get update && apt-get install -y --no-install-recommends \
15+
git sudo ca-certificates curl less xz-utils openssh-client \
16+
&& rm -rf /var/lib/apt/lists/*
17+
18+
# Pre-create /nix owned by the vscode user and a system-wide nix.conf. Two things to note:
19+
# - Sandboxing relies on CAP_SYS_ADMIN (sethostname etc.), which container
20+
# builds don't grant — without `sandbox = false` both the Nix self-install
21+
# and every subsequent `nix profile install` fail.
22+
# - The K Framework binary caches let `kup install k` pull prebuilt K binaries
23+
# instead of compiling from source. Single-user Nix (vscode owns /nix) honors
24+
# these substituters from the system config, so no per-user nix.conf needed.
25+
RUN mkdir -m 0755 /nix \
26+
&& chown vscode:vscode /nix \
27+
&& mkdir -p /etc/nix \
28+
&& printf '%s\n' \
29+
'sandbox = false' \
30+
'filter-syscalls = false' \
31+
'experimental-features = nix-command flakes' \
32+
'substituters = https://cache.nixos.org https://k-framework.cachix.org https://k-framework-binary.cachix.org' \
33+
'trusted-public-keys = cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY= k-framework.cachix.org-1:jeyMXB2h28gpNRjuVkehg+zLj62ma1RnyyopA/20yFE= k-framework-binary.cachix.org-1:pJedQ8iG19BW3v/DMMmiRVtwRBGO3fyMv2Ws0OpBADs=' \
34+
> /etc/nix/nix.conf
35+
36+
USER vscode
37+
WORKDIR /home/vscode
38+
39+
# Single-user Nix install — pinned to a specific release for reproducibility
40+
# and supply-chain safety. Update the version when upgrading Nix.
41+
ARG NIX_VERSION=2.28.3
42+
RUN curl -fsSL "https://releases.nixos.org/nix/nix-${NIX_VERSION}/install" -o /tmp/nix-install \
43+
&& sh /tmp/nix-install --no-daemon --no-channel-add \
44+
&& rm /tmp/nix-install
45+
46+
ENV PATH=/home/vscode/.nix-profile/bin:/home/vscode/.npm-global/bin:$PATH
47+
48+
# Point the `nixpkgs` flake registry at the nixos-25.05 release branch (matching
49+
# the project's flake.nix) — a stable channel, though not a frozen revision; it
50+
# only governs the container-level bootstrap tooling below, while the project
51+
# toolchain is pinned reproducibly via flake.nix/flake.lock. Then install only
52+
# that container-level bootstrap/runtime tooling via Nix:
53+
# - direnv + nix-direnv → auto-load and cache the flake's `nix develop` shell on
54+
# entry to /workspace; this is what actually provides the
55+
# project toolchain (python, uv, make, wabt, K Framework),
56+
# so those live in flake.nix's devShell, not here.
57+
# - nodejs_22 → host for the Claude Code CLI (run as `claude` from
58+
# anywhere, so it stays globally on PATH, not in the shell)
59+
RUN . /home/vscode/.nix-profile/etc/profile.d/nix.sh \
60+
&& nix registry add nixpkgs github:NixOS/nixpkgs/nixos-25.05 \
61+
&& nix profile install \
62+
nixpkgs#direnv \
63+
nixpkgs#nix-direnv \
64+
nixpkgs#nodejs_22
65+
66+
# Claude Code CLI — installed to a per-user prefix so root isn't required.
67+
RUN . /home/vscode/.nix-profile/etc/profile.d/nix.sh \
68+
&& npm config set prefix /home/vscode/.npm-global \
69+
&& npm install -g @anthropic-ai/claude-code@2.1.175
70+
71+
# Wire up nix-direnv, then hook direnv into interactive shells. Order matters:
72+
# Nix must be sourced before the direnv hook so the profile PATH is in place when
73+
# direnv evaluates the flake.
74+
RUN mkdir -p /home/vscode/.config/direnv \
75+
&& echo 'source $HOME/.nix-profile/share/nix-direnv/direnvrc' > /home/vscode/.config/direnv/direnvrc \
76+
&& printf '%s\n' \
77+
'. /home/vscode/.nix-profile/etc/profile.d/nix.sh' \
78+
'eval "$(direnv hook bash)"' \
79+
>> /home/vscode/.bashrc
80+
81+
WORKDIR /workspace

.devcontainer/devcontainer.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "komet_node",
3+
"build": {
4+
"dockerfile": "Dockerfile"
5+
},
6+
"remoteUser": "vscode",
7+
"workspaceFolder": "/workspace",
8+
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind",
9+
"init": true,
10+
"mounts": [
11+
"source=claude-code-config-${devcontainerId},target=/home/vscode/.claude,type=volume",
12+
"source=${localEnv:HOME}${localEnv:USERPROFILE}/.gitconfig,target=/home/vscode/.gitconfig,type=bind,readonly"
13+
],
14+
"containerEnv": {
15+
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude",
16+
"GIT_CONFIG_GLOBAL": "/home/vscode/.gitconfig.local"
17+
},
18+
"postCreateCommand": "sudo chown -R vscode:vscode /home/vscode/.claude && bash .devcontainer/setup-git.sh && direnv allow /workspace && nix develop /workspace --command bash -c 'uv sync --frozen'",
19+
"customizations": {
20+
"vscode": {
21+
"extensions": [
22+
"anthropic.claude-code",
23+
"mkhl.direnv"
24+
]
25+
}
26+
}
27+
}

.devcontainer/setup-git.sh

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/usr/bin/env bash
2+
# Wire up a writable global git config that inherits the host user's identity.
3+
#
4+
# devcontainer.json bind-mounts the host ~/.gitconfig read-only at
5+
# /home/vscode/.gitconfig and sets GIT_CONFIG_GLOBAL=/home/vscode/.gitconfig.local.
6+
# Git therefore writes global config to the (writable) .gitconfig.local while
7+
# still reading the host's name/email/aliases through the include set up below.
8+
#
9+
# The result: every user of this container can `git commit` with their own host
10+
# identity and run `git config --global ...` inside the container, without the
11+
# EBUSY breakage a directly-writable bind mount over ~/.gitconfig would cause.
12+
#
13+
# Adapted from trailofbits/claude-code-devcontainer's post_install.py. Invoked
14+
# from devcontainer.json's postCreateCommand. Idempotent — safe to re-run.
15+
set -euo pipefail
16+
17+
host_gitconfig="/home/vscode/.gitconfig"
18+
local_gitconfig="/home/vscode/.gitconfig.local"
19+
20+
# Ensure the writable global config exists without truncating it — a previous run
21+
# (or the user) may have written real settings here (e.g. core.editor), and this
22+
# script must not clobber them.
23+
touch "${local_gitconfig}"
24+
25+
if [ -f "${host_gitconfig}" ]; then
26+
# Host provided a ~/.gitconfig (bind-mounted as a regular file): inherit it.
27+
# Set just the include.path key via `git config` so any other keys the user
28+
# already wrote to the global config are preserved. --replace-all collapses
29+
# the entry to a single value, keeping the operation idempotent across re-runs.
30+
git config --file "${local_gitconfig}" --replace-all include.path "${host_gitconfig}"
31+
echo "setup-git: global config includes host identity from ${host_gitconfig}"
32+
else
33+
# No host ~/.gitconfig — the missing bind source is materialized as an empty
34+
# directory, which must NOT be included. Drop any stale include.path we may
35+
# have added on an earlier run, but leave every other key untouched.
36+
git config --file "${local_gitconfig}" --unset-all include.path 2>/dev/null || true
37+
echo "setup-git: no host ~/.gitconfig found; using empty ${local_gitconfig}"
38+
echo "setup-git: set your identity with 'git config --global user.name ...' / user.email"
39+
fi

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use flake

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/dist/
22
__pycache__/
33
.coverage
4+
.direnv/

0 commit comments

Comments
 (0)