Skip to content

Commit 0f1ced2

Browse files
gatezhclaude
andauthored
docs(claude-code): switch to Docker Compose with pull_policy (#52)
* docs(claude-code): switch consumer pattern to Docker Compose with pull_policy (#51) - Replace `image` in devcontainer.json with minimal docker-compose.yml using `pull_policy: always` so rebuilds always pull fresh images - Keep mounts, containerEnv, capAdd, init in devcontainer.json as cross-orchestrator properties (preserves ${devcontainerId} and ${localEnv:...} variable substitution) - Update local fallback, extending, sandbox auth, and file structure sections to reflect compose approach - Add metadata.author to sandbox-fetch-docs skill Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore(skills): add metadata to sandbox-fetch-docs and copy to project - Add url and version to skill metadata in claude-code/.claude/skills/ - Copy skill to .claude/skills/ for use in this project's devcontainer Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7186ee4 commit 0f1ced2

3 files changed

Lines changed: 177 additions & 26 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
name: sandbox-fetch-docs
3+
description: Use when needing to verify implementation against official library docs, check API contracts, or look up how a dependency works — especially after bugs caused by wrong assumptions about a library's API. Triggers on "check the docs", "verify against docs", "official documentation", "how does X API work", "canonical implementation".
4+
metadata:
5+
author: Serge Gatezh
6+
url: https://github.com/gatezh
7+
version: "1.0.0"
8+
---
9+
10+
# Fetch Documentation in Sandbox
11+
12+
## Overview
13+
14+
This sandbox only has network access to GitHub and npm registry. All other domains (vendor doc sites, CDNs, search engines) are firewalled. Use these approaches in priority order — do NOT attempt blocked approaches.
15+
16+
## Approach Priority
17+
18+
```dot
19+
digraph fetch_docs {
20+
rankdir=TB;
21+
"Need library docs?" [shape=doublecircle];
22+
"Installed in node_modules?" [shape=diamond];
23+
"Read local source + types" [shape=box, style=filled, fillcolor="#d4edda"];
24+
"Public repo on GitHub?" [shape=diamond];
25+
"Fetch via raw.githubusercontent.com" [shape=box, style=filled, fillcolor="#d4edda"];
26+
"Browse via GitHub Contents API" [shape=box, style=filled, fillcolor="#d4edda"];
27+
"Need package metadata?" [shape=diamond];
28+
"Query npm registry" [shape=box, style=filled, fillcolor="#d4edda"];
29+
"Ask user for help" [shape=box, style=filled, fillcolor="#fff3cd"];
30+
31+
"Need library docs?" -> "Installed in node_modules?";
32+
"Installed in node_modules?" -> "Read local source + types" [label="yes"];
33+
"Installed in node_modules?" -> "Public repo on GitHub?" [label="no"];
34+
"Read local source + types" -> "Public repo on GitHub?" [label="need more context"];
35+
"Public repo on GitHub?" -> "Fetch via raw.githubusercontent.com" [label="know file path"];
36+
"Public repo on GitHub?" -> "Browse via GitHub Contents API" [label="need to discover files"];
37+
"Public repo on GitHub?" -> "Need package metadata?" [label="no repo"];
38+
"Fetch via raw.githubusercontent.com" -> "Need package metadata?" [label="need more"];
39+
"Browse via GitHub Contents API" -> "Fetch via raw.githubusercontent.com" [label="found path"];
40+
"Need package metadata?" -> "Query npm registry" [label="yes"];
41+
"Need package metadata?" -> "Ask user for help" [label="no"];
42+
}
43+
```
44+
45+
## 1. Installed node_modules (fastest, always works)
46+
47+
The definitive source for how a library behaves at the exact version you're using:
48+
49+
```bash
50+
# Find the package (Bun layout — other managers use different paths, e.g. node_modules/PACKAGE)
51+
find node_modules/.bun -path "*PACKAGE_NAME*" -type d -maxdepth 3
52+
53+
# Read type definitions (the API contract)
54+
# Look for index.d.mts, index.d.ts, or index.d.cts
55+
Read: node_modules/.bun/PACKAGE@VERSION/node_modules/PACKAGE/dist/index.d.mts
56+
57+
# Read source implementation (how it actually works)
58+
Grep: pattern in node_modules/.bun/PACKAGE@VERSION/node_modules/PACKAGE/dist/index.mjs
59+
```
60+
61+
**When to use:** Always start here. Types show the API contract, source shows the implementation. This is more reliable than docs which may be outdated or describe a different version.
62+
63+
## 2. Raw GitHub content (no auth needed)
64+
65+
Fetch files directly from public repos via `raw.githubusercontent.com`:
66+
67+
```bash
68+
# WebFetch or curl — both work
69+
WebFetch: https://raw.githubusercontent.com/OWNER/REPO/BRANCH/path/to/file.ts
70+
71+
# Common branches to try: main, master, canary
72+
# Example: Resend SDK email handling
73+
WebFetch: https://raw.githubusercontent.com/resend/resend-node/main/src/emails/emails.ts
74+
```
75+
76+
If `main` returns 404, try `master` or `canary`. Use the Contents API (below) to discover the default branch and file paths.
77+
78+
## 3. GitHub Contents API (no auth, ~60 req/hr)
79+
80+
Browse repo structure when you don't know file paths:
81+
82+
```bash
83+
# List directory contents
84+
curl -s https://api.github.com/repos/OWNER/REPO/contents/src/path
85+
86+
# Read repo README
87+
curl -s https://api.github.com/repos/OWNER/REPO/readme
88+
89+
# Get file content (base64-encoded, decode with jq)
90+
curl -s https://api.github.com/repos/OWNER/REPO/contents/path/to/file | jq -r '.content' | base64 -d
91+
```
92+
93+
## 4. npm registry (package metadata only)
94+
95+
```bash
96+
curl -s https://registry.npmjs.org/PACKAGE/latest | jq '.version, .description, .repository'
97+
```
98+
99+
Useful for finding the GitHub repo URL when you don't know it.
100+
101+
## What NEVER works in this sandbox
102+
103+
| Approach | Why |
104+
|----------|-----|
105+
| WebFetch to vendor sites (resend.com, hono.dev, drizzle.team, etc.) | Firewall blocks non-GitHub domains |
106+
| WebSearch | Search engines not whitelisted |
107+
| `gh` CLI | Installed but not authenticated |
108+
| GitHub code search API (`/search/code`) | Requires authentication |
109+
| CDNs (unpkg, jsdelivr, cdnjs) | Not whitelisted |
110+
111+
Do NOT attempt these — they waste time with connection timeouts and auth errors.

‎claude-code/.claude/skills/sandbox-fetch-docs.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
---
22
name: sandbox-fetch-docs
33
description: Use when needing to verify implementation against official library docs, check API contracts, or look up how a dependency works — especially after bugs caused by wrong assumptions about a library's API. Triggers on "check the docs", "verify against docs", "official documentation", "how does X API work", "canonical implementation".
4+
metadata:
5+
author: Serge Gatezh
6+
url: https://github.com/gatezh
7+
version: "1.0.0"
48
---
59

610
# Fetch Documentation in Sandbox

‎claude-code/README.md‎

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -47,16 +47,29 @@ The image rebuilds daily at 5am MT (11:00 UTC) using native runners for both amd
4747

4848
### Default variant
4949

50-
Add to your project's `.devcontainer/devcontainer.json`:
50+
Add these files to your project's `.devcontainer/` directory. Docker Compose with `pull_policy: always` ensures "Rebuild Without Cache" always pulls the latest image — the image name stays in one place. All other config stays in `devcontainer.json` using cross-orchestrator properties (`mounts`, `containerEnv`, `capAdd`, `init`) so you keep devcontainer variable substitution (`${devcontainerId}`, `${localEnv:...}`).
51+
52+
`.devcontainer/docker-compose.yml`:
53+
54+
```yaml
55+
services:
56+
devcontainer:
57+
image: ghcr.io/gatezh/devcontainer-images/claude-code:latest
58+
pull_policy: always
59+
volumes:
60+
- ..:/workspace:cached
61+
```
62+
63+
`.devcontainer/devcontainer.json`:
5164

5265
```jsonc
5366
{
5467
"name": "Local Development",
55-
"image": "ghcr.io/gatezh/devcontainer-images/claude-code:latest",
68+
"dockerComposeFile": "docker-compose.yml",
69+
"service": "devcontainer",
70+
"workspaceFolder": "/workspace",
5671
"init": true,
5772
"remoteUser": "node",
58-
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind",
59-
"workspaceFolder": "/workspace",
6073
// Named volumes persist node_modules, Claude config, and fish history across rebuilds.
6174
// Dirs are pre-created in the image with node:node ownership, so fresh volumes
6275
// inherit correct permissions via Docker volume population.
@@ -84,16 +97,29 @@ Add to your project's `.devcontainer/devcontainer.json`:
8497

8598
> **No node_modules volumes** — the sandbox variant does not have passwordless sudo (only `sudo /usr/local/bin/init-firewall.sh` is allowed), so the `sudo chown` used in the default variant to fix volume ownership won't work. Let node_modules live in the bind mount instead.
8699

100+
`.devcontainer/claude-sandbox/docker-compose.yml`:
101+
102+
```yaml
103+
services:
104+
devcontainer:
105+
image: ghcr.io/gatezh/devcontainer-images/claude-code-sandbox:latest
106+
pull_policy: always
107+
volumes:
108+
- ../..:/workspace:cached
109+
```
110+
111+
`.devcontainer/claude-sandbox/devcontainer.json`:
112+
87113
```jsonc
88114
{
89115
"name": "Claude Code Sandbox",
90-
"image": "ghcr.io/gatezh/devcontainer-images/claude-code-sandbox:latest",
116+
"dockerComposeFile": "docker-compose.yml",
117+
"service": "devcontainer",
118+
"workspaceFolder": "/workspace",
91119
// Capabilities required for iptables firewall setup
92120
"capAdd": ["NET_ADMIN", "NET_RAW"],
93121
"init": true,
94122
"remoteUser": "node",
95-
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind",
96-
"workspaceFolder": "/workspace",
97123
"mounts": [
98124
"source=sandbox-fish-${devcontainerId},target=/home/node/.local/share/fish,type=volume",
99125
"source=sandbox-config-${devcontainerId},target=/home/node/.claude,type=volume",
@@ -215,10 +241,13 @@ The sandbox `devcontainer.json` already injects this via `${localEnv:CLAUDE_CODE
215241

216242
**Alternative — per-project `.env.local` file:**
217243

218-
If you prefer file-based configuration over host env vars, add `runArgs` to the sandbox `devcontainer.json`:
244+
If you prefer file-based configuration over host env vars, add `env_file` to the sandbox `docker-compose.yml` and remove `CLAUDE_CODE_OAUTH_TOKEN` from `containerEnv` in `devcontainer.json`:
219245

220-
```jsonc
221-
"runArgs": ["--env-file", ".devcontainer/claude-sandbox/.env.local"],
246+
```yaml
247+
services:
248+
devcontainer:
249+
env_file:
250+
- .env.local
222251
```
223252

224253
Create `.env.local` from the template (git-ignored):
@@ -237,16 +266,18 @@ The `.env.example` template to check into your project:
237266
CLAUDE_CODE_OAUTH_TOKEN=your-token-here
238267
```
239268

240-
Add `.env.local` to `.gitignore`. Note: Docker will fail to start if `.env.local` doesn't exist when using `--env-file`.
269+
Add `.env.local` to `.gitignore`. Note: Docker Compose fails to start if `.env.local` doesn't exist when using `env_file` (set `required: false` in compose to make it optional).
241270

242271
### Complete file structure
243272

244273
```
245274
.devcontainer/
246275
├── devcontainer.json ← default devcontainer
276+
├── docker-compose.yml ← default compose (image + pull_policy)
247277
├── init-plugins.sh ← Claude Code plugin setup (optional)
248278
└── claude-sandbox/
249279
├── devcontainer.json ← sandbox devcontainer
280+
├── docker-compose.yml ← sandbox compose (image + pull_policy)
250281
├── init-firewall.sh ← firewall script (customize domain allowlist)
251282
├── .env.example ← template for auth token (checked in)
252283
└── .env.local ← actual auth token (gitignored)
@@ -271,7 +302,7 @@ The image pre-creates these directories with `node:node` ownership so Docker's v
271302
└── database/node_modules/
272303
```
273304
274-
If your project has additional services, create volume mounts in `devcontainer.json` — Docker creates directories at container start. The `sudo chown` in `updateContentCommand` fixes ownership.
305+
If your project has additional services, add volume mounts in `devcontainer.json` `mounts` — Docker creates directories at container start. The `sudo chown` in `updateContentCommand` fixes ownership.
275306
276307
## Playwright Version Strategy
277308
@@ -322,20 +353,23 @@ docker build --target default -t claude-code:local .devcontainer
322353
docker build --target sandbox -t claude-code-sandbox:local .devcontainer
323354
```
324355

325-
Then update your project's `devcontainer.json` to use the local tag:
356+
Then update your project's `docker-compose.yml` to use the local tag:
326357

327-
```jsonc
328-
// Replace the GHCR reference:
329-
// "image": "ghcr.io/gatezh/devcontainer-images/claude-code:latest",
330-
// With the local build:
331-
"image": "claude-code:local",
358+
```yaml
359+
services:
360+
devcontainer:
361+
# Replace the GHCR reference:
362+
# image: ghcr.io/gatezh/devcontainer-images/claude-code:latest
363+
# With the local build:
364+
image: claude-code:local
365+
# pull_policy no longer needed for local images
332366
```
333367

334-
Everything else (`mounts`, `containerEnv`, `updateContentCommand`, etc.) stays the same — the local image is identical to the pre-built one.
368+
Everything else in `devcontainer.json` (mounts, containerEnv, lifecycle commands, etc.) stays the same — the local image is identical to the pre-built one.
335369

336370
### Extending the image for project-specific needs
337371

338-
If you need to layer project-specific tools on top, create a thin Dockerfile in your project:
372+
If you need to layer project-specific tools on top, create a thin Dockerfile and update your compose file to build it:
339373

340374
```dockerfile
341375
# .devcontainer/Dockerfile
@@ -344,12 +378,14 @@ FROM ghcr.io/gatezh/devcontainer-images/claude-code:latest
344378
RUN npm install -g your-tool
345379
```
346380

347-
```jsonc
348-
// .devcontainer/devcontainer.json
349-
{
350-
"build": { "dockerfile": "Dockerfile" },
351-
// ... rest of config (same as Quick Start, minus the "image" key)
352-
}
381+
```yaml
382+
# .devcontainer/docker-compose.yml — replace `image` with `build`
383+
services:
384+
devcontainer:
385+
build:
386+
context: .
387+
dockerfile: Dockerfile
388+
# ... rest stays the same (workspace volume, etc.)
353389
```
354390

355391
This pulls the pre-built image as a base layer (cached after first pull) and adds your customizations on top.

0 commit comments

Comments
 (0)