diff --git a/.github/workflows/build-claude-code.yml b/.github/workflows/build-claude-code.yml index 76779fe..b15cbd6 100644 --- a/.github/workflows/build-claude-code.yml +++ b/.github/workflows/build-claude-code.yml @@ -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 }} diff --git a/claude-code/.devcontainer/Dockerfile b/claude-code/.devcontainer/Dockerfile index a5c98e6..8336e39 100644 --- a/claude-code/.devcontainer/Dockerfile +++ b/claude-code/.devcontainer/Dockerfile @@ -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) diff --git a/claude-code/.devcontainer/claude-sandbox/devcontainer.json b/claude-code/.devcontainer/claude-sandbox/devcontainer.json index 3f6d2ea..11a93ad 100644 --- a/claude-code/.devcontainer/claude-sandbox/devcontainer.json +++ b/claude-code/.devcontainer/claude-sandbox/devcontainer.json @@ -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" } diff --git a/claude-code/.devcontainer/devcontainer.json b/claude-code/.devcontainer/devcontainer.json index b6f0855..cb67bd1 100644 --- a/claude-code/.devcontainer/devcontainer.json +++ b/claude-code/.devcontainer/devcontainer.json @@ -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" } diff --git a/claude-code/.devcontainer/init-plugins.sh b/claude-code/.devcontainer/init-plugins.sh index aa865bb..2b42f73 100644 --- a/claude-code/.devcontainer/init-plugins.sh +++ b/claude-code/.devcontainer/init-plugins.sh @@ -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/, diff --git a/claude-code/README.md b/claude-code/README.md index b4cbd8d..3854f10 100644 --- a/claude-code/README.md +++ b/claude-code/README.md @@ -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` @@ -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) @@ -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 { @@ -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.