A user installed Playwright and the pane still reported it missing, then needed six manual steps to get a browser working. Two detection bugs and a setup gap.
Detection bug 1 — npx installs were invisible. The probe searched /workspace, cwd, $HOME/node_modules and npm root -g. But claude mcp add … npx @playwright/mcp never installs into any node_modules; npx caches under ~/.npm/_npx/<hash>/node_modules. Those roots are now searched (honouring npm_config_cache).
Detection bug 2 — a current Playwright reported as too old. The probe read has_bind from whichever manifest resolved first. npm does not hoist for global installs, and the playwright wrapper ships no types/types.d.ts — so sudo npm i -g playwright made the pane claim the build predates browser.bind(). That is almost certainly why the user then added local dev deps on top. The probe now hops from the wrapper to its nested playwright-core.
Setup is now two clicks. A new install action runs in the container and streams progress: packages first, then browsers as a separate step with sizes stated before the click. install-deps runs as root — the missing OS libraries are the real reason plain npx playwright install chromium produced a pile of errors; the image ships none of them. Ends with a real headless launch, because "installed but unlaunchable" was the actual failure mode.
Both Chromium and the Chrome channel are offered with who-needs-which spelled out — @playwright/mcp specifically wants chrome, so it is a genuine requirement for that path, not a workaround.
Guidance corrected. The messages previously suggested @playwright/mcp as an install route; it can never satisfy the viewer requirement. It now appears only in its true role. A test asserts no message offers it as an install route.
Certificate errors are classified distinctly — behind a TLS-intercepting proxy a user should not be told Playwright is broken.
Verification beyond unit tests: the embedded probe was run against a genuine npx cache, a genuine npm i -g layout, a local tree from a subdirectory, and an empty container; and a real Chromium was downloaded and launched.
A user installed Playwright and the pane still reported it missing, then needed six manual steps to get a browser working. Two detection bugs and a setup gap.
**Detection bug 1 — npx installs were invisible.** The probe searched `/workspace`, cwd, `$HOME/node_modules` and `npm root -g`. But `claude mcp add … npx @playwright/mcp` never installs into any `node_modules`; npx caches under `~/.npm/_npx/<hash>/node_modules`. Those roots are now searched (honouring `npm_config_cache`).
**Detection bug 2 — a current Playwright reported as too old.** The probe read `has_bind` from whichever manifest resolved first. npm does **not** hoist for global installs, and the `playwright` wrapper ships no `types/types.d.ts` — so `sudo npm i -g playwright` made the pane claim the build predates `browser.bind()`. That is almost certainly why the user then added local dev deps on top. The probe now hops from the wrapper to its nested `playwright-core`.
**Setup is now two clicks.** A new install action runs in the container and streams progress: packages first, then browsers as a separate step with sizes stated **before** the click. `install-deps` runs as root — the missing OS libraries are the real reason plain `npx playwright install chromium` produced a pile of errors; the image ships none of them. Ends with a real headless launch, because "installed but unlaunchable" was the actual failure mode.
Both Chromium and the **Chrome channel** are offered with who-needs-which spelled out — `@playwright/mcp` specifically wants `chrome`, so it is a genuine requirement for that path, not a workaround.
**Guidance corrected.** The messages previously suggested `@playwright/mcp` as an install route; it can never satisfy the viewer requirement. It now appears only in its true role. A test asserts no message offers it as an install route.
**Certificate errors are classified distinctly** — behind a TLS-intercepting proxy a user should not be told Playwright is broken.
**Verification** beyond unit tests: the embedded probe was run against a genuine npx cache, a genuine `npm i -g` layout, a local tree from a subdirectory, and an empty container; and a real Chromium was downloaded and launched.
212 Rust tests, 279 frontend tests.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k
Detection missed the npx cache, so a Playwright installed through Claude
Code's MCP setup (`npx @playwright/mcp@latest`, which unpacks into
~/.npm/_npx/<hash>/node_modules and no node_modules at all) was invisible.
The probe now globs that cache alongside the existing roots and reports
every root it consulted.
It also read `has_bind` off whichever manifest resolved first. Verified
that npm does not hoist for global installs and that the `playwright`
wrapper ships no types/types.d.ts, so `npm i -g playwright` made the pane
call a current build "predates browser.bind()". The probe now hops from
the wrapper to its nested playwright-core.
The messages no longer offer `@playwright/mcp` as a way through setup: it
bundles a playwright-core that binds but never `@playwright/cli`, so that
route could not have worked. It is named only for what it does do.
New `install.rs` + two commands do the setup, streaming on the existing
`container-progress` event and re-probing on success:
* playwright + @playwright/cli into /workspace as `claude`, --no-save.
/workspace is not a bind mount (projects mount at
/workspace/{mount_name}), so nothing of the user's is touched, no sudo
is needed, and Node resolves it from scripts in the project.
* A browser, as its own action with the size stated first: apt libraries
as root, then the download, then a real headless launch to prove it
works. The base image ships none of Chromium's shared libraries, which
is why a download could succeed and the browser still not start.
Chromium and the Chrome channel are both offered — @playwright/mcp
asks for `chrome` specifically. A certificate failure is reported as a
container trust-store problem rather than a broken install.
Installing is always user-initiated; opening the tab only probes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k
`npx playwright install chromium` downloaded ~150 MB of browser that then
died with "error while loading shared libraries: libglib-2.0.so.0" —
verified, not inferred, against the current image. The image shipped none
of Chromium's shared libraries, which is why `apt install
google-chrome-stable` looked like the cure: apt was quietly installing the
same set as Chrome's own dependencies.
Installing them at runtime instead converges on the worst possible state.
The libraries land in the container's writable layer, so they are re-paid
after every Reset and *lost* on base-image migration, which replays apt
from a manifest. The browsers ride in ~/.cache/ms-playwright, inside the
home volume, and survive both — leaving a 400 MB browser present with its
libraries gone. So the libraries are baked and the browsers are not: each
half now lives where it already persists.
The layer runs `npx --yes playwright@latest install-deps chromium` rather
than a hand-written apt list. Ubuntu 24.04's 64-bit-time_t transition
renamed a swathe of these packages (libasound2t64, libatk1.0-0t64,
libglib2.0-0t64, …) and a new Chromium dependency would drift straight back
into the launch failure this exists to prevent; letting Playwright name its
own dependencies is self-maintaining. It sits immediately after Node — npx
is its only prerequisite — and well above the shim COPYs, so editing a shim
does not re-run it.
The `--dry-run` that follows is a build-time assertion, not decoration: on a
platform Playwright has no list for, `install-deps` prints a warning and
returns having installed **nothing, with exit status 0**. Without the
assertion that ships a broken image behind a clean build log.
Measured, on a build of this file with the layer applied over an otherwise
identical image: +99 packages, +334 MiB unpacked and +119 MiB compressed
(2950 → 3284 MiB, 759 → 878 MiB). Two thirds of that is not reachable by
trimming — libgbm1, which Chromium needs, pulls mesa-libgallium, which
pulls libllvm20. A chromium-only apt list measures 247 MiB against
install-deps' 341 MiB; the ~94 MiB difference is xvfb and the CJK/emoji
fonts, kept because the base ships no fonts at all and every page this
feature exists to display would otherwise render as tofu.
Verified on real builds, both architectures: a `--platform linux/arm64`
build of this file installs the same 99 packages and passes the same
assertion. On the new amd64 image, `playwright install chromium` with no
`--with-deps` and no `install-deps` launches headless Chromium 151.0.7922.34
and loads a page; on the old image the identical script fails on
libglib-2.0.so.0.
`install.rs` no longer runs `install-deps` unconditionally — that would be a
minutes-long apt run for nothing on a current image. It asks
`install-deps --dry-run` first and skips the install when everything is
present, saying which of the two happened on the progress stream. The check
is Playwright's rather than a probe of our own for library names, so check
and fix cannot disagree about what the dependency set is. Note that
`--dry-run` exits 0 both when everything is installed and when Playwright
has no list for the platform, so the verdict is read from its output.
Containers on older images stay the normal case until people migrate, and
they still work: on such an image the simulation cannot even resolve the
package names (the index is cleaned in every base image), which reports as
"couldn't tell" and installs — the right answer.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k
jknapp
merged commit 247f03b48c into main2026-08-10 18:19:28 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
A user installed Playwright and the pane still reported it missing, then needed six manual steps to get a browser working. Two detection bugs and a setup gap.
Detection bug 1 — npx installs were invisible. The probe searched
/workspace, cwd,$HOME/node_modulesandnpm root -g. Butclaude mcp add … npx @playwright/mcpnever installs into anynode_modules; npx caches under~/.npm/_npx/<hash>/node_modules. Those roots are now searched (honouringnpm_config_cache).Detection bug 2 — a current Playwright reported as too old. The probe read
has_bindfrom whichever manifest resolved first. npm does not hoist for global installs, and theplaywrightwrapper ships notypes/types.d.ts— sosudo npm i -g playwrightmade the pane claim the build predatesbrowser.bind(). That is almost certainly why the user then added local dev deps on top. The probe now hops from the wrapper to its nestedplaywright-core.Setup is now two clicks. A new install action runs in the container and streams progress: packages first, then browsers as a separate step with sizes stated before the click.
install-depsruns as root — the missing OS libraries are the real reason plainnpx playwright install chromiumproduced a pile of errors; the image ships none of them. Ends with a real headless launch, because "installed but unlaunchable" was the actual failure mode.Both Chromium and the Chrome channel are offered with who-needs-which spelled out —
@playwright/mcpspecifically wantschrome, so it is a genuine requirement for that path, not a workaround.Guidance corrected. The messages previously suggested
@playwright/mcpas an install route; it can never satisfy the viewer requirement. It now appears only in its true role. A test asserts no message offers it as an install route.Certificate errors are classified distinctly — behind a TLS-intercepting proxy a user should not be told Playwright is broken.
Verification beyond unit tests: the embedded probe was run against a genuine npx cache, a genuine
npm i -glayout, a local tree from a subdirectory, and an empty container; and a real Chromium was downloaded and launched.212 Rust tests, 279 frontend tests.
🤖 Generated with Claude Code
https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k
Detection missed the npx cache, so a Playwright installed through Claude Code's MCP setup (`npx @playwright/mcp@latest`, which unpacks into ~/.npm/_npx/<hash>/node_modules and no node_modules at all) was invisible. The probe now globs that cache alongside the existing roots and reports every root it consulted. It also read `has_bind` off whichever manifest resolved first. Verified that npm does not hoist for global installs and that the `playwright` wrapper ships no types/types.d.ts, so `npm i -g playwright` made the pane call a current build "predates browser.bind()". The probe now hops from the wrapper to its nested playwright-core. The messages no longer offer `@playwright/mcp` as a way through setup: it bundles a playwright-core that binds but never `@playwright/cli`, so that route could not have worked. It is named only for what it does do. New `install.rs` + two commands do the setup, streaming on the existing `container-progress` event and re-probing on success: * playwright + @playwright/cli into /workspace as `claude`, --no-save. /workspace is not a bind mount (projects mount at /workspace/{mount_name}), so nothing of the user's is touched, no sudo is needed, and Node resolves it from scripts in the project. * A browser, as its own action with the size stated first: apt libraries as root, then the download, then a real headless launch to prove it works. The base image ships none of Chromium's shared libraries, which is why a download could succeed and the browser still not start. Chromium and the Chrome channel are both offered — @playwright/mcp asks for `chrome` specifically. A certificate failure is reported as a container trust-store problem rather than a broken install. Installing is always user-initiated; opening the tab only probes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KSP2KNPhuWKQ4DL5TZEn3k