Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/build-claude-code.yml
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,19 @@ jobs:
matrix:
include:
- image-suffix: claude-code
verify-command: "bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version"
verify-command: "bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp"
runner: ubuntu-24.04
arch: amd64
- image-suffix: claude-code
verify-command: "bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version"
verify-command: "bun --version || true && claude --version && mise --version && fish --version && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp"
runner: ubuntu-24.04-arm
arch: arm64
- image-suffix: claude-code-sandbox
verify-command: "claude --version && mise --version && fish --version && which iptables && rtk --version && ralphex --version"
verify-command: "claude --version && mise --version && fish --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp"
runner: ubuntu-24.04
arch: amd64
- image-suffix: claude-code-sandbox
verify-command: "claude --version && mise --version && fish --version && which iptables && rtk --version && ralphex --version"
verify-command: "claude --version && mise --version && fish --version && which iptables && rtk --version && ralphex --version && test -x /usr/local/bin/patch-playwright-mcp"
runner: ubuntu-24.04-arm
arch: arm64
runs-on: ${{ matrix.runner }}
Expand Down
7 changes: 7 additions & 0 deletions claude-code/.devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,13 @@ ENV MISE_TRUSTED_CONFIG_PATHS="/workspace"
COPY --from=rtk-download /usr/local/bin/rtk /usr/local/bin/rtk
COPY --from=ralphex-download /usr/local/bin/ralphex /usr/local/bin/ralphex

# ── Playwright MCP patch script ──────────────────────────────────────────────
# Universal logic with no project-specific config — bake into the image so
# consumer projects don't carry a copy. Invoked from postCreateCommand
# (init-plugins.sh) and postStartCommand (devcontainer.json) to re-patch
# plugin auto-updates between sessions. See issue #87.
COPY --chmod=0755 patch-playwright-mcp.sh /usr/local/bin/patch-playwright-mcp

# ── Claude Code CLI ───────────────────────────────────────────────────────────
# Using npm instead of the native installer (curl claude.ai/install.sh | bash).
# The native installer is recommended for interactive use, but rate-limits (429)
Expand Down
4 changes: 2 additions & 2 deletions claude-code/.devcontainer/claude-sandbox/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@
// Firewall init (bind-mounted from project) + re-patch the Playwright MCP
// plugin's .mcp.json. The patch runs on every start so plugin auto-updates
// between sessions don't leave MCP pointing at the missing chrome channel.
// See issue #85.
"postStartCommand": "sudo /usr/local/bin/init-firewall.sh && bash .devcontainer/patch-playwright-mcp.sh",
// See issues #85, #87.
"postStartCommand": "sudo /usr/local/bin/init-firewall.sh && /usr/local/bin/patch-playwright-mcp",
"waitFor": "postStartCommand"
}
4 changes: 2 additions & 2 deletions claude-code/.devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
// Re-patch the Playwright MCP plugin's .mcp.json on every start.
// Plugin auto-updates between sessions create new cache dirs whose .mcp.json
// points at /opt/google/chrome/chrome (which we don't ship). Without this,
// MCP silently breaks until the next image rebuild. See issue #85.
"postStartCommand": "bash .devcontainer/patch-playwright-mcp.sh",
// MCP silently breaks until the next image rebuild. See issues #85, #87.
"postStartCommand": "/usr/local/bin/patch-playwright-mcp",
"waitFor": "postCreateCommand"
}
6 changes: 3 additions & 3 deletions claude-code/.devcontainer/init-plugins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ done

# ── Playwright MCP: route every cached .mcp.json to system chromium ─────────
# Universal across arches (no Chrome stable binary in either default or sandbox).
# Patch logic lives in patch-playwright-mcp.sh so it can also be invoked from
# postStartCommand to catch plugin auto-updates between sessions (#85).
bash "$(dirname "$0")/patch-playwright-mcp.sh"
# The patch binary is baked into the image (see Dockerfile #87) and also runs
# from postStartCommand to catch plugin auto-updates between sessions (#85).
/usr/local/bin/patch-playwright-mcp

# ── rtk init (token-optimized CLI proxy) ────────────────────────────────────
# Global hook-first mode: installs only the PreToolUse rewrite hook to ~/.claude/,
Expand Down
21 changes: 12 additions & 9 deletions claude-code/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,22 @@ Projects consuming these images need the following files in their repository.

Only pin tools that affect project stability — dev infrastructure (rtk, ralphex, Claude Code) is pre-installed in the image at latest. See [`mise.toml`](mise.toml) for a template.

### Optional: `.devcontainer/init-plugins.sh` and `.devcontainer/patch-playwright-mcp.sh`
### Optional: `.devcontainer/init-plugins.sh`

Claude Code plugin initialization. `init-plugins.sh` registers marketplaces, installs plugins, and (via `patch-playwright-mcp.sh`) rewrites every cached Playwright MCP `.mcp.json` to launch the system chromium. Both scripts are idempotent. See [`.devcontainer/init-plugins.sh`](.devcontainer/init-plugins.sh) and [`.devcontainer/patch-playwright-mcp.sh`](.devcontainer/patch-playwright-mcp.sh) for templates.
Claude Code plugin initialization. `init-plugins.sh` registers marketplaces, installs plugins, and invokes the image-baked `/usr/local/bin/patch-playwright-mcp` to rewrite every cached Playwright MCP `.mcp.json` to launch the system chromium. Idempotent. See [`.devcontainer/init-plugins.sh`](.devcontainer/init-plugins.sh) for the template.

Wire them into `devcontainer.json`:
Wire into `devcontainer.json`:

```jsonc
"postCreateCommand": "bash .devcontainer/init-plugins.sh",
"postStartCommand": "bash .devcontainer/patch-playwright-mcp.sh"
"postStartCommand": "/usr/local/bin/patch-playwright-mcp"
```

`postStartCommand` re-runs the patch on every container start so plugin auto-updates between sessions cannot leave MCP pointing at the missing chrome channel. See the [Playwright Strategy](#playwright-strategy) section.

Mark as executable: `chmod +x init-plugins.sh patch-playwright-mcp.sh`
Mark as executable: `chmod +x init-plugins.sh`

> **Why `init-plugins.sh` stays per-project but `patch-playwright-mcp` doesn't:** `init-plugins.sh` carries project-specific configuration (marketplace list, plugin list) — it's *meant* to be edited per project. The patch script has zero project-specific config and is identical across every consumer, so it's baked into the image and flows through the same daily-rebuild + `pull_policy: always` channel as the rest of the image. That boundary is the rule: project-specific config stays per-project; universal logic moves into the image.

### Sandbox-only: `.devcontainer/claude-sandbox/init-firewall.sh`

Expand Down Expand Up @@ -183,7 +185,6 @@ The template includes extensions for Claude Code, Bun, OXC, Tailwind, YAML, Dock
├── devcontainer.json ← default devcontainer
├── docker-compose.yml ← default compose (image + pull_policy)
├── init-plugins.sh ← Claude Code plugin setup (optional)
├── patch-playwright-mcp.sh ← Playwright MCP .mcp.json patch (optional)
└── claude-sandbox/
├── devcontainer.json ← sandbox devcontainer
├── docker-compose.yml ← sandbox compose (image + pull_policy)
Expand Down Expand Up @@ -252,8 +253,10 @@ export default defineConfig({

The `playwright@claude-plugins-official` plugin's `@playwright/mcp` defaults
to the `chrome` channel (`/opt/google/chrome/chrome`), which the image does
not ship. The included `patch-playwright-mcp.sh` rewrites every cached
`.mcp.json` under `~/.claude/plugins/cache/` to use the system chromium:
not ship. The image bakes in `/usr/local/bin/patch-playwright-mcp` (built
from [`patch-playwright-mcp.sh`](.devcontainer/patch-playwright-mcp.sh)),
which rewrites every cached `.mcp.json` under `~/.claude/plugins/cache/`
to use the system chromium:

```json
{
Expand All @@ -270,7 +273,7 @@ not ship. The included `patch-playwright-mcp.sh` rewrites every cached
}
```

`init-plugins.sh` invokes the patch script at `postCreateCommand`, and the
`init-plugins.sh` invokes the patch binary at `postCreateCommand`, and the
template `devcontainer.json` files run it again at `postStartCommand` so
plugin auto-updates between sessions cannot leave MCP pointing at the
missing chrome channel.
Expand Down