selkies/skills/chrome/SKILL.md
Google Chrome with DevTools on port 9222, Chrome DevTools MCP on port 9224, and browser-open helper. Use when working with Chrome, CDP, browser automation, or DevTools Protocol.
npx skillsauth add overthinkos/overthink-plugins chromeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
| Property | Value |
|----------|-------|
| Dependencies | supervisord |
| Composed candies | chrome-devtools-mcp |
| Ports | 9222 (CDP, via cdp-proxy), 9224 (Chrome DevTools MCP, via chrome-devtools-mcp) |
| CDP Proxy | cdp-proxy on 0.0.0.0:9222 → Chrome on 127.0.0.1:9223 |
| Volumes | chrome-data -> ~/.chrome-debug |
| Security | shm_size: 1g, memory_max: 6g, memory_high: 5g, memory_swap_max: 2g |
| Install files | charly.yml, task:, cdp-proxy, chrome-wrapper, chrome-restart, browser-open |
| Variable | Value |
|----------|-------|
| CHROME_FLAGS | --ozone-platform=wayland --enable-features=UseOzonePlatform,VaapiVideoDecodeLinuxGL,VaapiIgnoreDriverChecks |
| BROWSER | browser-open |
PATH additions: ~/.local/bin
| Variable | Template Value |
|----------|---------------|
| BROWSER_CDP_URL | http://{{.ContainerName}}:9222 |
Pod-aware: same-container consumers receive http://localhost:9222, cross-container consumers receive http://charly-<image>:9222. Consumed by hermes (env_accept: BROWSER_CDP_URL) for shared browser automation.
| Name | URL Template | Transport |
|------|-------------|-----------|
| chrome-devtools | http://{{.ContainerName}}:9224/mcp | http |
Provided by the auto-included chrome-devtools-mcp sub-candy (29 Chrome DevTools tools via mcp-proxy). Consumed by hermes (mcp_accept: chrome-devtools) for MCP-based browser inspection and automation. See /charly-selkies:chrome-devtools-mcp for tool list and architecture.
When deploying with -i <instance>, the MCP server name is automatically disambiguated to chrome-devtools-<instance> (e.g., chrome-devtools-31.58.9.4). See /charly-core:charly-config for MCP name disambiguation details.
| Variable | Description |
|----------|-------------|
| HTTP_PROXY | HTTP proxy URL for Chrome (e.g., http://proxy:8080) |
| HTTPS_PROXY | HTTPS proxy URL for Chrome (e.g., http://proxy:8080) |
| NO_PROXY | Comma-separated bypass list (e.g., localhost,127.0.0.1,.internal) |
Chrome does NOT natively respect HTTP_PROXY/HTTPS_PROXY environment variables. The chrome-wrapper script translates them to Chrome CLI flags at launch:
--proxy-server=http=<HTTP_PROXY>;https=<HTTPS_PROXY>--proxy-server=<value> (applies to all protocols)NO_PROXY → --proxy-bypass-list=<list> (only added when a proxy is configured)Uppercase takes precedence over lowercase (HTTP_PROXY over http_proxy).
NO_PROXY auto-enrichment: charly config runs enrichNoProxy() before writing the quadlet, adding every deployed container's hostname (charly-<image>, charly-<image>-<instance>) to NO_PROXY. This is required because Chrome does not support CIDR notation in NO_PROXY (unlike curl/requests) — only exact hostnames work, so charly pre-computes the list. Semicolons in user-provided values are auto-converted to commas since Chrome only accepts comma-separated lists. See /charly-core:charly-config (Environment Variable Handling → NO_PROXY enrichment).
# Deploy with proxy
charly config selkies-desktop -e HTTP_PROXY=http://proxy:8080 -e "NO_PROXY=localhost,127.0.0.1"
# Per-instance proxy (separate from base deployment)
charly config selkies-desktop -i proxy \
-e HTTP_PROXY=http://31.58.9.4:6077 \
-e HTTPS_PROXY=http://31.58.9.4:6077 \
-e "NO_PROXY=localhost,127.0.0.1" \
-p 3001:3000 -p 9232:9222
RPM (system): liberation-fonts, vulkan-loader, mesa-dri-drivers, alsa-lib, at-spi2-core, cups-libs, gtk3, libdrm, libxkbcommon, nss, procps-ng, iproute, libva-utils
RPM (tasks:): google-chrome-stable (from Google's direct RPM)
# charly.yml
my-browser:
candy:
- chrome
Usually used via the chrome-sway or sway-desktop composition candies rather than directly.
Web sign-in at accounts.google.com works via CDP + VNC hybrid automation (see /charly-check:cdp for the full recipe). All clicks locate the element with the cdp: coords verb (CDP selector targeting — it reports the desktop coords) and deliver the pointer via a vnc: click step at those coords, and all text input uses a vnc: type step (real OS-level keysym events). Sign-in cookies persist in the chrome-data volume (~/.chrome-debug), surviving container restarts. Use charly remove <image> --purge to clear for a fresh start — just rebuilding the box does not reset volumes.
App Passwords (required for automation): Google accounts with 2FA (now mandatory for most accounts) require a 16-character App Password. App Passwords bypass all verification challenges and 2FA prompts. Set GMAIL_PASSWORD to the App Password in .env.
Windows spoofing effectiveness: Tested with App Password — no CAPTCHA or verification challenges were triggered. The three-level identity spoofing (User-Agent header, navigator.platform JS, Sec-CH-UA-Platform headers) successfully prevents Google from flagging the sign-in as suspicious.
Fresh profile first-run: A fresh chrome-data volume triggers Chrome's first-run flow: a separate dialog window ("Make Google Chrome the default browser") + chrome://intro/ page. The dialog is invisible to CDP and must be dismissed via sway focus + VNC Return key before proceeding.
Chrome Sync: Fully supported. After sign-in, Chrome navigates to chrome://sync-confirmation/ (a chrome:// page, not a native dialog). Click #confirmButton ("Yes, I'm in") with --vnc to enable sync. Sync state persists in the chrome-data volume.
All Chrome launches go through chrome-wrapper (~/.local/bin/chrome-wrapper), which adds CDP flags (--remote-debugging-port=9223), Windows User-Agent spoofing, and GPU detection. A symlink ~/.local/bin/google-chrome-stable -> chrome-wrapper ensures Chrome always uses the wrapper regardless of how it's launched (sway exec, desktop file, app menu, or Chrome's self-restart after close/crash). The wrapper calls /opt/google/chrome/chrome directly to avoid recursion through the symlink.
The chrome-wrapper detects the sway renderer by reading Sway's /proc/<pid>/environ for WLR_RENDERER. On NVIDIA with gles2 (the default), Chrome gets full GPU acceleration. The wrapper sets NVIDIA EGL vars and VAAPI flags automatically.
If WLR_RENDERER=pixman is detected (e.g., explicit override via WLR_RENDERER env var), the wrapper strips NVIDIA EGL vars and VAAPI flags so Chrome falls back to software rendering naturally. --disable-gpu must NOT be used — it breaks CDP tab creation.
The detection uses pgrep -x sway (exact process name match — NOT pgrep -f which matches sway-wrapper and sway-autotile).
Chrome 146+ rejects HTTP requests to the DevTools API when the Host header contains a non-localhost, non-IP hostname (e.g., charly-selkies-desktop:9222). This breaks cross-container CDP access via env_provide: BROWSER_CDP_URL.
Architecture: A cdp-proxy Python supervisord service listens on 0.0.0.0:9222 and forwards requests to Chrome on 127.0.0.1:9223. Chrome binds only to the internal loopback address.
Host header rewrite: Incoming requests with container hostnames (e.g., Host: charly-selkies-desktop:9222) are rewritten to Host: localhost:9223 before forwarding to Chrome, satisfying Chrome's localhost-only check.
Response URL rewrite: Chrome's DevTools HTTP API returns WebSocket URLs containing ws://localhost:9223/... in JSON responses. The proxy rewrites these to ws://<client-host>:9222/... (using the original Host header from the client request) so that cross-container WebSocket connections route back through the proxy.
Content-Length correction: After URL rewriting, response body sizes change. The proxy recalculates and corrects the Content-Length header to match the rewritten body.
The chrome candy depends on supervisord (not socat) to run cdp-proxy as a managed service.
The wrapper cleans ShaderCache and GpuCache directories from the Chrome profile on startup. Corrupted caches (from pkill -9 or hard container stops) cause secondary SIGILL crashes even with correct GPU flags. Cleaning on every launch prevents this.
The browser-open helper (set as BROWSER env var) handles three states:
swaymsg exec chrome-wrapper with SWAYSOCK discovery (glob on /tmp/sway-ipc.*.sock, newest first).Uses pgrep to detect Chrome process state before deciding whether to launch. This is used by OAuth flows (BROWSER=browser-open causes OAuth URLs to auto-open in Chrome via CDP).
The chrome-restart script kills any running Chrome and relaunches it via chrome-wrapper with all proxy, CDP, and user-agent settings intact. Used by waybar's custom/chrome button (see /charly-selkies:waybar) for one-click Chrome restart.
Compositor detection: Tries swaymsg exec first (sway — needs IPC context for proper window management), falls back to direct chrome-wrapper & (labwc). Uses the same SWAYSOCK discovery pattern as browser-open and waybar-wrapper (ls -t /tmp/sway-ipc.*.sock).
Kill sequence: SIGTERM first, then SIGKILL after 1 second for stubborn processes. Matches Chrome by chrome.*remote-debugging-port pattern (same as browser-open).
# Manual usage (inside container)
chrome-restart
# Via charly shell
charly shell selkies-desktop -i 45.39.130.177 -c "chrome-restart"
When chrome-restart is not enough: Chrome uses memfd_create /
anonymous shared memory extensively (IPC buffers, Skia texture pools,
compositor tiles). These memfds are reference-counted — when a Chrome
child dies (OOM kill or SIGKILL), any surviving peer that still has the
memfd mapped keeps the pages pinned. The browser process and zygote
inherit memfds across renderer crashes. Over multiple crash cycles this
accumulates as "orphan shmem" inside the container's cgroup, and
chrome-restart cannot release it because the container (and therefore
the memory namespace) persists. The only way to clear it is to restart
the whole container, which tears down the cgroup:
systemctl --user restart charly-selkies-desktop.service
# or per-instance
systemctl --user restart charly-selkies-desktop-192.241.92.221.service
This container restart is manual (or via charly update); see "Chrome supervision
(selkies-core)" below for how the [program:chrome] service relaunches Chrome on
ordinary exits.
The chrome candy ships cgroup caps in its security: block:
| Directive | Value | Purpose |
|-----------|-------|---------|
| memory_max | 6g | Hard OOM threshold — cgroup-scoped, no host impact. |
| memory_high | 5g | Soft limit — reclaim pressure kicks in before OOM so Chrome can shed caches instead of being hard-killed. |
| memory_swap_max | 2g | Caps swap usage so a runaway tab can't drag the host into swap thrash. |
| shm_size | 1g | Existing /dev/shm sizing — unchanged. |
Override per box or per instance via charly config flags (see /charly-core:charly-config
"Resource Caps"). Merging follows the smallest-wins rule.
On the selkies streaming desktop, Chrome is launched + supervised by a
[program:chrome] supervisord service declared in the selkies-core candy
(/charly-selkies:selkies-core) — shared by both selkies flavors (labwc via
selkies-desktop, KDE Plasma via selkies-kde-desktop). sway-browser-vnc
launches Chrome via the separate chrome-sway candy and is not supervised this way.
The selkies-core service:
restart: always (autorestart) — relaunches Chrome on any exit, including the
clean self-exit a Chrome started during the nested compositor's startup-race
produces (the relaunch lands post-settle, where Chrome stays up).autostart defaults true and is self-synchronizing: chrome-wrapper polls
for the wayland-0 client socket itself, so no per-flavor supervisorctl start
handoff is needed.start_secs: 5 + start_retries: 3 — a run that lasts longer than 5 s resets
the retry budget, so the single startup-race self-exit never trips FATAL.A genuinely wedged crash loop (orphan memfd shmem accumulation) is cleared by
restarting the whole container, which tears down the cgroup — see "When
chrome-restart is not enough" above.
Chrome launches with a Windows User-Agent and loads the chrome-windows extension to present a consistent Windows identity:
Mozilla/5.0 (Windows NT 10.0; Win64; x64)... via --user-agent flag"Win32" via content script (chrome-windows/content.js)"Windows""Windows" via declarativeNetRequest rules (chrome-windows/rules.json)This reduces the chance of Google flagging the sign-in as suspicious (Linux + automation fingerprint).
Most boxes use gles2 on NVIDIA headless — Chrome gets full GPU acceleration. VNC boxes (sway-desktop-vnc) use pixman instead — Chrome falls back to software rendering (chrome-wrapper auto-detects WLR_RENDERER=pixman and strips NVIDIA EGL vars). No special handling needed — the chrome-wrapper adapts automatically to both renderers.
Chrome 147 introduced a breaking change to the CDP HTTP API:
/json/new requires PUT — GET /json/new?<url> returns 405 Method Not Allowed. Use PUT /json/new?<url> instead. This affects programmatic tab creation via curl or HTTP clients.cdp-proxy rewrites headers to satisfy this check (see CDP Proxy section above).# Chrome 147+: create new tab (PUT required)
curl -s -X PUT "http://localhost:9222/json/new?https://example.com"
# List tabs (GET still works)
curl -s "http://localhost:9222/json/list"
The cdp: check verb now shows diagnostics on connection failure: checks Chrome process, cdp-proxy status, and port binding. Hints point to relaunching Chrome with a wl: exec step running chrome-wrapper (not charly shell with bare swaymsg) for manual Chrome restart.
chrome-sway / sway-desktop composition (used in sway-browser-vnc)/charly-selkies:chrome-devtools-mcp — Chrome DevTools MCP server (auto-included via candy:)/charly-infrastructure:supervisord — required dependency for cdp-proxy service/charly-hermes:hermes — consumes BROWSER_CDP_URL via env_accept and chrome-devtools via mcp_accept/charly-selkies:selkies-desktop-layer — desktop metalayer composing chrome with labwc, pipewire, waybar, etc./charly-check:cdp — Chrome DevTools Protocol automation (click, type, check, screenshot)/charly-core:shell — Interactive shell to access Chrome/charly-check:vnc — VNC automation (delivers the pointer for cdp:-located clicks; a cdp: coords step reports the desktop X/Y, then a vnc: click step delivers the click there)/charly-check:wl — Wayland automation (delivers the pointer for cdp:-located clicks via wlrctl; a cdp: coords step reports the desktop X/Y, then a wl: click step delivers the click there)/charly-core:charly-config — Proxy deployment, normalizeNoProxy() auto-conversion, sep:"none" env handling/charly-build:charly-mcp-cmd — the auto-included chrome-devtools-mcp sub-candy exposes 29 tools via Streamable HTTP on port 9224; run the declarative 2-check suite via charly check live <image> --filter mcpUse when the user asks about:
browser-openchrome candy, CHROME_FLAGS, or shm_size/charly-image:layer — candy authoring reference (charly.yml schema, task verbs, service declarations)/charly-check:check — declarative testing (check: block, charly check box, charly check live)tools
Use when authoring or modifying a charly PLUGIN — a candy with a `plugin:` block that contributes Providers (verbs/kinds/deploy-targets/steps/builders/commands), its own CUE schema, builtin (compiled-in) or external (out-of-tree git repo). Covers the unified Provider model, the per-plugin CUE-schema contract (single source → Go params for dev + schema-over-Describe RPC for runtime), the SDK, and the loader.
tools
The CUE data-validation / configuration CLI (cue), pinned to v0.16.1. Use when working with the cue candy, installing the cue binary into a box or onto a target:local dev host, or running the offline schema-vendoring pipeline that feeds charly's egress validation.
tools
CUE EGRESS validation — validating (and, where it adds value, generating) the config files charly WRITES to a system BEFORE the bytes hit disk. MUST be invoked before working on charly/egress.go, the vendored schemas under candy/plugin-egress/egress-schemas/vendor/, the ValidateEgress / registerVendoredEgressKind path, the offline `task cue:vendor` pipeline, or adding an egress schema for any written artifact (cloud-init, k8s manifests, traefik routes, runtime config, install ledger, systemd/quadlet units, ssh_config, libvirt XML).
tools
Kubernetes cluster-probe declarative check verb — the `kube:` check verb (nodes, pods, ingress, storage class, addon health, apply/delete, and arbitrary resource GETs) served out-of-process by the candy/plugin-kube plugin (vendored client-go; no external kubectl required).