Skip to content

Commit 58bda1f

Browse files
authored
feat(claude-code): add devcontainer-upstream-sync skill (#95)
Teaches Claude Code to audit a downstream project's .devcontainer/ and bundled .claude/skills/ against this repo, adopt missed changes, draft upstream issues for shared bugs, and self-update. Same copy-paste distribution channel as sandbox-fetch-docs and sandbox-playwright — infrastructure files (Dockerfile, compose, devcontainer.json) keep the existing template flow; the skill rides alongside them. README adds the recommended-skill section and the structure-tree entry.
1 parent 7e52b0b commit 58bda1f

2 files changed

Lines changed: 317 additions & 2 deletions

File tree

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
---
2+
name: devcontainer-upstream-sync
3+
description: Use to audit a project's .devcontainer/ and bundled .claude/skills/ against gatezh/devcontainers upstream, adopt missed changes, draft upstream issues for shared bugs, and self-update. Triggers on "check upstream devcontainer", "sync devcontainer with upstream", "is upstream up to date", "check the latest changes", "make sure everything was picked up", "upstream is the source of truth".
4+
metadata:
5+
author: Serge Gatezh
6+
url: https://github.com/gatezh
7+
version: "1.0.0"
8+
---
9+
10+
# Devcontainer Upstream Sync
11+
12+
## Overview
13+
14+
This project's `.devcontainer/` and bundled `.claude/skills/` track
15+
[`gatezh/devcontainers`](https://github.com/gatezh/devcontainers) (the
16+
`claude-code` template) as the single source of truth. Upstream evolves —
17+
new postStartCommand wiring (e.g. `patch-playwright-mcp`), new bundled
18+
skills, retired allowlist hosts, image refactors. Without an audit loop
19+
those changes silently rot the local config.
20+
21+
This skill codifies the audit/adopt loop: per-file diff against upstream,
22+
classify drift as intentional vs unwanted, adopt the unwanted, draft
23+
upstream issues for bugs that exist on both sides, and update itself.
24+
25+
## Decision flow
26+
27+
```dot
28+
digraph upstream_sync {
29+
rankdir=TB;
30+
"User asked about upstream / sync / drift" [shape=doublecircle];
31+
"Run audit (Workflow 1)" [shape=box, style=filled, fillcolor="#d4edda"];
32+
"Per-row verdict" [shape=diamond];
33+
"Adopt drift (Workflow 2)" [shape=box, style=filled, fillcolor="#d4edda"];
34+
"Drift = upstream bug?" [shape=diamond];
35+
"Draft issue (Workflow 3)" [shape=box, style=filled, fillcolor="#fff3cd"];
36+
"Skill version stale?" [shape=diamond];
37+
"Self-update (Workflow 4)" [shape=box, style=filled, fillcolor="#d4edda"];
38+
"Done — report" [shape=doublecircle];
39+
40+
"User asked about upstream / sync / drift" -> "Run audit (Workflow 1)";
41+
"Run audit (Workflow 1)" -> "Per-row verdict";
42+
"Per-row verdict" -> "Adopt drift (Workflow 2)" [label="framework-track drift"];
43+
"Per-row verdict" -> "Drift = upstream bug?" [label="local diverges intentionally"];
44+
"Per-row verdict" -> "Skill version stale?" [label="all clean"];
45+
"Drift = upstream bug?" -> "Draft issue (Workflow 3)" [label="yes"];
46+
"Drift = upstream bug?" -> "Skill version stale?" [label="no"];
47+
"Adopt drift (Workflow 2)" -> "Skill version stale?";
48+
"Draft issue (Workflow 3)" -> "Skill version stale?";
49+
"Skill version stale?" -> "Self-update (Workflow 4)" [label="yes"];
50+
"Skill version stale?" -> "Done — report" [label="no"];
51+
"Self-update (Workflow 4)" -> "Done — report";
52+
}
53+
```
54+
55+
## Sync manifest
56+
57+
Authoritative list of what this skill tracks. Spelled out — do not
58+
discover paths from the filesystem; that misses files that should exist
59+
but don't, and includes files that shouldn't be tracked.
60+
61+
| local path | upstream path | bucket |
62+
|---|---|---|
63+
| `.devcontainer/devcontainer.json` | `claude-code/.devcontainer/devcontainer.json` | framework-track |
64+
| `.devcontainer/docker-compose.yml` | `claude-code/.devcontainer/docker-compose.yml` | starter-customize |
65+
| `.devcontainer/init-plugins.sh` | `claude-code/.devcontainer/init-plugins.sh` | starter-customize |
66+
| `.devcontainer/claude-sandbox/devcontainer.json` | `claude-code/.devcontainer/claude-sandbox/devcontainer.json` | framework-track |
67+
| `.devcontainer/claude-sandbox/docker-compose.yml` | `claude-code/.devcontainer/claude-sandbox/docker-compose.yml` | starter-customize |
68+
| `.devcontainer/claude-sandbox/init-firewall.sh` | `.devcontainer/claude-sandbox/init-firewall.sh` *(repo root, not `claude-code/`)* | exemplar |
69+
| `.claude/skills/sandbox-fetch-docs/SKILL.md` | `claude-code/.claude/skills/sandbox-fetch-docs/SKILL.md` | framework-track |
70+
| `.claude/skills/sandbox-playwright/SKILL.md` | `claude-code/.claude/skills/sandbox-playwright/SKILL.md` | framework-track |
71+
| `.claude/skills/devcontainer-upstream-sync/SKILL.md` | `claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md` | framework-track |
72+
| `.claude/settings.json` | `claude-code/.claude/settings.json` | starter-customize |
73+
| `mise.toml` | `claude-code/mise.toml` | starter-customize |
74+
75+
### Bucket meanings
76+
77+
- **framework-track** — should match upstream byte-for-byte modulo
78+
the project-name substitution in §"Customization-preservation rules".
79+
Drift is a bug to fix locally, or a bug to file upstream if upstream
80+
has it too.
81+
- **starter-customize** — upstream is a starter template; downstream
82+
edits are *expected* (project-specific extension lists, allowlists,
83+
marketplaces, tool versions). Don't auto-overwrite. Surface
84+
upstream's diff and ask which hunks to adopt.
85+
- **exemplar** — upstream provides a canonical example at a
86+
*different* path than the framework root. Treat as starter-customize,
87+
but use the special source path noted in the manifest.
88+
89+
## Customization-preservation rules
90+
91+
When adopting an upstream change, preserve these per-project patterns:
92+
93+
1. **Volume-name prefixes.** Upstream uses `myproject-…`; downstream
94+
uses `<project>-…`. Read the project name from
95+
`.devcontainer/docker-compose.yml`'s top-level `name:` field, then
96+
substitute.
97+
2. **VS Code `extensions` list.** Keep local additions (e.g. Hugo,
98+
OpenAI Codex). When adopting upstream, only port over upstream's
99+
*new* additions; don't strip what's local.
100+
3. **`runArgs`** like `--hostname=<project>` — keep verbatim.
101+
4. **`postCreateCommand` / `postStartCommand`.** These chain commands.
102+
Keep local additions (e.g. `bash .devcontainer/init-plugins.sh`,
103+
`ln -sfn /workspace/.codex ~/.codex`); sync upstream's *required*
104+
additions like `/usr/local/bin/patch-playwright-mcp`.
105+
5. **`init-firewall.sh` allowlist** and **`init-plugins.sh` plugin
106+
list** — never auto-overwrite. Surface upstream's recipe; ask
107+
before adopting.
108+
6. **`mise.toml` versions.** Versions may legitimately lead or lag
109+
upstream. Surface; ask.
110+
111+
## Conventions
112+
113+
- Upstream branch is `master` (not `main`).
114+
- Raw URL prefix:
115+
`https://raw.githubusercontent.com/gatezh/devcontainers/master/`
116+
- Use `gh api` for repo introspection (commits, issues, file listings)
117+
and `curl -sSL <raw-url>` for plain file fetches.
118+
- All workflows are read-only by default. Apply changes only after
119+
showing the diff to the user.
120+
121+
## Workflow 1: Audit
122+
123+
Walk the manifest, fetch each upstream file, diff, classify, report.
124+
125+
```sh
126+
# Per-row commands (run for each manifest row):
127+
curl -sSL "https://raw.githubusercontent.com/gatezh/devcontainers/master/${UPSTREAM_PATH}" -o /tmp/upstream-sync/$(basename "${UPSTREAM_PATH}").upstream
128+
diff -u "${LOCAL_PATH}" /tmp/upstream-sync/$(basename "${UPSTREAM_PATH}").upstream || true
129+
```
130+
131+
Classification rules per row:
132+
133+
- Diff is empty → **clean** (or `clean (after project-name substitution)`).
134+
- Diff is non-empty AND row is `framework-track`:
135+
- If every hunk maps to a customization-preservation pattern → **intentional-customization**.
136+
- Otherwise → **drift-needs-adopt**. Note the offending hunks.
137+
- Diff is non-empty AND row is `starter-customize` or `exemplar`:
138+
- **template-divergence**. Show the diff in the report; do not adopt
139+
automatically.
140+
141+
Report format (one row per manifest entry):
142+
143+
```
144+
✓ .devcontainer/claude-sandbox/devcontainer.json framework-track clean
145+
✓ .devcontainer/devcontainer.json framework-track intentional-customization (Hugo extensions, hostname)
146+
⚠ .devcontainer/claude-sandbox/init-firewall.sh exemplar template-divergence: see Workflow 2
147+
✗ .claude/skills/sandbox-playwright/SKILL.md framework-track drift-needs-adopt: 3 hunks
148+
```
149+
150+
Audit is read-only. Don't apply edits in this phase.
151+
152+
## Workflow 2: Adopt
153+
154+
Driven by Workflow 1's report. For each row that needs action:
155+
156+
### `drift-needs-adopt` (framework-track)
157+
158+
1. Print the diff (already in `/tmp/upstream-sync/…`).
159+
2. Confirm with the user it's not actually intentional.
160+
3. Apply via `Edit` (preserving customization-preservation rules in §3).
161+
4. Stage the file: `git add <local-path>`. Don't commit — let the user
162+
review and gate.
163+
164+
### `template-divergence` (starter-customize, exemplar)
165+
166+
1. Print the diff.
167+
2. Walk hunks one at a time (or in a small batch) with the user.
168+
3. Apply only the hunks they accept.
169+
4. Stage; don't commit.
170+
171+
### Special cases
172+
173+
- **`init-firewall.sh` (exemplar):** the canonical upstream copy is at
174+
the **repo root**, not under `claude-code/`. URL is
175+
`…/master/.devcontainer/claude-sandbox/init-firewall.sh`. The
176+
`claude-code/` template intentionally omits this file because
177+
per-project allowlist customization is the whole point.
178+
- **`postStartCommand` chains:** when upstream adds a step (e.g.
179+
`/usr/local/bin/patch-playwright-mcp`), splice it into the local
180+
chain rather than overwriting the chain. Verify the binary is in the
181+
image first:
182+
```sh
183+
docker run --rm --entrypoint /bin/sh \
184+
ghcr.io/gatezh/devcontainers/claude-code-sandbox:latest \
185+
-c 'ls -la /usr/local/bin/<binary>'
186+
```
187+
188+
## Workflow 3: Draft upstream issue
189+
190+
When Workflow 1 surfaces drift that exists in *both* local and
191+
upstream — i.e. the upstream is shipping a bug — draft an issue.
192+
193+
### Search for existing reports first
194+
195+
```sh
196+
gh issue list --repo gatezh/devcontainers --search '<keyword>' --state all --json number,title,state
197+
gh search issues --repo gatezh/devcontainers '<keyword>' --json title,state,number
198+
```
199+
200+
If a relevant open issue exists, **stop**. Add a comment if needed; do
201+
not file a duplicate.
202+
203+
### Draft the issue body
204+
205+
Save to `docs/upstream-issue-<short-slug>.md` in the project. Use this
206+
template (replace placeholders):
207+
208+
```markdown
209+
## Summary
210+
211+
<one paragraph: what's broken, where it is, what the user-visible
212+
symptom is>
213+
214+
## Reproduction
215+
216+
1. <step>
217+
2. <step>
218+
219+
```
220+
<verbatim error excerpt>
221+
```
222+
223+
## Verification
224+
225+
```
226+
<commands proving the bug>
227+
```
228+
229+
## Suggested fix
230+
231+
<diff or prose>
232+
233+
## Affected files in this repo
234+
235+
- `<upstream path>:<line>`
236+
- `<other upstream path>` (if a sibling copy exists)
237+
238+
## Related
239+
240+
- <other repo / issue references>
241+
```
242+
243+
### Print the file command
244+
245+
```sh
246+
gh issue create --repo gatezh/devcontainers \
247+
--title "[BUG] <concise title>" \
248+
--body-file docs/upstream-issue-<short-slug>.md
249+
```
250+
251+
**Do not run the `gh issue create` command.** Filing public issues is a
252+
user action — print the command and let the user run it. The harness
253+
blocks unattended posts to external systems, correctly.
254+
255+
## Workflow 4: Self-update
256+
257+
Check whether this skill itself is up to date.
258+
259+
1. Fetch upstream's frontmatter:
260+
```sh
261+
curl -sSL "https://raw.githubusercontent.com/gatezh/devcontainers/master/claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md" \
262+
| head -30
263+
```
264+
2. Extract `version:` from upstream and compare to the local file's
265+
`version:` (this file's frontmatter).
266+
3. If versions match → done.
267+
4. If upstream is newer:
268+
- Fetch the full upstream SKILL.md to a temp path.
269+
- `diff -u .claude/skills/devcontainer-upstream-sync/SKILL.md /tmp/upstream-skill.md`
270+
- Show the diff to the user.
271+
- On accept: overwrite the local file. **Do not auto-overwrite.**
272+
5. If local is newer (e.g. the user is working on a future version):
273+
report; do nothing.
274+
275+
## One-time install (for new projects)
276+
277+
For a new downstream project to adopt this skill, paste these three
278+
lines once:
279+
280+
```sh
281+
mkdir -p .claude/skills/devcontainer-upstream-sync
282+
curl -sSL https://raw.githubusercontent.com/gatezh/devcontainers/master/claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md \
283+
> .claude/skills/devcontainer-upstream-sync/SKILL.md
284+
git add .claude/skills/devcontainer-upstream-sync/SKILL.md
285+
```
286+
287+
After the one-time copy, the skill manages its own updates via
288+
Workflow 4.
289+
290+
## Out of scope
291+
292+
- **Auto-merging Dockerfile changes.** The Dockerfile is a baked-image
293+
concern; downstream consumes the image, not the Dockerfile.
294+
- **`init-plugins.sh` semantic merge.** The marketplace and plugin
295+
lists are legitimately project-specific. Skill surfaces upstream's
296+
diff and stops.
297+
- **Reverse direction (project → upstream PRs).** Filing issues is in
298+
scope; opening upstream PRs uses normal `gh pr create` flow, not
299+
this skill.

‎claude-code/README.md‎

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,20 @@ Both image variants ship system chromium and the `/usr/local/bin/patch-playwrigh
126126
127127
Copy `.claude/skills/sandbox-playwright/` into your project's `.claude/skills/` directory so Claude Code picks it up automatically.
128128
129+
### Recommended: Claude Code skill for upstream sync
130+
131+
To keep your project's `.devcontainer/` and bundled `.claude/skills/` in step with this repo, copy the [devcontainer-upstream-sync](.claude/skills/devcontainer-upstream-sync/SKILL.md) skill into your project's `.claude/skills/` directory. The skill audits drift, helps adopt missed changes, drafts upstream issues for shared bugs, and self-updates when this skill's `version:` bumps.
132+
133+
```bash
134+
mkdir -p .claude/skills/devcontainer-upstream-sync
135+
curl -sSL https://raw.githubusercontent.com/gatezh/devcontainers/master/claude-code/.claude/skills/devcontainer-upstream-sync/SKILL.md \
136+
> .claude/skills/devcontainer-upstream-sync/SKILL.md
137+
git add .claude/skills/devcontainer-upstream-sync/SKILL.md
138+
```
139+
140+
After the one-time copy, the skill manages its own updates.
141+
142+
129143
### Sandbox Authentication
130144
131145
The sandbox firewall blocks outbound traffic, so `claude login` (which opens a browser OAuth flow) won't work inside the container. Instead, generate a token on the host and inject it via environment variable.
@@ -202,8 +216,10 @@ The template includes extensions for Claude Code, Bun, OXC, Tailwind, YAML, Dock
202216
└── skills/
203217
├── sandbox-fetch-docs/
204218
│ └── SKILL.md ← teaches Claude Code to fetch docs within sandbox firewall
205-
└── sandbox-playwright/
206-
└── SKILL.md ← teaches Claude Code to drive Playwright MCP + @playwright/test
219+
├── sandbox-playwright/
220+
│ └── SKILL.md ← teaches Claude Code to drive Playwright MCP + @playwright/test
221+
└── devcontainer-upstream-sync/
222+
└── SKILL.md ← keeps project's .devcontainer/ + skills synced with this repo
207223
```
208224
209225
## Workspace Directory Layout

0 commit comments

Comments
 (0)