Skip to content

Commit d645eb3

Browse files
authored
Add claude sandbox (#337)
Three new copier questions gate a sandboxed Claude Code setup: add_claude (top-level — env var blockers, ~/.claude bind mount, Claude Code CLI install, postCreate/postStart hooks, node, justfile), install_gh and install_glab (each gated on add_claude). Breaks the template/.devcontainer symlink so devcontainer.json can be Jinja-conditional. The meta repo's own .devcontainer/devcontainer.json and Dockerfile become the add_claude=no baseline; a new test_meta_matches_no_claude_template drift test enforces it. Adds remote.autoForwardPorts: false and explicit forwardPorts: [8000] so VS Code stops stealing sphinx-autobuild's port on restart (separate commit).
2 parents 1dfadc8 + 5b874f9 commit d645eb3

11 files changed

Lines changed: 261 additions & 6 deletions

.devcontainer/devcontainer.json

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
"remoteEnv": {
99
// Allow X11 apps to run inside the container
1010
"DISPLAY": "${localEnv:DISPLAY}",
11+
// Mark this shell as running inside the devcontainer
12+
"IN_DEVCONTAINER": "1",
1113
// Put things that allow it in the persistent cache
1214
"PRE_COMMIT_HOME": "/cache/pre-commit",
1315
"UV_CACHE_DIR": "/cache/uv",
@@ -29,7 +31,10 @@
2931
"python.terminal.activateEnvironment": false,
3032
// Workaround to prevent garbled python REPL in the terminal
3133
// https://github.com/microsoft/vscode-python/issues/25505
32-
"python.terminal.shellIntegration.enabled": false
34+
"python.terminal.shellIntegration.enabled": false,
35+
// Only forward explicitly listed ports — auto-detection races with
36+
// sphinx-autobuild and steals the port on restart
37+
"remote.autoForwardPorts": false
3338
},
3439
// Add the IDs of extensions you want installed when the container is created.
3540
"extensions": [
@@ -43,7 +48,11 @@
4348
]
4449
}
4550
},
46-
// Create the config folder for the bash-config feature and uv cache
51+
// Explicitly forward sphinx-autobuild port (auto-detection disabled above)
52+
"forwardPorts": [
53+
8000
54+
],
55+
// Create host-side dirs needed for bind mounts before the container starts
4756
"initializeCommand": "mkdir -p ${localEnv:HOME}/.config/terminal-config",
4857
"runArgs": [
4958
// Allow the container to access the host X11 display and EPICS CA

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
# The developer stage is used as a devcontainer including dev versions
2-
# of the build dependencies
1+
# The devcontainer should use the developer target and run as root with podman
2+
# or docker with user namespaces.
33
FROM ghcr.io/diamondlightsource/ubuntu-devcontainer:noble AS developer
44

55
# Add any system dependencies for the developer/build environment here

copier.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,30 @@ docker_debug:
113113
be useful if debugging the service inside of the cluster
114114
infrastructure is required.
115115
116+
add_claude:
117+
type: bool
118+
help: |
119+
Add a Claude Code sandbox to the devcontainer?
120+
Disables host SSH agent / VS Code git credential injection inside
121+
the container, mounts ~/.claude from the host, installs Claude Code
122+
CLI, and enables `--dangerously-skip-permissions` autopilot mode.
123+
124+
install_gh:
125+
type: bool
126+
when: "{{ add_claude }}"
127+
help: |
128+
Install the GitHub CLI (gh) so Claude can push/pull via PAT auth?
129+
Only useful inside the Claude sandbox — ordinary users typically
130+
rely on SSH keys or VS Code git credentials.
131+
132+
install_glab:
133+
type: bool
134+
when: "{{ add_claude }}"
135+
help: |
136+
Install the GitLab CLI (glab) for projects that talk to a GitLab
137+
instance (e.g. gitlab.diamond.ac.uk submodules)?
138+
Only useful inside the Claude sandbox.
139+
116140
docs_type:
117141
type: str
118142
help: |

example-answers.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ description: An expanded https://github.com/DiamondLightSource/python-copier-tem
77
distribution_name: dls-python-copier-template-example
88
docker: true
99
docker_debug: true
10+
add_claude: true
11+
install_gh: true
12+
install_glab: true
1013
docs_type: sphinx
1114
git_platform: github.com
1215
github_org: DiamondLightSource

template/.devcontainer

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// For format details, see https://containers.dev/implementors/json_reference/
2+
{
3+
"name": "Python 3 Developer Container",
4+
"build": {
5+
"dockerfile": "../Dockerfile",
6+
"target": "developer"
7+
},{% if add_claude %}
8+
// Tell VS Code the remote user is root so copyGitConfig writes to
9+
// /root/.gitconfig (matching $HOME in the shell); otherwise it falls back
10+
// to the base image's USER and the copy lands in the wrong home.
11+
"remoteUser": "root",{% endif %}
12+
"remoteEnv": {
13+
// Allow X11 apps to run inside the container
14+
"DISPLAY": "${localEnv:DISPLAY}",{% if add_claude %}
15+
// Disable SSH agent forwarding — prevents Claude from using host SSH keys
16+
"SSH_AUTH_SOCK": "",
17+
// Disable VS Code git credential injection — prevents askpass from
18+
// relaying host GitHub credentials into the container over the IPC socket
19+
"GIT_ASKPASS": "",
20+
"VSCODE_GIT_IPC_HANDLE": "",
21+
"VSCODE_GIT_ASKPASS_MAIN": "",
22+
"VSCODE_GIT_ASKPASS_NODE": "",
23+
"VSCODE_GIT_ASKPASS_EXTRA_ARGS": "",{% endif %}
24+
// Mark this shell as running inside the devcontainer
25+
"IN_DEVCONTAINER": "1",
26+
// Put things that allow it in the persistent cache
27+
"PRE_COMMIT_HOME": "/cache/pre-commit",
28+
"UV_CACHE_DIR": "/cache/uv",
29+
"UV_PYTHON_CACHE_DIR": "/cache/uv-python",
30+
// Make a venv that is specific for this workspace path as the cache is shared
31+
"UV_PROJECT_ENVIRONMENT": "/cache/venv-for${localWorkspaceFolder}",
32+
// Do the equivalent of "activate" the venv so we don't have to "uv run" everything
33+
"VIRTUAL_ENV": "/cache/venv-for${localWorkspaceFolder}",
34+
"PATH": "/cache/venv-for${localWorkspaceFolder}/bin:${containerEnv:PATH}"
35+
},
36+
"customizations": {
37+
"vscode": {
38+
// Set *default* container specific settings.json values on container create.
39+
"settings": {
40+
// Use the container's python by default
41+
"python.defaultInterpreterPath": "/cache/venv-for${localWorkspaceFolder}/bin/python",
42+
// Don't activate the venv as it is already in the PATH
43+
"python.terminal.activateEnvInCurrentTerminal": false,
44+
"python.terminal.activateEnvironment": false,
45+
// Workaround to prevent garbled python REPL in the terminal
46+
// https://github.com/microsoft/vscode-python/issues/25505
47+
"python.terminal.shellIntegration.enabled": false{% if sphinx %},
48+
// Only forward explicitly listed ports — auto-detection races with
49+
// sphinx-autobuild and steals the port on restart
50+
"remote.autoForwardPorts": false{% endif %}
51+
},
52+
// Add the IDs of extensions you want installed when the container is created.
53+
"extensions": [
54+
"ms-python.python",
55+
"github.vscode-github-actions",
56+
"tamasfe.even-better-toml",
57+
"redhat.vscode-yaml",
58+
"ryanluker.vscode-coverage-gutters",
59+
"charliermarsh.ruff",
60+
"ms-azuretools.vscode-docker"{% if add_claude %},
61+
"anthropic.claude-code"{% endif %}
62+
]
63+
}
64+
},{% if sphinx %}
65+
// Explicitly forward sphinx-autobuild port (auto-detection disabled above)
66+
"forwardPorts": [
67+
8000
68+
],{% endif %}
69+
// Create host-side dirs needed for bind mounts before the container starts
70+
"initializeCommand": "mkdir -p ${localEnv:HOME}/.config/terminal-config{% if add_claude %} ${localEnv:HOME}/.claude{% endif %}",
71+
"runArgs": [
72+
// Allow the container to access the host X11 display and EPICS CA
73+
"--net=host",
74+
// Make sure SELinux does not disable with access to host filesystems like tmp
75+
"--security-opt=label=disable"
76+
],
77+
"mounts": [
78+
// Mount in the user terminal config folder so it can be edited
79+
{
80+
"source": "${localEnv:HOME}/.config/terminal-config",
81+
"target": "/user-terminal-config",
82+
"type": "bind"
83+
},
84+
// Keep a persistent cross container cache for uv, pre-commit, and the venvs
85+
{
86+
"source": "devcontainer-shared-cache",
87+
"target": "/cache",
88+
"type": "volume"
89+
}{% if install_gh %},
90+
// Persist gh auth across container rebuilds with per-repo scoped PAT
91+
{
92+
"source": "gh-auth-${localWorkspaceFolderBasename}",
93+
"target": "/root/.config/gh",
94+
"type": "volume"
95+
}{% endif %}{% if install_glab %},
96+
// Persist glab auth across container rebuilds (GitLab CLI)
97+
{
98+
"source": "glab-auth-${localWorkspaceFolderBasename}",
99+
"target": "/root/.config/glab-cli",
100+
"type": "volume"
101+
}{% endif %}{% if add_claude %},
102+
// Mount Claude config from host (settings, memory, skills)
103+
{
104+
"source": "${localEnv:HOME}/.claude",
105+
"target": "/root/.claude",
106+
"type": "bind"
107+
}{% endif %}
108+
],
109+
// Mount the parent as /workspaces so we can pip install peers as editable
110+
"workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind",{% if add_claude %}
111+
"postCreateCommand": ".devcontainer/postCreate.sh",
112+
"postStartCommand": ".devcontainer/postStart.sh"{% else %}
113+
// After the container is created, recreate the venv then make pre-commit first run faster
114+
"postCreateCommand": "uv venv --clear && uv sync && pre-commit install --install-hooks"{% endif %}
115+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
# Install Claude Code CLI
5+
curl -fsSL https://claude.ai/install.sh | bash
6+
7+
# Install Python dependencies and pre-commit hooks
8+
uv venv --clear
9+
uv sync
10+
pre-commit install --install-hooks
11+
12+
# Initialise git submodules if any are declared
13+
[ -f .gitmodules ] && git submodule update --init || true
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
# Wipe any credential helpers and SSH URL rewrites injected by VS Code's
5+
# Dev Containers extension when it copies the host gitconfig. An empty-string
6+
# value resets the helper list so only an explicit PAT via `just gh-auth`
7+
# can authenticate to remotes.
8+
git config --global credential.helper ''
9+
git config --global --unset-all url.ssh://git@github.com/.insteadOf 2>/dev/null || true
10+
11+
# Force all SSH-style remotes to use HTTPS so the gh/glab credential helpers
12+
# handle auth. This keeps the container SSH-key-free (Claude stays sandboxed)
13+
# while still allowing push/pull on repos whose remotes are set to git@...:.
14+
git config --global url."https://github.com/".insteadOf "git@github.com:"
15+
{%- if install_glab %}
16+
git config --global url."https://gitlab.diamond.ac.uk/".insteadOf "git@gitlab.diamond.ac.uk:"
17+
{%- endif %}
18+
19+
{% if install_gh -%}
20+
# If gh CLI has cached credentials (survive container rebuild), re-register
21+
# its git credential helper so HTTPS remotes authenticate automatically.
22+
if gh auth status &>/dev/null; then
23+
gh auth setup-git
24+
fi
25+
{%- endif %}

template/Dockerfile.jinja

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,29 @@ FROM ghcr.io/diamondlightsource/ubuntu-devcontainer:noble AS developer
55
# Add any system dependencies for the developer/build environment here
66
RUN apt-get update -y && apt-get install -y --no-install-recommends \
77
graphviz \
8-
&& apt-get dist-clean{% if docker %}
8+
&& apt-get dist-clean{% if add_claude %}
9+
10+
# Node is required by Claude Code's hook runtime
11+
RUN apt-get update -y && apt-get install -y --no-install-recommends \
12+
nodejs \
13+
&& apt-get dist-clean{% endif %}{% if install_gh %}
14+
15+
# GitHub CLI — used by Claude to authenticate to github.com via PAT
16+
RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | \
17+
dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && \
18+
chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && \
19+
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \
20+
| tee /etc/apt/sources.list.d/github-cli.list > /dev/null && \
21+
apt-get update && apt-get install -y --no-install-recommends gh && \
22+
apt-get dist-clean{% endif %}{% if install_glab %}
23+
24+
# GitLab CLI — used by Claude to authenticate to gitlab instances via PAT.
25+
# No apt repo, so install from the upstream release tarball.
26+
ARG GLAB_VERSION=1.92.1
27+
RUN curl -fsSL "https://gitlab.com/gitlab-org/cli/-/releases/v${GLAB_VERSION}/downloads/glab_${GLAB_VERSION}_linux_amd64.tar.gz" \
28+
| tar -xz -C /tmp bin/glab && \
29+
install -m 0755 /tmp/bin/glab /usr/local/bin/glab && \
30+
rm -rf /tmp/bin{% endif %}{% if docker %}
931

1032
# The build stage installs the context into the venv
1133
FROM developer AS build
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Start Claude Code in sandbox mode (no SSH agent, skip permission prompts)
2+
claude:
3+
SSH_AUTH_SOCK= IS_SANDBOX=1 claude --dangerously-skip-permissions{% if install_gh %}
4+
5+
6+
# Authenticate gh CLI with a GitHub PAT (token not stored in shell history)
7+
gh-auth:
8+
#!/bin/bash
9+
read -sp "GitHub PAT: " t && echo
10+
echo "$t" | gh auth login --with-token
11+
unset t
12+
gh auth setup-git
13+
gh auth status{% endif %}{% if install_glab %}
14+
15+
16+
# Authenticate glab CLI with a GitLab PAT (token not stored in shell history).
17+
# --git-protocol https prevents glab's SSH insteadOf rewrite.
18+
glab-auth hostname="gitlab.com":
19+
#!/bin/bash
20+
read -sp "GitLab PAT for {{ '{{' }} hostname {{ '}}' }}: " t && echo
21+
echo "$t" | glab auth login --stdin --hostname {{ '{{' }} hostname {{ '}}' }} --git-protocol https
22+
unset t
23+
glab auth status{% endif %}

0 commit comments

Comments
 (0)