Skip to content

Commit f2bcf25

Browse files
committed
fix(claude-sandbox): close VS Code credential helper leak
VS Code's Dev Containers extension re-injects a /tmp credential bridge after postStart runs, allowing host GitHub PATs to leak into the container even with VSCODE_GIT_IPC_HANDLE blanked. Fix by: - Use --unset-all (not =) for credential.helper, so the multi-valued entry VS Code writes is actually cleared. - Remove /tmp/vscode-remote-containers-*.js so the bridge cannot answer even if a stale helper survives. - Pin per-host helpers to command -v gh / glab so a stale host path (/usr/local/bin/gh) doesn't fall through to the next helper. - Re-run cleanup on postAttachCommand because VS Code injects after postStartCommand has already finished. Also: - Install just explicitly when add_claude (recipes need it) - Bump glab to 1.93.0 - Add CLAUDE.md describing sandbox boundaries and intentional exposures (NFS-mounted ~/.claude, /workspaces parent bind, --net=host) - Link CLAUDE.md from README
1 parent 5b874f9 commit f2bcf25

11 files changed

Lines changed: 388 additions & 16 deletions

File tree

template/.devcontainer/devcontainer.json.jinja

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,11 @@
109109
// Mount the parent as /workspaces so we can pip install peers as editable
110110
"workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind",{% if add_claude %}
111111
"postCreateCommand": ".devcontainer/postCreate.sh",
112-
"postStartCommand": ".devcontainer/postStart.sh"{% else %}
112+
"postStartCommand": ".devcontainer/postStart.sh",
113+
// VS Code's Dev Containers extension re-injects its credential bridge
114+
// when the editor attaches — after postStart has already run. Re-run
115+
// the cleanup at attach so the leak is closed before any git operation.
116+
"postAttachCommand": ".devcontainer/postStart.sh"{% else %}
113117
// After the container is created, recreate the venv then make pre-commit first run faster
114118
"postCreateCommand": "uv venv --clear && uv sync && pre-commit install --install-hooks"{% endif %}
115119
}
Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,28 @@
11
#!/bin/bash
22
set -euo pipefail
33

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
4+
# Wipe any credential helpers and SSH URL rewrites that VS Code's Dev
5+
# Containers extension injects when it copies the host gitconfig and
6+
# spawns its own credential bridge. We need --unset-all (not =''),
7+
# because VS Code stores the helper as a single multi-valued line that
8+
# `git config <key> <value>` only replaces if there is a single value.
9+
# IMPORTANT: VS Code writes its credential.helper to /etc/gitconfig
10+
# (system scope), not ~/.gitconfig — so the system scope must also be
11+
# cleared, otherwise the helper still runs.
12+
for scope in --system --global; do
13+
git config $scope --unset-all credential.helper 2>/dev/null || true
14+
git config $scope --unset-all credential.https://github.com.helper 2>/dev/null || true
15+
{%- if install_glab %}
16+
git config $scope --unset-all credential.https://gitlab.diamond.ac.uk.helper 2>/dev/null || true
17+
{%- endif %}
18+
git config $scope --unset-all url.ssh://git@github.com/.insteadOf 2>/dev/null || true
19+
done
20+
21+
# VS Code drops a Node-based credential bridge in /tmp that talks back
22+
# to the host over a named pipe — even with VSCODE_GIT_IPC_HANDLE blank
23+
# it can still surface host PATs. Remove it so any stale `credential.helper`
24+
# entries cannot fall through to it.
25+
rm -f /tmp/vscode-remote-containers-*.js
1026

1127
# Force all SSH-style remotes to use HTTPS so the gh/glab credential helpers
1228
# handle auth. This keeps the container SSH-key-free (Claude stays sandboxed)
@@ -17,9 +33,22 @@ git config --global url."https://gitlab.diamond.ac.uk/".insteadOf "git@gitlab.di
1733
{%- endif %}
1834

1935
{% if install_gh -%}
36+
# Pin per-host helper to the in-container gh path. The host gitconfig may
37+
# reference /usr/local/bin/gh which doesn't exist here (apt installs to
38+
# /usr/bin/gh); without this, git falls through to the next helper.
39+
if command -v gh >/dev/null; then
40+
git config --global credential.https://github.com.helper "!$(command -v gh) auth git-credential"
41+
fi
42+
2043
# If gh CLI has cached credentials (survive container rebuild), re-register
2144
# its git credential helper so HTTPS remotes authenticate automatically.
2245
if gh auth status &>/dev/null; then
2346
gh auth setup-git
2447
fi
2548
{%- endif %}
49+
{% if install_glab %}
50+
# Pin per-host helper to the in-container glab path.
51+
if command -v glab >/dev/null; then
52+
git config --global credential.https://gitlab.diamond.ac.uk.helper "!$(command -v glab) auth git-credential"
53+
fi
54+
{%- endif %}

template/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,7 @@ lockfiles/
6969

7070
# ruff cache
7171
.ruff_cache/
72+
73+
# Claude Code local state (commit settings.json, commands, skills, hooks)
74+
.claude/settings.local.json
75+
.claude/scheduled_tasks.lock

template/Dockerfile.jinja

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ RUN apt-get update -y && apt-get install -y --no-install-recommends \
77
graphviz \
88
&& apt-get dist-clean{% if add_claude %}
99

10-
# Node is required by Claude Code's hook runtime
10+
# Node is required by Claude Code's hook runtime; just powers the
11+
# container's claude/gh-auth/glab-auth recipes in justfile.
1112
RUN apt-get update -y && apt-get install -y --no-install-recommends \
1213
nodejs \
14+
just \
1315
&& apt-get dist-clean{% endif %}{% if install_gh %}
1416

1517
# GitHub CLI — used by Claude to authenticate to github.com via PAT
@@ -23,7 +25,7 @@ RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | \
2325

2426
# GitLab CLI — used by Claude to authenticate to gitlab instances via PAT.
2527
# No apt repo, so install from the upstream release tarball.
26-
ARG GLAB_VERSION=1.92.1
28+
ARG GLAB_VERSION=1.93.0
2729
RUN curl -fsSL "https://gitlab.com/gitlab-org/cli/-/releases/v${GLAB_VERSION}/downloads/glab_${GLAB_VERSION}_linux_amd64.tar.gz" \
2830
| tar -xz -C /tmp bin/glab && \
2931
install -m 0755 /tmp/bin/glab /usr/local/bin/glab && \

template/README.md.jinja

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010
This is where you should write a short paragraph that describes what your module does,
1111
how it does it, and why people should use it.
1212

13-
{# #}What | Where
14-
{# #}:---: | :---:
15-
{# #}Source | <{{repo_url}}>
16-
{% if pypi %}PyPI | `pip install {{distribution_name}}`
17-
{% endif %}{% if docker %}Docker | `docker run ghcr.io/{{github_org | lower}}/{{repo_name}}:latest`
18-
{% endif %}{% if sphinx %}Documentation | <{{docs_url}}>
19-
{% endif %}Releases | <{{repo_url}}/releases>
13+
{# #}What | Where
14+
{# #}:---: | :---:
15+
{# #}Source | <{{repo_url}}>
16+
{% if pypi %}PyPI | `pip install {{distribution_name}}`
17+
{% endif %}{% if docker %}Docker | `docker run ghcr.io/{{github_org | lower}}/{{repo_name}}:latest`
18+
{% endif %}{% if sphinx %}Documentation | <{{docs_url}}>
19+
{% endif %}{% if add_claude %}Claude sandbox | [README-CLAUDE.md](./README-CLAUDE.md)
20+
{% endif %}Releases | <{{repo_url}}/releases>
2021

2122
This is where you should put some images or code snippets that illustrate
2223
some relevant examples. If it is a library then you might put some
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
description: Save current task state to auto-memory, then promote reusable lessons to skills and trim memory.
3+
---
4+
5+
# Memo
6+
7+
Save a snapshot of current work to persistent memory, then clean up.
8+
9+
## Step 1 — Save current state
10+
11+
Write a concise summary of in-progress or recently completed work to the
12+
auto-memory `MEMORY.md` for this project. Include:
13+
14+
- What was done (feature, bug, refactor, area of code)
15+
- Current status (completed, blocked, in-progress)
16+
- Key decisions or outcomes worth remembering across conversations
17+
18+
Do not duplicate information already in skills, CLAUDE.md, or README-CLAUDE.md.
19+
20+
## Step 2 — Promote to skills
21+
22+
Review the memory file for items that represent **reusable patterns or
23+
lessons** — things that would help future sessions on this project. For
24+
each such item:
25+
26+
1. Identify which skill file it belongs in (or create a new one under
27+
`.claude/skills/<name>/SKILL.md`).
28+
2. Add it to the appropriate skill.
29+
3. Remove it from memory (it now lives in the skill).
30+
31+
Examples of promotable items:
32+
- A non-obvious convention specific to this project
33+
- A "foot-gun" pattern worth warning future-you about
34+
- A reusable recipe (test invocation, deploy command, debugging trick)
35+
36+
## Step 3 — Trim memory
37+
38+
Remove from memory anything that is:
39+
- Already captured in skills, CLAUDE.md, or README-CLAUDE.md
40+
- Too specific to a single completed task to be useful again
41+
- Stale or superseded by later work
42+
43+
Keep memory concise — ideally under 30 lines.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#!/bin/bash
2+
# UserPromptSubmit hook: verify the Claude sandbox is intact before
3+
# executing any prompt. Exit code 2 blocks the prompt and shows the
4+
# message to the user. See README-CLAUDE.md for the full sandbox model.
5+
6+
fail() { echo "BLOCKED: $1" >&2; exit 2; }
7+
8+
# Are we in the devcontainer at all?
9+
[ -n "${IN_DEVCONTAINER:-}" ] || \
10+
fail "not in the devcontainer (IN_DEVCONTAINER unset). Reopen the project in the devcontainer."
11+
12+
# Host SSH agent must not be reachable.
13+
[ -z "${SSH_AUTH_SOCK:-}" ] || \
14+
fail "SSH_AUTH_SOCK is set ($SSH_AUTH_SOCK) — host SSH agent is reachable."
15+
16+
# VS Code git credential bridge must be silenced.
17+
[ -z "${VSCODE_GIT_IPC_HANDLE:-}" ] || \
18+
fail "VSCODE_GIT_IPC_HANDLE is set — VS Code credential bridge is reachable."
19+
[ -z "${GIT_ASKPASS:-}" ] || \
20+
fail "GIT_ASKPASS is set — VS Code askpass is injected."
21+
22+
# The /tmp credential helper script VS Code drops in must have been removed.
23+
if compgen -G '/tmp/vscode-remote-containers-*.js' >/dev/null; then
24+
fail "/tmp/vscode-remote-containers-*.js bridge present — re-run .devcontainer/postStart.sh."
25+
fi
26+
27+
# system-scope credential.helper is where VS Code injects; if anything
28+
# is set there git will use it before our per-host helpers.
29+
if git config --system --get credential.helper >/dev/null 2>&1; then
30+
fail "system credential.helper is still set — re-run .devcontainer/postStart.sh."
31+
fi
32+
33+
exit 0
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Edit(/workspaces/**)",
5+
"Write(/workspaces/**)",
6+
"Read(/workspaces/**)",
7+
"Bash(*)"
8+
],
9+
"deny": [
10+
"Bash(git push --force *)",
11+
"Bash(git reset --hard*)",
12+
"Bash(ssh *)",
13+
"Bash(ssh-agent *)",
14+
"Bash(*ssh-agent*)",
15+
"Bash(scp *)",
16+
"Bash(rsync *)",
17+
"Bash(sftp *)",
18+
"Bash(telnet *)",
19+
"Bash(mail *)",
20+
"Bash(sendmail *)"
21+
],
22+
"additionalDirectories": [
23+
"/workspaces/**"
24+
]
25+
},
26+
"hooks": {
27+
"UserPromptSubmit": [
28+
{
29+
"hooks": [
30+
{
31+
"type": "command",
32+
"command": ".claude/hooks/sandbox-check.sh"
33+
}
34+
]
35+
}
36+
]
37+
}
38+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
name: copier-derived
3+
description: This project was generated from python-copier-template. Use when editing devcontainer / Dockerfile / .github / pre-commit / justfile / .gitleaks / renovate config, or when the user asks about updating from the template, resolving copier conflicts, or why a config looks the way it does.
4+
---
5+
6+
# Copier-template-derived project
7+
8+
This project was generated from
9+
[python-copier-template](https://github.com/diamondlightsource/python-copier-template).
10+
The template is recorded in `.copier-answers.yml`:
11+
12+
```bash
13+
grep _src_path .copier-answers.yml # template source
14+
grep _commit .copier-answers.yml # version applied
15+
```
16+
17+
## Template-managed files
18+
19+
`copier update` overwrites these from the template. Local edits will
20+
either merge cleanly (good) or produce `.rej` / inline conflicts.
21+
**Prefer editing the upstream template** for any change that should
22+
apply to all projects — otherwise the next update reverts it.
23+
24+
- `.devcontainer/**`
25+
- `Dockerfile`
26+
- `.github/workflows/*.yml`, `.github/CONTRIBUTING.md`,
27+
`.github/ISSUE_TEMPLATE/`, `.github/PULL_REQUEST_TEMPLATE/`
28+
- `.pre-commit-config.yaml`, `.gitleaks.toml`, `renovate.json`
29+
- `justfile`
30+
- `pyproject.toml` — top-level metadata, build-system, ruff/pyright/mypy
31+
config, tox config (project deps and scripts are project-owned)
32+
- `tests/conftest.py`, `tests/test_cli.py`
33+
- `CLAUDE.md`, `README-CLAUDE.md`, `.claude/**`
34+
35+
## Project-owned files
36+
37+
Edit freely; never overwritten by `copier update`:
38+
39+
- `src/<package>/**`
40+
- New tests under `tests/` (other than the seeded `test_cli.py`)
41+
- `README.md` (rendered once with placeholders, then yours)
42+
- `.copier-answers.yml` answers (only `_commit` / `_src_path` are bumped
43+
by `copier update`)
44+
45+
## When the user asks to change a template-managed file
46+
47+
1. Make the requested change in this project so it works now.
48+
2. **Tell the user** the file is template-managed, and offer to also
49+
update the upstream template if they have it checked out (commonly
50+
at `/workspaces/python-copier-template`). Phrase as a choice — they
51+
may want a project-only patch.
52+
3. If both edits are made, the project edit can be reverted on the
53+
next `copier update` once the template change reaches a release.
54+
55+
## Running `copier update`
56+
57+
The user runs this themselves (it touches many files); only run it
58+
yourself if explicitly asked. Always pass `--trust`. After update,
59+
resolve any conflicts (look for `<<<<<<<` markers and `.rej` files)
60+
before committing.
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# CLAUDE.md
2+
3+
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
4+
5+
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
6+
7+
## 1. Think Before Coding
8+
9+
**Don't assume. Don't hide confusion. Surface tradeoffs.**
10+
11+
Before implementing:
12+
- State your assumptions explicitly. If uncertain, ask.
13+
- If multiple interpretations exist, present them - don't pick silently.
14+
- If a simpler approach exists, say so. Push back when warranted.
15+
- If something is unclear, stop. Name what's confusing. Ask.
16+
17+
## 2. Simplicity First
18+
19+
**Minimum code that solves the problem. Nothing speculative.**
20+
21+
- No features beyond what was asked.
22+
- No abstractions for single-use code.
23+
- No "flexibility" or "configurability" that wasn't requested.
24+
- No error handling for impossible scenarios.
25+
- If you write 200 lines and it could be 50, rewrite it.
26+
27+
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
28+
29+
## 3. Surgical Changes
30+
31+
**Touch only what you must. Clean up only your own mess.**
32+
33+
When editing existing code:
34+
- Don't "improve" adjacent code, comments, or formatting.
35+
- Don't refactor things that aren't broken.
36+
- Match existing style, even if you'd do it differently.
37+
- If you notice unrelated dead code, mention it - don't delete it.
38+
39+
When your changes create orphans:
40+
- Remove imports/variables/functions that YOUR changes made unused.
41+
- Don't remove pre-existing dead code unless asked.
42+
43+
The test: Every changed line should trace directly to the user's request.
44+
45+
## 4. Goal-Driven Execution
46+
47+
**Define success criteria. Loop until verified.**
48+
49+
Transform tasks into verifiable goals:
50+
- "Add validation" → "Write tests for invalid inputs, then make them pass"
51+
- "Fix the bug" → "Write a test that reproduces it, then make it pass"
52+
- "Refactor X" → "Ensure tests pass before and after"
53+
54+
For multi-step tasks, state a brief plan:
55+
```
56+
1. [Step] → verify: [check]
57+
2. [Step] → verify: [check]
58+
3. [Step] → verify: [check]
59+
```
60+
61+
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
62+
63+
---
64+
65+
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.

0 commit comments

Comments
 (0)