Why this matters
The current Playwright Strategy section in the README does great work for @playwright/test and @playwright/mcp, but its wiring guidance is written as if playwright.config.ts is the only Playwright entry point in a consumer project. It isn't.
Concrete recurrence: a consumer (UtiliTsar, the project I'm filing this from) followed the upstream README end to end — wired playwright.config.ts, set up init-plugins.sh, copied sandbox-fetch-docs and sandbox-playwright skills, set the env vars. The MCP plugin worked, @playwright/test worked. Then the storybook test suite (@vitest/browser-playwright) failed at first run with a misleading message:
Executable doesn't exist at /home/node/.cache/ms-playwright/chromium_headless_shell-1217/chrome-linux/headless_shell
Looks like Playwright was just installed or updated.
Please run the following command to download new browsers:
npx playwright install
That message points at the wrong fix. Running npx playwright install would re-download the bundled binary the image specifically tries to avoid. The actual fix is: wire launchOptions.executablePath in vitest.config.ts the same way playwright.config.ts already wires it. One line.
The reason this took multiple back-and-forths to find is the README's wording (verbatim):
PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH is a project convention — Playwright does not read it automatically. Each project's playwright.config.ts must honor it:
That sentence enumerates one Playwright entry point. A reasonable consumer reads this, wires their playwright.config.ts, and stops. If their project also uses vitest browser mode (typically via @storybook/addon-vitest for storybook interaction tests, or @vitest/browser for browser-mode unit tests), it silently breaks at first run.
This issue closes that gap by enumerating every Playwright consumer a typical project might have, with copy-paste-ready wiring snippets for each.
What's already documented
The existing Playwright Strategy section is solid for what it covers:
- The two env vars (
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 + PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium) and the rationale (avoid version coupling between @playwright/mcp and managed-cache binaries; image stays smaller).
- The convention statement (Playwright doesn't read the env var automatically).
- The
@playwright/mcp story end to end: /usr/local/bin/patch-playwright-mcp rewrites .mcp.json, init-plugins.sh invokes it at postCreateCommand, postStartCommand re-runs it for plugin auto-updates.
- The debug snippet (
cat ~/.claude/plugins/cache/...).
Nothing about the above needs to change. This issue only proposes to expand the consumer wiring part of the section to cover all Playwright entry points — not just @playwright/test.
The gap
The Playwright Strategy section's only consumer wiring example is @playwright/test. A representative monorepo project may have any of these Playwright consumers, none of which the README enumerates:
| Consumer |
Why it shows up |
Wiring required? |
@playwright/mcp |
bundled with playwright@claude-plugins-official |
No — image-baked patch script handles it |
@playwright/test |
scripted E2E suite |
Yes — playwright.config.ts use.launchOptions.executablePath |
@vitest/browser-playwright |
vitest browser mode (used by @storybook/addon-vitest for storybook interaction tests, by @vitest/browser for browser-mode unit tests) |
Yes — vitest.config.ts playwright({ launchOptions: { … } }) |
playwright-core / playwright (direct) |
custom scripts that call chromium.launch(...) themselves (CI scrapers, custom E2E without @playwright/test, codegen) |
Yes — at every chromium.launch(...) call site |
agent-browser (default target only) |
image npm-installs it |
No — reads PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH natively |
Of these, only @playwright/test is documented. @vitest/browser-playwright is the one this issue is filing against — it's a real consumer, increasingly common via storybook tests, and silent when broken.
Proposed change
Replace the existing Playwright Strategy → consumer wiring snippet in the README (the part that says "Each project's playwright.config.ts must honor it:" plus the single TS code block) with the table + snippets below. Pasteable verbatim.
`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` is a project convention — Playwright
does **not** read it automatically. Every Playwright entry point in your
project must honor it. The image's job ends at "the env var points at the
binary"; consumer projects do the wiring.
| Consumer | Where to wire | What to add |
|---|---|---|
| `@playwright/mcp` | nothing (image-baked) | The `/usr/local/bin/patch-playwright-mcp` binary rewrites every cached `.mcp.json` to use the system chromium. See "Playwright MCP plugin" below. |
| `@playwright/test` | `playwright.config.ts` | `use.launchOptions.executablePath` |
| `@vitest/browser-playwright` (incl. `@storybook/addon-vitest`, `@vitest/browser`) | `vitest.config.ts` | `playwright({ launchOptions: { executablePath } })` |
| Direct `playwright-core` / `playwright` use in scripts | every `chromium.launch(...)` site | `chromium.launch({ executablePath })` |
| `agent-browser` (default target only) | nothing | Reads `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` natively. |
### `@playwright/test` — `playwright.config.ts`
```ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
use: {
launchOptions: {
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
},
},
});
```
In a monorepo, repeat this in every `playwright.config.ts` (one per service
that has a Playwright suite). The same env-var value works for all.
### `@vitest/browser-playwright` — `vitest.config.ts`
The provider's options match the Playwright `LaunchOptions` interface
verbatim under the `launchOptions` key (see the
[`PlaywrightProviderOptions` interface](https://github.com/vitest-dev/vitest/blob/main/packages/browser-playwright/src/index.ts)
in `@vitest/browser-playwright`):
```ts
import { defineConfig, mergeConfig } from "vitest/config";
import { playwright } from "@vitest/browser-playwright";
import viteConfig from "./vite.config";
export default mergeConfig(
viteConfig,
defineConfig({
test: {
browser: {
enabled: true,
headless: true,
provider: playwright({
launchOptions: {
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
},
}),
instances: [{ browser: "chromium" }],
},
},
}),
);
```
Symptom when this is missing: `Executable doesn't exist at
/home/node/.cache/ms-playwright/chromium_headless_shell-<rev>/chrome-linux/headless_shell`
followed by a "Please run npx playwright install" message. Don't follow
that hint — wire `launchOptions.executablePath` instead.
### Direct `playwright-core` / `playwright` use
For CI scripts, custom E2E that doesn't use `@playwright/test`, or codegen
sites that import `chromium` directly:
```ts
import { chromium } from "playwright-core"; // or "playwright"
const browser = await chromium.launch({
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
headless: true,
});
```
Optional small wording fixes (Project Setup Guide section)
Two items are currently labeled "Optional" but materially affect Playwright support:
- "Optional: Claude Code skill for Playwright" — the skill encodes the discovery protocol Claude needs to find a project's
playwright.config.ts, the port, etc. Without it Claude has to be hand-held every time. Suggest renaming to "Recommended". Same for sandbox-fetch-docs if the project uses the sandbox variant — without it, Claude can't fetch library docs through the firewall.
- "Optional:
.devcontainer/init-plugins.sh" — required if the project uses Playwright MCP (otherwise the patch binary never runs). Soft call; could stay "Optional" with a parenthetical "(required for Playwright MCP)".
Not blocking on these — feel free to keep the wording as-is if "Optional" is meant in the strict "image works without it" sense. The Playwright-consumer-wiring change above is the load-bearing part.
Verification plan
After the change, a fresh consumer following only the Playwright Strategy section should be able to wire all known Playwright entry points without external help. Concrete check: search the new section for the strings playwright.config.ts, vitest.config.ts, playwright-core, agent-browser, @playwright/mcp — all five should appear at least once. They do in the snippet above.
End-to-end test (in a consumer project): with the wiring applied, run the project's full test stack — playwright test, vitest browser projects, any custom Playwright scripts. None of them should print "Please run npx playwright install" or look for binaries under ~/.cache/ms-playwright/. All three should launch via /usr/bin/chromium.
Migration impact for consumer projects
Purely additive. Projects that already wired playwright.config.ts keep working. Projects that hadn't yet wired vitest.config.ts (this one, before the local fix that prompted this issue) will see the new section and apply it the next time they sync against the README. A one-line change per missing wiring point.
Trade-offs / alternatives considered
- Make
executablePath automatic: rejected, would require a custom Playwright fork or a global JS shim. The "project convention" stance is correct — it's just under-documented for non-@playwright/test consumers.
- Move some of this guidance into the
sandbox-playwright SKILL.md instead of the README: the skill is for Claude; the README is for humans setting up the project. Both audiences need the wiring guidance, but the README is where a human first lands. Keep the canonical version there. Optionally mirror into the skill too — but that's a follow-up.
- Generate a stub
vitest.config.ts template the way the README links to a playwright.config.ts template (it doesn't, actually — the example is inline only): not necessary. The README inline-snippet pattern is fine.
Closes the loop with the previous three issues
This issue is the consumer-wiring completeness pass. After it lands, a fresh consumer should be able to set up Playwright in their project from the README alone, regardless of which Playwright packages they use.
Why this matters
The current Playwright Strategy section in the README does great work for
@playwright/testand@playwright/mcp, but its wiring guidance is written as ifplaywright.config.tsis the only Playwright entry point in a consumer project. It isn't.Concrete recurrence: a consumer (UtiliTsar, the project I'm filing this from) followed the upstream README end to end — wired
playwright.config.ts, set upinit-plugins.sh, copiedsandbox-fetch-docsandsandbox-playwrightskills, set the env vars. The MCP plugin worked,@playwright/testworked. Then the storybook test suite (@vitest/browser-playwright) failed at first run with a misleading message:That message points at the wrong fix. Running
npx playwright installwould re-download the bundled binary the image specifically tries to avoid. The actual fix is: wirelaunchOptions.executablePathinvitest.config.tsthe same wayplaywright.config.tsalready wires it. One line.The reason this took multiple back-and-forths to find is the README's wording (verbatim):
That sentence enumerates one Playwright entry point. A reasonable consumer reads this, wires their
playwright.config.ts, and stops. If their project also uses vitest browser mode (typically via@storybook/addon-vitestfor storybook interaction tests, or@vitest/browserfor browser-mode unit tests), it silently breaks at first run.This issue closes that gap by enumerating every Playwright consumer a typical project might have, with copy-paste-ready wiring snippets for each.
What's already documented
The existing Playwright Strategy section is solid for what it covers:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1+PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/bin/chromium) and the rationale (avoid version coupling between@playwright/mcpand managed-cache binaries; image stays smaller).@playwright/mcpstory end to end:/usr/local/bin/patch-playwright-mcprewrites.mcp.json,init-plugins.shinvokes it atpostCreateCommand,postStartCommandre-runs it for plugin auto-updates.cat ~/.claude/plugins/cache/...).Nothing about the above needs to change. This issue only proposes to expand the consumer wiring part of the section to cover all Playwright entry points — not just
@playwright/test.The gap
The Playwright Strategy section's only consumer wiring example is
@playwright/test. A representative monorepo project may have any of these Playwright consumers, none of which the README enumerates:@playwright/mcpplaywright@claude-plugins-official@playwright/testplaywright.config.tsuse.launchOptions.executablePath@vitest/browser-playwright@storybook/addon-vitestfor storybook interaction tests, by@vitest/browserfor browser-mode unit tests)vitest.config.tsplaywright({ launchOptions: { … } })playwright-core/playwright(direct)chromium.launch(...)themselves (CI scrapers, custom E2E without@playwright/test, codegen)chromium.launch(...)call siteagent-browser(default target only)PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATHnativelyOf these, only
@playwright/testis documented.@vitest/browser-playwrightis the one this issue is filing against — it's a real consumer, increasingly common via storybook tests, and silent when broken.Proposed change
Replace the existing Playwright Strategy → consumer wiring snippet in the README (the part that says "Each project's
playwright.config.tsmust honor it:" plus the single TS code block) with the table + snippets below. Pasteable verbatim.Optional small wording fixes (Project Setup Guide section)
Two items are currently labeled "Optional" but materially affect Playwright support:
playwright.config.ts, the port, etc. Without it Claude has to be hand-held every time. Suggest renaming to "Recommended". Same forsandbox-fetch-docsif the project uses the sandbox variant — without it, Claude can't fetch library docs through the firewall..devcontainer/init-plugins.sh" — required if the project uses Playwright MCP (otherwise the patch binary never runs). Soft call; could stay "Optional" with a parenthetical "(required for Playwright MCP)".Not blocking on these — feel free to keep the wording as-is if "Optional" is meant in the strict "image works without it" sense. The Playwright-consumer-wiring change above is the load-bearing part.
Verification plan
After the change, a fresh consumer following only the Playwright Strategy section should be able to wire all known Playwright entry points without external help. Concrete check: search the new section for the strings
playwright.config.ts,vitest.config.ts,playwright-core,agent-browser,@playwright/mcp— all five should appear at least once. They do in the snippet above.End-to-end test (in a consumer project): with the wiring applied, run the project's full test stack —
playwright test, vitest browser projects, any custom Playwright scripts. None of them should print "Please run npx playwright install" or look for binaries under~/.cache/ms-playwright/. All three should launch via/usr/bin/chromium.Migration impact for consumer projects
Purely additive. Projects that already wired
playwright.config.tskeep working. Projects that hadn't yet wiredvitest.config.ts(this one, before the local fix that prompted this issue) will see the new section and apply it the next time they sync against the README. A one-line change per missing wiring point.Trade-offs / alternatives considered
executablePathautomatic: rejected, would require a custom Playwright fork or a global JS shim. The "project convention" stance is correct — it's just under-documented for non-@playwright/testconsumers.sandbox-playwrightSKILL.md instead of the README: the skill is for Claude; the README is for humans setting up the project. Both audiences need the wiring guidance, but the README is where a human first lands. Keep the canonical version there. Optionally mirror into the skill too — but that's a follow-up.vitest.config.tstemplate the way the README links to aplaywright.config.tstemplate (it doesn't, actually — the example is inline only): not necessary. The README inline-snippet pattern is fine.Closes the loop with the previous three issues
claude-code-sandbox, and the existing ARM64 patch is broken inclaude-codetoo #85 — made Playwright MCP work at all.patch-playwright-mcp.shinto the image so consumer projects don't carry a copy #87 — bakedpatch-playwright-mcpinto the image.sandbox-playwrightskill.This issue is the consumer-wiring completeness pass. After it lands, a fresh consumer should be able to set up Playwright in their project from the README alone, regardless of which Playwright packages they use.