{"id":"4dd4ead1-df8c-4b86-9f17-b8b82d44fa6e","entityType":"agent","slug":"clawhub-drumrobot-web-browser","name":"web-browser","canonicalUrl":"https://www.xpersona.co/agent/clawhub-drumrobot-web-browser","canonicalPath":"/agent/clawhub-drumrobot-web-browser","generatedAt":"2026-10-10T10:42:28.109Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T05:13:16.327Z","emptyReason":null},"description":"Environment-aware browser operations. Detects wmux/cmux/tmux and routes to backend. Topics — ui-test (snapshots, click/fill, shadow DOM), credential-issue (login via backend -> wait sign-in -> issue/refresh token/secret). Use when: \"browser\", \"web-browser\", \"ui-test\", \"credential-issue\", \"playwright\", \"chrome-devtools\", \"UI check\", \"browser test\", \"screen verify\", \"Playwright test\", \"shadow DOM cascade\", \"::part not working\", \"CDP trace\", \"issue token\", \"service credential\", \"open login screen\", \"PAT refresh\", \"scope expansion\", \"device-code auth\", \"browser device-code\", \"GitHub social login\".","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.7K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17ay1v6v88r2m102pvvc44gz183qcrm:web-browser","sourceUrl":"https://clawhub.ai/drumrobot/web-browser","homepage":"https://clawhub.ai/drumrobot/skills/web-browser","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/drumrobot/web-browser","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/drumrobot/skills/web-browser","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":64,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"web-browser technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T05:13:16.327Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T05:13:16.327Z","emptyReason":null},"stars":null,"forks":null,"downloads":1662,"packageName":null,"latestVersion":"0.2.10","tractionLabel":"1.7K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T05:13:16.318Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T05:13:16.327Z","lastCrawledAt":"2026-10-10T05:13:16.318Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T05:13:16.318Z","lastVerifiedAt":null,"highlights":[{"version":"0.2.10","createdAt":"2026-10-06T07:55:49.382Z","changelog":"web-browser v0.2.10 - Added explicit privacy gate: always ask for user approval before capturing any credential-input or sign-in screen (accessibility snapshot, screenshot, or full content read). - Clarified self-checks and table guidance for credential field leaks (browser autofill → accessibility tree plaintext). - Condensed and focused top-level skill description for quicker understanding. - Updated documentation for credential-issue: hardened workflow routing and visibility priorities. - Removed obsolete skill-card.md file.","fileCount":9,"zipByteSize":48635},{"version":"0.2.9","createdAt":"2026-09-18T15:54:29.383Z","changelog":"web-browser 0.2.9 - Removed the skill-card.md file to streamline documentation. - Updated CHANGELOG.md (details not shown). - No changes to functionality or user-facing features. - Skill behavior and usage guidelines remain unchanged.","fileCount":9,"zipByteSize":44622},{"version":"0.2.8","createdAt":"2026-08-26T17:26:00.061Z","changelog":"web-browser v0.2.8 - Documentation files updated for improved clarity and detail in SKILL.md and CHANGELOG.md. - Removed skill-card.md file. - No changes to core logic or functional behavior.","fileCount":9,"zipByteSize":44416},{"version":"0.2.7","createdAt":"2026-08-18T05:42:51.408Z","changelog":"web-browser 0.2.7 - Docs updated: Improved, clarified, or extended guides for `ui-test.md`, `credential-issue.md`, and `ui-test.md`. - File cleanup: Removed outdated or redundant file `skill-card.md`. - No changes to core logic or APIs; this release is primarily documentation and organizational updates.","fileCount":9,"zipByteSize":43172},{"version":"0.2.6","createdAt":"2026-08-09T14:11:30.352Z","changelog":"web-browser 0.2.6 - Updated credential-issue documentation, improving guidance and handling of browser-login assisted credential workflows. - Removed the redundant skill-card.md file for simplification and clarity. - Minor edits to changelog and documentation to reflect recent adjustments and maintain consistency.","fileCount":9,"zipByteSize":40155},{"version":"0.2.5","createdAt":"2026-08-06T09:21:52.817Z","changelog":"web-browser 0.2.5 - Enforces new HARD STOP rules: if API tokens/keys are missing but API automation is possible, fallback must switch to credential-issue (no direct manual UI fallback). - Updated documentation to clarify the required steps in API-capable environments lacking credentials. - Emphasizes that manual user UI clicking is not acceptable if backend API automation is possible post credential issuance. - Removed obsolete skill-card.md file.","fileCount":9,"zipByteSize":37909},{"version":"0.2.4","createdAt":"2026-07-23T13:39:34.733Z","changelog":"web-browser 0.2.4 - Added section describing required user prompt when a login wall is encountered during capture/verification tasks—assistant must ask whether to continue or stop, instead of unilaterally stopping. - Expanded and clarified best practices for handling login and payment flows—distinguishing between login (can continue with user consent) and sensitive credential entry (defer to user/business). - Updated guides to emphasize user decision authority when reports depend on information behind login gates. - Removed outdated file: skill-card.md. - Updated SKILL.md with new critical guidance and best-practice violation examples.","fileCount":9,"zipByteSize":34740},{"version":"0.2.3","createdAt":"2026-07-07T19:12:36.895Z","changelog":"web-browser 0.2.3 - Updated documentation for credential issuance and browser backend environment detection. - Clarified required environment variable checks for cmux detection, emphasizing multi-variable (OR) logic over single-variable detection. - No functional or code changes; this version focuses on refining guides and changelogs for accuracy.","fileCount":9,"zipByteSize":30659}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17ay1v6v88r2m102pvvc44gz183qcrm:web-browser","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17ay1v6v88r2m102pvvc44gz183qcrm:web-browser` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/drumrobot/web-browser before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-drumrobot-web-browser/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-drumrobot-web-browser/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-drumrobot-web-browser/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-drumrobot-web-browser/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-drumrobot-web-browser/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-drumrobot-web-browser/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T10:42:28.105Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-drumrobot-web-browser/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-drumrobot-web-browser/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-drumrobot-web-browser/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-drumrobot-web-browser/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T05:13:16.327Z","emptyReason":null},"readme":"Skill: web-browser\n\nOwner: drumrobot\n\nSummary: Environment-aware browser operations. Detects wmux/cmux/tmux and routes to backend. Topics — ui-test (snapshots, click/fill, shadow DOM), credential-issue (login via backend -> wait sign-in -> issue/refresh token/secret). Use when: \"browser\", \"web-browser\", \"ui-test\", \"credential-issue\", \"playwright\", \"chrome-devtools\", \"UI check\", \"browser test\", \"screen verify\", \"Playwright test\", \"shadow DOM cascade\", \"::part not working\", \"CDP trace\", \"issue token\", \"service credential\", \"open login screen\", \"PAT refresh\", \"scope expansion\", \"device-code auth\", \"browser device-code\", \"GitHub social login\".\n\nTags: latest:0.2.10\n\nVersion history:\n\nv0.2.10 | 2026-10-06T07:55:49.382Z | auto\n\nweb-browser v0.2.10\n\n- Added explicit privacy gate: always ask for user approval before capturing any credential-input or sign-in screen (accessibility snapshot, screenshot, or full content read).\n- Clarified self-checks and table guidance for credential field leaks (browser autofill → accessibility tree plaintext).\n- Condensed and focused top-level skill description for quicker understanding.\n- Updated documentation for credential-issue: hardened workflow routing and visibility priorities.\n- Removed obsolete skill-card.md file.\n\nv0.2.9 | 2026-09-18T15:54:29.383Z | auto\n\nweb-browser 0.2.9\n\n- Removed the skill-card.md file to streamline documentation.\n- Updated CHANGELOG.md (details not shown).\n- No changes to functionality or user-facing features.\n- Skill behavior and usage guidelines remain unchanged.\n\nv0.2.8 | 2026-08-26T17:26:00.061Z | auto\n\nweb-browser v0.2.8\n\n- Documentation files updated for improved clarity and detail in SKILL.md and CHANGELOG.md.\n- Removed skill-card.md file.\n- No changes to core logic or functional behavior.\n\nv0.2.7 | 2026-08-18T05:42:51.408Z | auto\n\nweb-browser 0.2.7\n\n- Docs updated: Improved, clarified, or extended guides for `ui-test.md`, `credential-issue.md`, and `ui-test.md`.\n- File cleanup: Removed outdated or redundant file `skill-card.md`.\n- No changes to core logic or APIs; this release is primarily documentation and organizational updates.\n\nv0.2.6 | 2026-08-09T14:11:30.352Z | auto\n\nweb-browser 0.2.6\n\n- Updated credential-issue documentation, improving guidance and handling of browser-login assisted credential workflows.\n- Removed the redundant skill-card.md file for simplification and clarity.\n- Minor edits to changelog and documentation to reflect recent adjustments and maintain consistency.\n\nv0.2.5 | 2026-08-06T09:21:52.817Z | auto\n\nweb-browser 0.2.5\n\n- Enforces new HARD STOP rules: if API tokens/keys are missing but API automation is possible, fallback must switch to credential-issue (no direct manual UI fallback).\n- Updated documentation to clarify the required steps in API-capable environments lacking credentials.\n- Emphasizes that manual user UI clicking is not acceptable if backend API automation is possible post credential issuance.\n- Removed obsolete skill-card.md file.\n\nv0.2.4 | 2026-07-23T13:39:34.733Z | auto\n\nweb-browser 0.2.4\n\n- Added section describing required user prompt when a login wall is encountered during capture/verification tasks—assistant must ask whether to continue or stop, instead of unilaterally stopping.\n- Expanded and clarified best practices for handling login and payment flows—distinguishing between login (can continue with user consent) and sensitive credential entry (defer to user/business).\n- Updated guides to emphasize user decision authority when reports depend on information behind login gates.\n- Removed outdated file: skill-card.md.\n- Updated SKILL.md with new critical guidance and best-practice violation examples.\n\nv0.2.3 | 2026-07-07T19:12:36.895Z | auto\n\nweb-browser 0.2.3\n\n- Updated documentation for credential issuance and browser backend environment detection.\n- Clarified required environment variable checks for cmux detection, emphasizing multi-variable (OR) logic over single-variable detection.\n- No functional or code changes; this version focuses on refining guides and changelogs for accuracy.\n\nv0.2.2 | 2026-07-02T01:41:06.990Z | auto\n\nweb-browser v0.2.2\n\n- Documentation updates across multiple guides and the main skill file for clarity and accuracy.\n- Removed redundant file: skill-card.md.\n- Clarified backend detection logic and selection process in all documentation.\n- No feature or logic changes; all updates are non-functional and documentation-focused.\n\nv0.2.1 | 2026-06-19T23:10:49.842Z | auto\n\n- Adds comprehensive documentation for environment-aware browser operations and backend selection.\n- Details support for UI testing and credential issuance workflows, with strict user-visibility requirements.\n- Documents environment detection logic for wmux, cmux, and fallback to Playwright MCP.\n- Explains backend routing based on environment variables and CLI presence, including multi-var detection for cmux.\n- Emphasizes that a visible browser session is mandatory when requested by the user; screenshots alone are insufficient.\n\nArchive index:\n\nArchive v0.2.10: 9 files, 48635 bytes\n\nFiles: cdp-trace.md (7016b), CHANGELOG.md (8107b), credential-issue.md (55453b), LICENSE (1063b), scripts/cdp-trace.js (6557b), skill-card.md (2638b), SKILL.md (20120b), ui-test.md (18647b), _meta.json (131b)\n\nFile v0.2.10:SKILL.md\n\n---\nname: web-browser\nmetadata:\n  author: es6kr\n  version: \"0.1.0\"\ndescription: |\n  Environment-aware browser operations. Detects wmux/cmux/tmux and routes to backend. Topics — ui-test (snapshots, click/fill, shadow DOM), credential-issue (login via backend -> wait sign-in -> issue/refresh token/secret). Use when: \"browser\", \"web-browser\", \"ui-test\", \"credential-issue\", \"playwright\", \"chrome-devtools\", \"UI check\", \"browser test\", \"screen verify\", \"Playwright test\", \"shadow DOM cascade\", \"::part not working\", \"CDP trace\", \"issue token\", \"service credential\", \"open login screen\", \"PAT refresh\", \"scope expansion\", \"device-code auth\", \"browser device-code\", \"GitHub social login\".\n---\n\n# Web Browser\n\nEnvironment-aware browser operations skill. Detects the runtime environment and routes to the\nappropriate browser backend, then runs one of two workflows: UI testing/verification (`ui-test`) or\nbrowser-login-assisted credential issuance (`credential-issue`).\n\n## Topics\n\n| Topic | Description | Guide |\n|-------|-------------|-------|\n| ui-test | Snapshot analysis, click/fill/verify, page-state diagnosis | [ui-test.md](./ui-test.md) |\n| cdp-trace | CDP-based closed shadow DOM cascade diagnosis (DOM.getDocument pierce:true + CSS.getMatchedStylesForNode) | [cdp-trace.md](./cdp-trace.md) |\n| credential-issue | service+command param → open login screen → wait for user login → issue access key/token/secret → hand off to automation | [credential-issue.md](./credential-issue.md) |\n\n## Topic Dependencies\n\n```\nweb-browser (Step 0: environment detection — shared by all topics)\n  ├─→ ui-test (UI verification)\n  │     └─→ cdp-trace (extends ui-test for closed shadow DOM)\n  └─→ credential-issue (browser-login-assisted token/key issuance)\n        └─→ chrome-devtools backend preferred (reuses the user's real logged-in session)\n```\n\n- **Step 0 (below) is shared** — every topic detects the backend first, then runs its workflow.\n- `ui-test`, `cdp-trace` are the UI-testing family.\n- `credential-issue` reuses the same backend routing + the user-visibility rule, generalized into a\n  service+command parameterized auth flow.\n- **Authentik SSO verification** (`sso-verify`) is **not** included in this skill — it remains in a\n  separate local-only `sso-verify` skill (user-environment specific, untracked).\n\n## CRITICAL — capturing a credential-input screen requires an explicit ask (HARD STOP)\n\n**Before capturing a sign-in / credential-input screen — accessibility snapshot, screenshot, or any\nfull page-content read — call `AskUserQuestion` and get explicit approval.** Applies to every topic\nin this skill and to every backend.\n\nThe reason is not privacy etiquette, it is a measured leak path: a browser profile's saved-password\nautofill populates the password field, and the accessibility tree renders that field's **value in\nplaintext**. The capture therefore carries a live credential into the transcript even though nothing\nwas typed and no screenshot of characters was taken. `document.body.innerText` does not expose input\nvalues, but the accessibility snapshot does.\n\n| # | Don't | Do |\n|---|-------|-----|\n| 1 | Snapshot a sign-in page to \"see what step we're on\" | Read only `location.href` and `document.title`, or specific marker booleans. Ask before any fuller capture |\n| 2 | Assume an empty-looking form is safe because you typed nothing | Profile autofill fills fields without any typing. Emptiness is not verifiable before the capture that would leak it |\n| 3 | Redact only the field you expected to carry the secret | Apply redaction to **every** returned field — element text, `aria-label`, `title`, `placeholder`, `value`. A secret leaked through an unredacted sibling field is the common failure |\n| 4 | Return page values when a count or boolean answers the question | Prefer structure over content: `hasPasswordField: true`, `blockMarkers: {...}`, `buttonCount: 3` |\n| 5 | Treat \"the user asked me to test the login\" as approval to capture the screen | Testing the flow and capturing the credential screen are separate permissions. Ask for the second one explicitly |\n\n**Self-check (before every snapshot / screenshot / full-text read)**: does the current page accept a\npassword, token, secret, or OTP? → If yes, or if unsure, restrict to URL + title and ask before\ncapturing more.\n\n## CRITICAL — user visibility is the top priority (HARD STOP)\n\n**The primary purpose of browser diagnosis/verification is \"the user sees it on their own screen\"**. Screenshot capture is **supporting evidence**, not a substitute for visibility.\n\n| # | Don't | Do |\n|---|-------|-----|\n| 1 | Launch with `chromium.launch({ headless: true })` and only attach a screenshot in chat | `chromium.launch({ headless: false, slowMo: 500 })` — let the user follow in real time |\n| 2 | \"I showed the user a screenshot, so it's fine\" | screenshot ≠ visible to the user. If the user says \"show me\", open a visible browser + slowMo |\n| 3 | wmux/cmux/Playwright MCP disconnected → fall back to headless CLI | Even on CLI fallback, force `headless: false`. On a Windows desktop OS, a chromium GUI is available |\n| 4 | \"headless is faster and more stable by default\" mindset | Speed costs user visibility. If the user says \"show me\", visibility wins |\n| 5 | Playwright MCP disconnected → CLI fallback auto-selects headless | CLI fallback is also `headless: false`. headless is only for explicit non-interactive cases (e.g., CI assertion) |\n| 6 | SaaS/API task lacks credentials → fallback to manual user UI operation | Do NOT recommend manual user UI clicking when API access is available; fallback to `credential-issue` topic to issue token/key first |\n\n## API-capable environment without credentials — fallback to credential-issue (HARD STOP)\n\n**When a task can be performed via API (e.g., Google Forms API, GitHub API, AWS API), but required API tokens or access keys are missing in the environment, do NOT recommend manual user UI clicking or surrender to direct manual UI operation.** You MUST recommend `credential-issue` topic to issue the access key/token via browser login first, then proceed with backend API automation.\n\n| # | Don't | Do |\n|---|-------|----|\n| 1 | API token missing → \"Please edit/click manually on the website\" | Recommend `credential-issue` topic to issue API token/key via browser login |\n| 2 | Direct UI automation fails → fallback to manual user operation | Check if API automation is available → issue credential via `credential-issue` → execute API |\n\n### Self-check (every time before launching Playwright/chromium)\n\n1. Did the user use a visibility request keyword such as \"show me\", \"open it\", \"web-browser\", or \"browser test\"? → If yes, force `headless: false`\n2. Is this work interactive verification or diagnosis for the user? → If yes, `headless: false`\n3. headless is justified only when (a) CI assertion (b) the user explicitly said \"in headless\" (c) Playwright MCP is used (the UI shows itself)\n4. screenshot is supporting evidence — it can be attached to a chat report, but it does not replace user visibility\n\n### Violation case (2026-05-28, 1st)\n\nDuring a closed shadow DOM `ak-library` cascade investigation, used a `npx playwright` Bash invocation + `chromium.launch({ headless: true })` and only attached a screenshot in chat. The user requested \"show it via web-ui-test\" and no visible browser was provided. The user reacted angrily that the Chromium UI never appeared.\n\n## Login wall mid-capture — ask before stopping, don't silently defer (HARD STOP)\n\n**When a capture/documentation task (report evidence, purchase/registration flow guide, etc.) hits a screen that requires login, and completing that login would reveal materially different information than what's already captured (e.g., the real final price vs. a promotional pre-login price, actual post-login UI state vs. an assumption), do NOT silently stop and paper over the gap with a deferral disclaimer.** Ask the user via `AskUserQuestion` whether to continue (via interactive login in a visible backend) or whether the pre-login capture is sufficient for the purpose at hand.\n\n| # | Don't | Do |\n|---|-------|----|\n| 1 | Hit a login wall → write \"please have finance/ops enter payment details themselves for security\" and stop, without asking | Decompose the remaining flow: **payment/credential entry** should be deferred to the user/business owner, but **login + viewing the resulting screen** is often just informational — ask which is actually needed before deciding to stop |\n| 2 | Treat \"login\" and \"entering payment info\" as one bundled decision to skip together | They are different risk levels. Login-then-observe (e.g., see the real cart/checkout price) does not require entering card/account credentials — only the latter needs deferral |\n| 3 | Report a pre-login/promotional price or state as if it were final, without flagging the gap | If the login-gated final screen wasn't verified, explicitly flag it (\"actual payment screen not verified — may differ from the listed price\") instead of presenting the pre-login figure as authoritative |\n| 4 | Assume the backend can't support interactive login without checking | Check chrome-devtools connection + visibility (per `credential-issue.md` \"Fresh-login flow\") first; if visible, open the page there and have the user sign in in that same window, then continue capturing |\n| 5 | Decide unilaterally that \"this is good enough\" when the report's factual accuracy depends on the gated screen | If the gap could make a delivered report/guide factually wrong (e.g., a payment-request report citing a price that turns out incorrect), the stop-vs-continue decision belongs to the user, not the assistant |\n\n### Violation case (2026-07-22, 1st)\n\nWhile building a domain-registration payment-request report, captured the domain-search-result page (showing a promotional price) and the login screen, then stopped at the login wall with a disclaimer (\"have finance/ops enter payment details\"), never asking whether to continue via login to verify the real checkout price. The report's stated price differed from the actual payment-screen price. User feedback (paraphrased): \"don't arbitrarily skip capturing screens that require login — ask first.\"\n\n## Known Automation Limitations — SaaS Portal Action-Level CAPTCHA Gates\n\nSome SaaS portals allow full browser login automation but selectively trigger CAPTCHA challenges\non **creation/mutation actions** (not just on login). Document confirmed cases here so agents do\nnot repeat failed automation attempts.\n\n| Service | Automatable | CAPTCHA-blocked | Fallback |\n|---------|-------------|-----------------|----------|\n| **Discord Developer Portal** | Login (via persistent profile with saved credentials) | **New application creation**, bot token reset | Keep browser visible (`headless: false`); user handles hCaptcha manually; script polls `page.url()` for `/bot` URL and auto-captures token once user navigates there |\n| **Discord Developer Portal** | Reading existing app info, navigating between tabs | _(same)_ | _(same)_ |\n\n### Discord Developer Portal — specific notes (2026-08-18, 1st confirmed)\n\n- **Login**: Playwright persistent context (`launchPersistentContext`) with a saved user data directory\n  retains Discord session cookies. Navigation to `discord.com/developers/applications` succeeds\n  without re-authentication.\n- **Bot creation blocked**: Clicking \"New Application\" and submitting the modal triggers an hCaptcha\n  dialog (e.g. \"Hold on! You are human, right?\"). The `force: true` checkbox click and JS `dispatchEvent`\n  workarounds successfully activate the Create button, but Discord's backend detects the automated\n  browser and intercepts submission with CAPTCHA.\n- **Recommended hybrid flow**:\n  1. Launch Playwright with `headless: false` + `launchPersistentContext` (reuses login session).\n  2. Navigate to the applications page.\n  3. Set up a polling loop watching `page.url()` for the `/bot` path (every 2s, max ~4min timeout).\n  4. Inform user to manually create the application (handle hCaptcha) and navigate to the Bot tab.\n  5. When `/bot` URL is detected, script resumes: click \"Reset Token\" → capture `input[readonly]` value → enable `[role=\"switch\"]` intents → save changes.\n  6. Write token to a temp file → hand off to next automation (K8s Secret injection, etc.).\n\n---\n\n## Step 0: Environment & Tool Priority Resolution (MANDATORY — before any browser action)\n\nResolve which browser capability backend to invoke based on the current environment, prioritizing native/editor plugins over MCP servers.\n\n### Step 0a: Host OS layer — WSL detection (MANDATORY, runs before the Tool Priority Matrix)\n\nThe multiplexer matrix below answers *which tool* drives the browser. It does not answer *which OS\nthe browser process runs on* — and for the CDP-hostile services in\n[credential-issue.md](./credential-issue.md) that second axis decides whether automation works at\nall. A Playwright MCP server started inside WSL drives a **Linux** Chrome, a different fingerprint\nsurface from `chrome-devtools-mcp` attaching to the user's **Windows** Chrome. Resolve this layer\nfirst, record the result, and never carry a CDP-hostile verdict across it.\n\n```bash\n# WSL detection — either signal is sufficient\n[[ -n \"$WSL_DISTRO_NAME\" ]] && echo \"host=wsl\"\ngrep -qi microsoft /proc/version 2>/dev/null && echo \"host=wsl\"\n```\n\n| Host layer | Record as | Consequence for CDP-hostile services |\n|---|---|---|\n| WSL (either signal true) | `host=wsl` | Playwright MCP (Linux Chrome) is a **first-class candidate**, not a skipped backend — measure before escalating |\n| Windows, no WSL signal | `host=windows` | The documented Windows `chrome-devtools-mcp` findings apply as written |\n| macOS / Linux native | `host=native` | Unmeasured for these services — treat as unknown and measure before asserting |\n\n| # | Don't | Do |\n|---|-------|-----|\n| 1 | Jump straight to the Tool Priority Matrix and pick a backend | Resolve `host=` first — it gates the CDP-hostile escalation ladder |\n| 2 | Apply a CDP-hostile verdict recorded on one host layer to another | Verdicts are per host layer. A Windows block is not a WSL block |\n| 3 | Escalate a CDP-hostile service to wmux/cmux/OS-launch while `host=wsl` is untested | On `host=wsl`, try Playwright MCP first and record the outcome before escalating |\n\n### Tool Priority Matrix\n\n| Environment | 1st Priority (Native Plugin / CLI) | 2nd Priority (MCP Server Fallback) |\n|-------------|------------------------------------|------------------------------------|\n| **wmux** | `wmux browser` commands via Bash (User-visible) | Playwright MCP (`mcp__playwright__*`) |\n| **cmux** | `cmux browser` commands via Bash (User-visible) | Playwright MCP (`mcp__playwright__*`) |\n| **Plain / tmux** | Playwright MCP (`mcp__playwright__*`) (Headless default) | Headless Playwright CLI via Bash |\n\n### Backend Availability Check\n\n```bash\n# wmux check\n[[ -n \"$WMUX\" ]] || command -v wmux >/dev/null 2>&1\n\n# cmux check (detect via ANY of these; CMUX_SESSION is NOT set by cmux app)\n[[ -n \"$CMUX_BUNDLE_ID\" || -n \"$CMUX_PANEL_ID\" || -n \"$CMUX_BUNDLED_CLI_PATH\" ]] || command -v cmux >/dev/null 2>&1\n```\n\n### Do & Don't — Browser Backend Selection\n\n| Environment | Detect (ANY true → environment matches) | Do (use this) | Don't (forbidden) |\n|-------------|----------------------------------------|---------------|-------------------|\n| **wmux** | `$WMUX` set OR `command -v wmux` succeeds | `wmux browser open/snapshot/click/type` commands via Bash | Playwright MCP — user cannot see the invisible Playwright window |\n| **cmux** | `$CMUX_BUNDLE_ID` set OR `$CMUX_PANEL_ID` set OR `$CMUX_BUNDLED_CLI_PATH` set OR `command -v cmux` succeeds (e.g. `/Applications/cmux.app/Contents/Resources/bin/cmux`) | cmux browser panel commands | Playwright MCP — same reason |\n| **Plain / tmux** | None of wmux/cmux signals present | Playwright MCP (Step 1 below) | — |\n\n#### cmux detection — multi-var OR rationale\n\ncmux app sets several env vars when launching a shell, **but `CMUX_SESSION` is NOT one of them** (a legacy guess by analogy with `WMUX`). Real vars observed in a cmux-launched shell:\n\n- `CMUX_BUNDLE_ID` (e.g. `com.cmuxterm.app`)\n- `CMUX_PANEL_ID` (UUID per panel)\n- `CMUX_BUNDLED_CLI_PATH` (CLI absolute path)\n- `CMUX_SHELL_INTEGRATION_DIR`\n- `CMUX_AGENT_LAUNCH_*`\n- `GHOSTTY_RESOURCES_DIR` (cmux uses Ghostty-based terminal)\n\n`CMUX_SOCKET` is **set but often empty** — do not use it as the sole signal. Use the OR matrix above.\n\n| # | Don't (single-var assumption) | Do (multi-var OR) |\n|---|-------------------------------|-------------------|\n| 1 | `[ -n \"$CMUX_SESSION\" ]` only check → false negative on cmux app | OR across `CMUX_BUNDLE_ID` / `CMUX_PANEL_ID` / `CMUX_BUNDLED_CLI_PATH` |\n| 2 | Use `CMUX_SOCKET` as detection (empty in many cases) | Treat empty `CMUX_SOCKET` as no-signal; rely on the 3 vars above + CLI presence |\n| 3 | Assume cmux env var name mirrors wmux (`*_SESSION`) | Verify against actual cmux app shell environment — vars differ per terminal multiplexer |\n\n### wmux Browser Commands Reference\n\nWhen `$WMUX` is set, use these instead of Playwright MCP.\n\n**Invocation form**: the rest of this document uses the bare `wmux browser …` form, which is what runs when `wmux` is on `PATH` (the common case). If `wmux` is **not** on `PATH` in the current environment, substitute `node \"$WMUX_CLI\"` for `wmux` in every command below — `$WMUX_CLI` points to the same entry point. The two forms are interchangeable; pick whichever resolves on the current shell and use it consistently.\n\n```bash\nwmux browser open <url>          # navigate (= playwright navigate)\nwmux browser snapshot            # get accessibility tree with @eN refs\nwmux browser click @eN           # click element\nwmux browser type @eN <text>     # type into element\nwmux browser fill @eN <value>    # set input value\nwmux browser get-text            # get page text\nwmux browser screenshot          # capture screenshot\nwmux browser eval <js>           # run JavaScript\nwmux browser back                # go back\nwmux browser forward             # go forward\nwmux browser reload              # reload page\n```\n\n**Workflow**: `browser open <url>` → `browser snapshot` → read tree → `browser click/type @eN` → `browser snapshot` again.\n\n**Refs (`@e1`, `@e2`...) expire after page changes** — always re-snapshot.\n\n### Do & Don't — wmux vs Playwright Mapping\n\n| Action | wmux (Do) | Playwright MCP (Don't in wmux) |\n|--------|-----------|-------------------------------|\n| Navigate | `Bash(\"wmux browser open <url>\")` | `mcp__playwright__browser_navigate` |\n| Snapshot | `Bash(\"wmux browser snapshot\")` | `mcp__playwright__browser_snapshot` |\n| Click | `Bash(\"wmux browser click @eN\")` | `mcp__playwright__browser_click` |\n| Type | `Bash(\"wmux browser type @eN text\")` | `mcp__playwright__browser_type` |\n| Screenshot | `Bash(\"wmux browser screenshot\")` | `mcp__playwright__browser_take_screenshot` |\n| Evaluate JS | `Bash(\"wmux browser eval <js>\")` | `mcp__playwright__browser_evaluate` |\n| Wait for text | Re-snapshot + check | `mcp__playwright__browser_wait_for` |\n\n**Key difference**: wmux browser is visible to the user in real-time on the right panel. Playwright opens an invisible window the user cannot see.\n\n---\n\n\n## Quick Reference\n\nAfter Step 0 backend detection, route to the topic:\n\n| Goal | Topic | Entry |\n|------|-------|-------|\n| Verify a UI change, snapshot, click/fill | `ui-test` | [ui-test.md](./ui-test.md) |\n| Diagnose `::part` not applying / closed shadow DOM cascade | `cdp-trace` | [cdp-trace.md](./cdp-trace.md) |\n| Open a service login → wait for user login → issue access key/token | `credential-issue` | [credential-issue.md](./credential-issue.md) |\n\n**Step execution order**: Step 0 (this file — detect backend + user-visibility rule) → read the\ntarget topic `.md` → follow its procedure. The topic `.md` files hold the actual procedures; this\nfile is the shared backend-detection + index.\n\nFile v0.2.10:_meta.json\n\n{\n  \"ownerId\": \"kn74k8yfvftx6f062qa8fzyd8h8373jd\",\n  \"slug\": \"web-browser\",\n  \"version\": \"0.2.10\",\n  \"publishedAt\": 1791273349382\n}\n\nFile v0.2.10:cdp-trace.md\n\n# CDP Trace — Closed Shadow DOM Cascade Diagnosis\n\nExtract the computed style + matched CSS rules of elements inside a closed shadow DOM directly via the Chrome DevTools Protocol (CDP). Identifies which stylesheet is the actual carrier and verifies cascade entry when `::part(...)` outer-scope selectors fail to apply.\n\n## When to use\n\n- Outer `::part(<name>) { ... }` rules visually have no effect\n- You need the computed style of an element inside a closed shadow DOM\n- You need to identify the carrier (outer document vs shadow-root inject)\n- You need to verify the cascade for `[part=\"...\"]` direct selectors on a web component\n\n## Mechanism\n\n`DOM.getDocument({ depth: -1, pierce: true })` of the Chrome DevTools Protocol (Playwright `newCDPSession`) returns the full DOM tree **including closed shadow roots**. For each `nodeId`, call `CSS.getComputedStyleForNode` + `CSS.getMatchedStylesForNode` to dump the applied rules and the rules that were ignored.\n\n## Quick Reference\n\n```bash\n# 1. Install playwright into .tmp/ and pull headed chromium\ncd <repo>/.tmp && npm init -y && npm install playwright@latest\nnpx playwright install chromium\n\n# 2. Run cdp-trace.js (user-visible browser)\n# --parts (canonical, plural) and --part (legacy singular) are both accepted;\n# the script tolerates either form to match the historical Quick Reference example.\nnode scripts/cdp-trace.js --url http://<target>/<path> --parts \"app-group,card-wrapper\"\n```\n\n## Script pattern\n\n```javascript\nconst { chromium } = require('playwright');\n\n(async () => {\n  // headless: false — user-visible (per web-browser SKILL.md Step 0 user-visibility rule)\n  const browser = await chromium.launch({ headless: false, slowMo: 800 });\n  const page = await browser.newContext({ ignoreHTTPSErrors: true, viewport: null }).then(c => c.newPage());\n\n  await page.goto(URL, { waitUntil: 'domcontentloaded' });\n  // (handle login / auth as needed)\n\n  const cdp = await page.context().newCDPSession(page);\n  await cdp.send('DOM.enable');\n  await cdp.send('CSS.enable');\n\n  // pierce:true — expose closed shadow roots as well\n  const { root } = await cdp.send('DOM.getDocument', { depth: -1, pierce: true });\n\n  // Recursive walk — collect every element carrying a part attribute\n  function walk(node, found = []) {\n    if (!node) return found;\n    const attrs = node.attributes || [];\n    const partIdx = attrs.findIndex((v, i) => i % 2 === 0 && v === 'part');\n    if (partIdx >= 0) {\n      found.push({ id: node.nodeId, name: node.nodeName, part: attrs[partIdx + 1] });\n    }\n    if (node.children) node.children.forEach(c => walk(c, found));\n    if (node.shadowRoots) node.shadowRoots.forEach(s => walk(s, found));\n    return found;\n  }\n  const parts = walk(root);\n\n  for (const p of parts.filter(p => TARGET_PARTS.includes(p.part))) {\n    const cs = await cdp.send('CSS.getComputedStyleForNode', { nodeId: p.id });\n    const matched = await cdp.send('CSS.getMatchedStylesForNode', { nodeId: p.id });\n\n    console.log(`[part=\"${p.part}\"] (nodeId=${p.id}):`);\n    // computed values\n    for (const prop of ['width', 'grid-template-columns', '--app-card-min-width']) {\n      const v = cs.computedStyle.find(c => c.name === prop);\n      if (v) console.log(`  ${prop}: ${v.value}`);\n    }\n    // matched CSS rules (which sheet matched, which property won out)\n    console.log(`  matched CSS rules:`);\n    for (const r of matched.matchedCSSRules || []) {\n      console.log(`    [${r.rule.origin}] ${r.rule.selectorList.text}`);\n      for (const prop of r.rule.style.cssProperties) {\n        if (prop.disabled) continue;\n        console.log(`      ${prop.name}: ${prop.value}${prop.important ? ' !important' : ''}`);\n      }\n    }\n  }\n\n  // Let the user inspect the page and close it (X button) themselves\n  await new Promise(() => {});\n})();\n```\n\n## Interpreting the output\n\n### Matched CSS rules include the outer brand sheet = cascade entry OK\n\n```\n[part=\"app-group\"]:\n  matched CSS rules:\n    [regular] [part=\"app-group\"] (sheet=style-sheet-20984-37)  ← brand!\n      grid-template-columns: 1fr 1fr 1fr !important\n```\n\n→ The `[part=\"app-group\"] { ... }` direct selector reaches the shadow-root cascade.\n\n### Matched CSS rules show only inner shadow stylesheet = outer rule did not enter\n\n```\n[part=\"app-group\"]:\n  matched CSS rules:\n    [regular] [part=\"app-group\"] (sheet=style-sheet-20984-66)  ← inner only\n      grid-template-columns: var(--app-group-template-columns, 1fr)\n```\n\n→ Zero matches for the external `ak-library::part(app-group) { ... }` rule. **Outer `::part` does not enter the cascade** (a Chrome closed-shadow limitation). Switch to a `[part=\"...\"]` direct selector.\n\n## Don't / Do\n\n| # | Don't | Do |\n|---|-------|-----|\n| 1 | Run CDP trace with `headless: true` | `headless: false, slowMo: 800` — user-visibility rule (web-browser SKILL.md Step 0) |\n| 2 | `::part(...)` doesn't work → only report \"no visible effect\" | Use CDP `getMatchedStylesForNode` to confirm whether the cascade is entered and identify the carrier |\n| 3 | Retry the same selector form 5+ times | 0 matched CSS rules = evidence of spec/runtime divergence. Switch carrier immediately (`:host` / `[part=\"...\"]` direct) |\n| 4 | Leave `DOM.getDocument` with `pierce: false` (default) | Set `pierce: true` explicitly — exposes closed shadow roots too |\n| 5 | Inspect only by `nodeId` + computed style | Also dump `matchedCSSRules` — check which sheet, which property, and `!important` |\n\n## Self-check (every time before running a CDP trace)\n\n1. Is the browser launched with `headless: false`? (user-visibility rule)\n2. Did you pass `pierce: true` to `DOM.getDocument`?\n3. Did you call `CSS.getMatchedStylesForNode` after identifying the target element's `nodeId`?\n4. Does the output print every matched rule's sheet id + selector text + property + `!important` flag?\n\n## Exceptions\n\n- The user says \"I just want a quick visual check\" → skip CDP, screenshot only\n- Inspecting a non-closed-shadow element → CDP is overkill; `getComputedStyle` is enough\n\n## Case study (2026-05-28)\n\nCard layout work on an IdP `ak-library` page. The attempt `ak-library::part(app-group) { grid-template-columns: ... !important }` produced zero visual effect. A CDP trace confirmed zero matched rules → outer `::part` could not enter the cascade → switched to `[part=\"app-group\"] { ... }` direct selector → applied immediately. After 5 verification rounds the right carrier was found; this topic was added so future investigations cost fewer rounds.\n\n## References\n\n- [Chrome DevTools Protocol — DOM domain](https://chromedevtools.github.io/devtools-protocol/tot/DOM/)\n- [CDP — CSS.getMatchedStylesForNode](https://chromedevtools.github.io/devtools-protocol/tot/CSS/#method-getMatchedStylesForNode)\n- [Playwright — context.newCDPSession()](https://playwright.dev/docs/api/class-browsercontext#browser-context-new-cdp-session)\n- `~/.agents/rules/authentik-customization.md` — IdP brand CSS carrier selection (one application domain for this topic)\n\nFile v0.2.10:CHANGELOG.md\n\n# Changelog\n\n## [0.2.10](https://github.com/es6kr/skills/compare/web-browser-v0.2.9...web-browser-v0.2.10) (2026-10-04)\n\n\n### Bug Fixes\n\n* **web-browser:** resolve host OS layer before backend, scope CDP-hostile table per host ([#571](https://github.com/es6kr/skills/issues/571)) ([1bd3d66](https://github.com/es6kr/skills/commit/1bd3d662c7b29a82322476f36c9aba7dfb485295))\n\n## [0.2.9](https://github.com/es6kr/skills/compare/web-browser-v0.2.8...web-browser-v0.2.9) (2026-09-18)\n\n\n### Bug Fixes\n\n* **cleanup:** make the session-end report table self-sufficient ([#487](https://github.com/es6kr/skills/issues/487)) ([c4a0255](https://github.com/es6kr/skills/commit/c4a02557fb8de3b32cf337c549f62535dabf824b))\n\n## [0.2.8](https://github.com/es6kr/skills/compare/web-browser-v0.2.7...web-browser-v0.2.8) (2026-08-26)\n\n\n### Bug Fixes\n\n* accumulate 16 patch-level bug fixes and guard enhancements across skills ([d214e5d](https://github.com/es6kr/skills/commit/d214e5dcc7fac1bc07baf3b6cec62999aea732f0))\n* **core:** align workflow steps, next suggestion patterns, and browser topics ([c68d489](https://github.com/es6kr/skills/commit/c68d489d01a79862b8933b4a0542168cf676cd3a))\n* promote next-fix batch (consolidate fabrication guard, session rewind, config-driven PR base) ([7ca0ccb](https://github.com/es6kr/skills/commit/7ca0ccbf13cefafedc33a16a7361756c95f8b8f6))\n\n## [0.2.7](https://github.com/es6kr/skills/compare/web-browser-v0.2.6...web-browser-v0.2.7) (2026-08-17)\n\n\n### Bug Fixes\n\n* promote next-fix staging (30 fixes across 14 skills) ([ee467c0](https://github.com/es6kr/skills/commit/ee467c045d779d7b80d30f160763ec3534a9742b))\n* **web-browser:** credential-issue backend routing — session-existence gate + account-mismatch rule ([e5a9098](https://github.com/es6kr/skills/commit/e5a90986812c90b101a24554f3de9038a59906b4))\n* **web-browser:** document virtualized table bulk row operation pattern ([c5bc8f0](https://github.com/es6kr/skills/commit/c5bc8f0610263a963e8a75bfd30f36f2a5dafa76))\n* **wip:** cross-ref PR-URL and TaskCreate subject repo-qualifier rules ([#186](https://github.com/es6kr/skills/issues/186)) ([4982364](https://github.com/es6kr/skills/commit/49823641a7b08123ebd0325273892bee41bc3280))\n\n## [0.2.6](https://github.com/es6kr/skills/compare/web-browser-v0.2.5...web-browser-v0.2.6) (2026-08-09)\n\n\n### Bug Fixes\n\n* **consolidate:** address CodeRabbit/Copilot review findings on PR [#270](https://github.com/es6kr/skills/issues/270) ([3b11a73](https://github.com/es6kr/skills/commit/3b11a730b5ad68803d35a8264eda540e48265d75))\n* promote accumulated next-fix fixes to main ([95656e9](https://github.com/es6kr/skills/commit/95656e9b551ee0bb77904a0a571d49c53bc01cc9))\n* **web-browser:** add revoke command type to credential-issue topic ([306cf63](https://github.com/es6kr/skills/commit/306cf63752638d2cae7b924978cb036467382b00))\n* **web-browser:** add revoke command type to credential-issue topic ([cbd7121](https://github.com/es6kr/skills/commit/cbd712145d6faf293409df956706b1afae8ef969))\n* **web-browser:** clarify revoke command shape and step mapping ([ed6ce6e](https://github.com/es6kr/skills/commit/ed6ce6e2ec36fea2b4125d67baceacdba3761dcf))\n* **web-browser:** correct stale priority-1 row reference to priority-3 ([#250](https://github.com/es6kr/skills/issues/250)) ([dd823e7](https://github.com/es6kr/skills/commit/dd823e7596fc4a4d1fb51f6fe81ddac4eda06602))\n\n## [0.2.5](https://github.com/es6kr/skills/compare/web-browser-v0.2.4...web-browser-v0.2.5) (2026-08-05)\n\n\n### Bug Fixes\n\n* promote next-fix staging (38 fixes across 16 skills) ([94f8c33](https://github.com/es6kr/skills/commit/94f8c33800ce411ae63e22c5259cdae8435508a4))\n* **web-browser:** CDP-hostile escalation overrides recorded browser preference ([13a13a0](https://github.com/es6kr/skills/commit/13a13a0d6de9a0a9057dbdad1acb9b600710153e))\n* **web-browser:** check locally-recorded preferred browser before naming one in OS-level open ([2b9eef2](https://github.com/es6kr/skills/commit/2b9eef283560c0f0b5b8ec40aab79af9f2629fff))\n* **web-browser:** preferred-browser check + credential-issue fallback for API tasks ([a5a7859](https://github.com/es6kr/skills/commit/a5a7859d6c0c9ebfc2a7a6417b30e6a6299fd3da))\n* **web-browser:** recommend credential-issue instead of manual UI clicking when API access is available ([b127109](https://github.com/es6kr/skills/commit/b127109bcd1fd11c809d1f861c88fcc2cd6c072a))\n* **web-browser:** verify app's actual CLI support before borrowing another tool's flag syntax ([7ce4a28](https://github.com/es6kr/skills/commit/7ce4a28937e1c07e95eecfc511802c445385ee6b))\n* **wip:** cross-ref PR-URL and TaskCreate subject repo-qualifier rules ([#186](https://github.com/es6kr/skills/issues/186)) ([951c1e6](https://github.com/es6kr/skills/commit/951c1e6871e78e226757c6a7ae5ae53efeb7bfb0))\n\n## [0.2.4](https://github.com/es6kr/skills/compare/web-browser-v0.2.3...web-browser-v0.2.4) (2026-07-23)\n\n\n### Bug Fixes\n\n* **next-fix:** accumulate bug fixes for docxport, wip, fix-plan, and hook-kit ([6eec083](https://github.com/es6kr/skills/commit/6eec083b7fbc429bdabcfcc89d7778b185dd7497))\n* skills body bundle — consolidate/fix/hook-kit refinements + English-clean guard hooks (Ralph-loop bypass) ([2e26f41](https://github.com/es6kr/skills/commit/2e26f412a5b963984898e13caaca186c3617ca08))\n* **web-browser:** don't abandon automation on a single login-block signal ([fed542e](https://github.com/es6kr/skills/commit/fed542e600d83979d3e31fde597da13e0ce0e18e))\n\n## [0.2.3](https://github.com/es6kr/skills/compare/web-browser-v0.2.2...web-browser-v0.2.3) (2026-07-07)\n\n\n### Bug Fixes\n\n* **fix,web-browser:** publish-scope edit gate + managed-surface backend detection ([2e28fc4](https://github.com/es6kr/skills/commit/2e28fc4d39953ee6a88f64bd8c8d20907ba01e39))\n* **skills:** review-feedback bundle — consolidate trigger wording + fix wiki paths ([784854e](https://github.com/es6kr/skills/commit/784854e3e07696ca8d16274215004488861862d1))\n\n## [0.2.2](https://github.com/es6kr/skills/compare/web-browser-v0.2.1...web-browser-v0.2.2) (2026-06-30)\n\n\n### Bug Fixes\n\n* **skills:** add procedural guards + standardize description scalar ([#66](https://github.com/es6kr/skills/issues/66)) ([fcc921f](https://github.com/es6kr/skills/commit/fcc921fba3928aad7421ecff888d5dcee5ae5655))\n\n## [0.2.1](https://github.com/es6kr/skills/compare/web-browser-v0.2.0...web-browser-v0.2.1) (2026-06-19)\n\n\n### Bug Fixes\n\n* bundle skill patches across 7 scopes ([f18f47c](https://github.com/es6kr/skills/commit/f18f47c2d05f13b8e3f3ad42675a2dabbb31c824))\n* **credential:** add PAT scope matrix + Settings UI procedure + Service × Store persist matrix ([c61525b](https://github.com/es6kr/skills/commit/c61525b1a2d886b677c85d2638d39a1e2311c142))\n* **web-browser:** compress SKILL.md description + replace credential-issue placeholder ([4fb1148](https://github.com/es6kr/skills/commit/4fb114800991d757c07eb63d1a3d3b8fc19bde4a))\n\n## [0.2.0](https://github.com/es6kr/skills/compare/web-browser-v0.1.0...web-browser-v0.2.0) (2026-06-12)\n\n\n### Features\n\n* decompose workflow/git rules + rename web-ui-test→web-browser ([#50](https://github.com/es6kr/skills/issues/50)) ([e10d48f](https://github.com/es6kr/skills/commit/e10d48fea4e507b95888de44812b53484d32128d))\n\n## [0.1.0] (2026-06-09)\n\nInitial release. `web-browser` is the environment-aware browser-operations skill, succeeding the\nlegacy `web-ui-test` skill (which is retained, local-only, for `sso-verify`).\n\n### Features\n\n* **ui-test**: snapshot analysis, click/fill/verify UI, page-state diagnosis (migrated from web-ui-test).\n* **cdp-trace**: CDP-based closed shadow DOM cascade diagnosis (migrated from web-ui-test).\n* **credential-issue**: new topic — take a service + command as parameters, open the service login\n  screen via the detected backend, wait for the user to sign in, then issue the requested access\n  key / token / secret and hand the result to follow-up automation (aws-cli upload, gh secret set,\n  etc.). chrome-devtools backend preferred for real-session reuse.\n* Shared **Step 0** environment detection (wmux/cmux/Playwright) + user-visibility HARD STOP.\n\nFile v0.2.10:credential-issue.md\n\n# Credential Issue (web-browser topic)\n\nTake a **service** + **command** as parameters, open the service's login screen via the detected\nbrowser backend, wait for the user to sign in, then on a completion signal **issue the requested\naccess key / token / secret** and hand the result to follow-up automation (aws-cli upload, `gh secret\nset`, terraform var injection, etc.).\n\nThis generalizes the \"open the page + user interaction + collect the result\" pattern into a reusable\nparameterized flow. It is the credential-issuance counterpart to [ui-test.md](./ui-test.md).\n\n## Parameters\n\n| Param | Meaning | Example |\n|-------|---------|---------|\n| `service` | The provider whose console issues the credential | `google-forms`, `cloudflare-r2`, `github`, `oci`, `aws`, `authentik` |\n| `command` | What to issue / do once logged in | `issue Google API OAuth token`, `issue R2 S3 token`, `issue fine-grained PAT`, `revoke <key-id>` (see \"Revoke flow\" below) |\n| `login-url` | Direct URL to the issuance page (when known) | `https://console.cloud.google.com/apis/credentials`, `https://dash.cloudflare.com/?to=/:account/r2/api-tokens` |\n| `handoff` | Follow-up automation to run with the issued credential | `gcloud auth print-access-token`, `aws s3 cp`, `gh secret set` |\n\n## Backend selection (Step 0 + credential-specific preference)\n\nDetect the backend via **SKILL.md Step 0** first. For credential issuance the preference order\ndiffers from ui-test, because the user must **sign in** and reusing their real logged-in session is\nfastest:\n\n| Priority | Backend | Why | When |\n|----------|---------|-----|------|\n| 1 | **chrome-devtools** (real session) | Reuses the user's already-logged-in browser session — often no login needed | `chrome-devtools-mcp` connected **AND the instance actually holds a logged-in session** (see session-existence gate below) |\n| 2 | **Default browser** (`Start-Process <url>` / `open <url>`) | Opens the user's real browser (real session, fully interactive) | login-required + chrome-devtools absent |\n| 3 | **wmux/cmux panel** | User-visible panel, interactive | `$WMUX` / `$CMUX_SESSION` set |\n| 4 | Playwright MCP | **Last resort** — invisible window, user cannot log in interactively | only when a persisted/automated session already exists (no fresh login needed) |\n\n**Session-existence gate (HARD STOP — the priority column is conditional routing, not a fixed\nranking)**: each priority's \"Why\" is its **applicability condition**. chrome-devtools ranks 1st\n*because* it reuses a real logged-in session — an MCP-launched instance whose `list_pages` shows only\n`about:blank` (or whose target page redirects to a login screen) has **no session to reuse**, so the\nrank-1 rationale is void and a backend that *does* hold a session (e.g., an already-open cmux panel)\noutranks it. Before switching backends mid-flow, verify the destination backend actually holds a\nlogged-in session; if it does not, the switch buys nothing and costs the user a fresh login plus a\nsecond browser window.\n\n**Account mismatch is an account problem, not a backend problem (HARD STOP)**: when the current\nbackend's session is logged in as the **wrong account** (e.g., panel session = account B, credential\nmust be issued under account A), the fix is **account switching inside the same backend** (GitHub\n\"Switch account\" / `login?add_account=1` — the user signs in once in a panel they can see), NOT\nabandoning the backend for another one. A backend swap discards a working session+UI surface and, if\nthe destination is a blank instance, degrades to a fresh-login flow anyway. Only swap backends when\nthe destination verifiably holds the *correct* account's session.\n\n**Managed-surface detection (HARD STOP — before treating `open <url>` as manual)**: on hosts where\ncmux/wmux wraps the system opener, `open <url>` prints a surface handle (e.g.\n`OK surface=surface:N pane=pane:M placement=reuse`). That output means the page opened in a\n**cmux-managed browser surface** — full automation is available via\n`cmux browser --surface <handle> snapshot|fill|click|eval`. Detect via (a) the printed handle in the\n`open` output, (b) `command -v cmux` / `command -v wmux`. Treating a managed surface as a plain\nmanual browser and delegating post-login steps (form fill, Generate click, token copy) to the user\nviolates the \"Boundary: login wait vs token automation\" table and the Phase 3-5 entry gate below.\n\n**Key rule**: Playwright MCP opens an **invisible** window — the user cannot complete an interactive\nlogin there. For any flow that needs a fresh user sign-in, prefer chrome-devtools (real session, or\nfresh `new_page` if visible) or the user's default browser. Do not drive an interactive login through\ninvisible Playwright.\n\n**chrome-devtools with no session still outranks Default browser when visible (HARD STOP)**: \"no\nexisting session\" is not the same as \"unusable for this flow.\" If chrome-devtools-mcp is connected and\nits window is confirmed visible (not headless), open the login/issuance URL there via `new_page` even\nwhen a fresh login is required — the user signs in inside that same window, and automation continuity\n(navigate → fill → click → extract) survives past login. Falling back to `Start-Process`/`open` (OS-level\nlaunch) at this point creates a **disconnected, non-automatable window**: whatever the user does there\nafterward (bucket creation, token generation) cannot be driven or read by any backend tool, forcing a\nfull manual handoff for the rest of the flow. Only use Default browser when chrome-devtools is\ndisconnected or confirmed headless/invisible — see \"Fresh-login flow\" below.\n\n### Fresh-login flow: chrome-devtools `new_page` vs OS `Start-Process` (HARD STOP)\n\n| # | Don't | Do |\n|---|-------|-----|\n| 1 | See no existing session in chrome-devtools (`list_pages` → `about:blank`) and immediately switch to `Start-Process`/OS default browser | Confirm chrome-devtools is connected + visible → open the URL there via `new_page` first. Absence of a session only means the user must log in — it does not disqualify the backend |\n| 2 | Open the login window via OS `Start-Process`, let the user complete everything there, then explain the automatable tool (chrome-devtools) is a \"separate instance\" as if that were an external constraint | If a disconnected window was already created by OS-level open, own the choice explicitly (\"I opened this window in a way I can't automate\") rather than framing it as the automation tool's limitation |\n| 3 | Treat Default browser as the obvious/default choice for any login-required flow | Default browser is the fallback **only** when chrome-devtools is disconnected or confirmed headless — check both before choosing it |\n| 4 | After work has already progressed in a disconnected OS-opened window (bucket created, token page open), abandon it and restart automation in a fresh chrome-devtools window | Mid-flow, prefer continuing in whichever window already holds progress — switch backends going forward only for the *next* fresh-login flow, not by discarding in-progress user work |\n| 5 | See a single anti-automation block screen in chrome-devtools (e.g. Google's \"Couldn't sign you in / this browser or app may not be secure\") and immediately conclude the backend is unusable → open a second, disconnected Default-browser window | A site-level bot-detection message is not the same as \"backend disconnected/headless\" (the only two disqualifying conditions in row 3). Ask the user to retry in the **same visible chrome-devtools window** first (click retry, or reload) — the block is sometimes a one-shot heuristic, not a hard wall, and the window stays automatable if it succeeds |\n\n**Case (2026-07-22)**: chrome-devtools showed Google's \"Couldn't sign you in\" block once; assistant concluded the backend was blocked and opened a separate OS `Start-Process` window, asking the user to log in there instead — creating two windows and abandoning the automatable one. The user, following the original instruction to use \"the browser you opened,\" logged into the **chrome-devtools window** anyway and it succeeded (reached the real cart/checkout page with the actual VAT-inclusive price). A single block screenshot is a data point, not a terminal verdict — retry (or ask the user to retry) in place before downgrading.\n\n**⚠️ Superseded for the services listed below** — see \"CDP-hostile services\" immediately below (see failed-attempts.md \"CDP-hostile services stop-retry\").\n\n### CDP-hostile services — stop retrying, escalate immediately (HARD STOP)\n\n**Some providers actively fingerprint and block CDP-attached (remote-debugging-protocol) browser instances — no amount of matcher-switching (Order 1-4 in the automation cascade above) works around this, because the detected signal is the automation connection itself (`navigator.webdriver`, missing browser extensions/plugins, CDP-specific timing/behavior), not the page's markup.** This is a different failure class from a generic login wall or a one-shot anti-bot heuristic (contrast with the \"Case\" example in the row above, which resolved on retry).\n\n**Known CDP-hostile services** (grows as new cases are confirmed):\n\n**Scope: this table is per host OS layer (SKILL.md Step 0a), not per service alone.** Every row below\nwas recorded on `host=windows` with `chrome-devtools-mcp` attached to the user's Windows Chrome. A\nverdict here does not transfer to `host=wsl`, where Playwright MCP drives a separate Linux Chrome —\nsee \"WSL measurements\" immediately after the table.\n\n| Service | Symptom (`host=windows`, chrome-devtools) | Confirmed working alternative | Notes |\n|---------|-------------------------------------------|-------------------------------|-------|\n| Cloudflare (dash.cloudflare.com and any Cloudflare-fronted site) | Turnstile \"Verifying you are human\" interstitial that does not clear, or clears then re-triggers | **wmux/cmux panel**, when detected (see Backend selection table above); on `host=wsl` the login form is reachable — see below | Self-referential — Cloudflare's own dashboard sits behind Cloudflare's bot protection |\n| Google (accounts.google.com and Google-account-gated consoles) | \"Couldn't sign you in — this browser or app may not be secure\" | **`host=wsl` + Playwright MCP** (measured end-to-end, see below); **wmux/cmux panel**, when detected | The flag is attached to the browser instance, so a separate Linux Chrome is not covered by the Windows finding |\n\n#### WSL measurements (`host=wsl`, Playwright MCP / Linux Chrome)\n\n| Service | Result | Evidence scope |\n|---------|--------|----------------|\n| Google (accounts.google.com → console.cloud.google.com) | **Sign-in completed; console fully drivable** | Reached `signin/challenge/pwd`, completed reauth, then created *and* deleted an OAuth client through the console UI — including the confirm-word delete dialog. No \"browser may not be secure\" screen at any point |\n| Cloudflare (dash.cloudflare.com/login) | **Login form reached; no interstitial** | Email + password fields present, no `challenges.cloudflare.com` iframe, and none of \"Verifying you are human\" / \"Checking your browser\" / \"Access denied\" present. **Sign-in completion not verified** (no credential submitted) — do not read this row as \"login succeeds\" |\n\n**Consequence for the escalation rule below**: while `host=wsl`, do not treat a listed service as\npre-blocked. Try Playwright MCP first and record what actually happens; escalate only on an observed\nblock. The ladder applies unchanged on `host=windows`.\n\n**A recorded preferred browser (see \"Preferred-browser check\" above) does not override this table**: even when a specific desktop app is the user's stated preference, if that app is itself automation/CDP-instrumented (e.g. QA/testing-oriented browsers that embed their own remote-debugging or automation daemon), CDP-hostile services will still block it the same way they block chrome-devtools-mcp — confirmed with a browser built on this kind of architecture failing Cloudflare login. For services in this table, escalate to wmux/cmux (or the documented handoff) regardless of the recorded preferred-browser fact; the preferred-browser check governs the generic OS-level-open case, not this escalation ladder.\n\n**Escalation rule**: on the **first** confirmed bot-check/anti-automation screen (or prior knowledge of bot detection) from a listed service in the current session, do not cycle through Order 1-4 matchers and do not retry in place. Escalate in this order:\n\n0. **wmux/cmux panel, if Step 0 already detected it** — this is not a fallback of last resort here, it is the **preferred** backend for these services (see Backend selection table's priority-3 row above). Try it before considering OS-level browser launch or an environment handoff.\n1. **OS-level Default browser with `--new-window`** (`cmd.exe /c start chrome.exe --new-window \"<url>\"` or `cmd.exe /c start msedge.exe --new-window \"<url>\"` on Windows, or `open \"<url>\"` on macOS) — use when wmux/cmux is not available. MANDATORY `--new-window` on Windows when background Chrome/CDP instances exist; plain `Start-Process` silently routes the URL into existing background process tabs without popping up a GUI window.\n   - **Preferred-browser check before naming an app (HARD STOP)**: the browser names above (Chrome, Edge) are illustrative examples, not a default to apply unchecked. Before launching a specific named app (e.g. `open -na \"<App>\" --args --new-window \"<url>\"` on macOS), check whether the user's actual preferred desktop browser is already recorded (a rule fact, skill data, or durable memory tied to this machine). If recorded, use it. If not recorded, either use the plain OS opener (`open \"<url>\"` with no app name, letting the OS pick its registered default) or ask the user once which app to use — do not silently substitute a well-known browser name (Chrome, Edge, Firefox) as a stand-in for \"the user's browser\".\n   - **Verify the app's actual CLI support before constructing its launch command (HARD STOP)**: once a specific app is identified (recorded preference or a name the user just gave), do not assume it shares Chrome/Edge's flag syntax (`--new-window`, `--args`, etc.) by analogy. A non-standard or unfamiliar app (anything other than a small set of well-known browsers whose CLI surface is already common knowledge) may be a custom Electron app or other wrapper with its own argument parsing that ignores or mishandles borrowed flags. Check the app's actual supported invocation first — `<app-binary> --help`, its bundled documentation, or a documentation lookup tool (e.g. context7) if one is available — before running a launch command built from another tool's convention. Confirm the command actually took effect (the target page/window appeared) rather than trusting a zero exit code, since a misparsed argument can silently no-op instead of erroring.\n2. **Handoff to a different execution environment** (e.g., a different agent harness) when cross-harness execution is explicitly required and neither wmux/cmux nor a fresh OS-level window is viable — write a self-contained handoff document.\n\n| # | Don't | Do |\n|---|-------|----|\n| 1 | Plain `Start-Process chrome.exe <url>` without `--new-window` when background Chrome instances exist | Use `--new-window` or fallback to `msedge.exe --new-window` so OS IPC creates a fresh foreground GUI window |\n| 2 | Output text-only URL instructions when automated CDP browsing is blocked by Cloudflare/Google bot protection | Immediately launch the OS browser (`cmd.exe /c start chrome.exe --new-window \"<url>\"`) via shell command so the page is presented on the user's screen |\n| 3 | Keep cycling Order 1-4 automation matchers against a CDP-hostile service, burning turns on a signal-level block no matcher can bypass | Recognize CDP-hostile services need a **different backend class** (non-CDP), not a different matcher within the same CDP backend |\n| 4 | Assume a prior one-off success generalizes to \"it usually works\" | One success does not override a provider's structural bot-detection design. If the user reports repeated failures for a listed service, trust that over a single historical success |\n| 5 | Jump straight to OS-level `--new-window` or an environment handoff when wmux/cmux was already detected by Step 0 | wmux/cmux outranks both — it is the row-0 escalation, not row 1/2. Skipping past it re-creates the exact gap this table exists to close |\n\n**Self-check addition (before opening any browser for issuance)**: is `service` (or the domain being navigated to) in the CDP-hostile table? → If yes: is wmux/cmux detected (Step 0)? → If yes, use it (escalation row 0) — skip the automation cascade but do NOT skip past wmux/cmux to OS-level browser launch. Only when wmux/cmux is unavailable does the cascade skip straight to OS-level browser launch (row 1).\n\n### Fallback: raw CDP WebSocket scripting when both chrome-devtools-mcp and profile-copy fail\n\nWhen chrome-devtools-mcp's own browser is genuinely **unstable** (resets to a blank page between\ntool calls — no persisted state across calls) and copying the user's real profile cookies into a\nfresh `--user-data-dir` fails (modern Chrome's App-Bound Encryption binds the cookie-decryption key\nto the original profile path, so a copied `Cookies` DB silently fails to decrypt and the browser\nredirects to login), the working fallback is: launch a **debug-enabled Chrome instance with\n`--remote-debugging-port`**, have the **user log in there directly** (this is a fresh, real window —\nnot a copy), then drive the rest of the flow via **raw CDP WebSocket commands** from a script (e.g.\n`uv run --with websockets python -c \"...\"` on Windows), bypassing MCP tools entirely:\n\n1. `Start-Process chrome.exe -ArgumentList '--remote-debugging-port=<port>', '--user-data-dir=<short-path>', '--profile-directory=Default', '<url>'` — **use a SHORT `--user-data-dir` path** (e.g. `C:\\ccdp`, not a deeply nested scratchpad path). Chrome's `Service Worker\\CacheStorage\\...` subpaths are among the longest in a profile and silently fail with `UnknownError: Failed to execute 'open' on 'CacheStorage'` once the full path approaches Windows' `MAX_PATH` (260 chars) — a symptom easy to misattribute to broken extensions instead of path length.\n2. Confirm the CDP endpoint: `curl http://localhost:<port>/json/version`, then `curl http://localhost:<port>/json` to get each page's `webSocketDebuggerUrl`.\n3. User signs in interactively in that real window (screen-visible, normal login/2FA).\n4. Drive the rest (navigate / `Runtime.evaluate` for form fills and clicks / extract the issued token from the page) via a small script sending JSON-RPC messages over the WebSocket — `Runtime.evaluate` with a `document.querySelector(...).click()` expression works for React/SPA forms that don't respond to plain CSS-selector automation.\n5. Use `Log.enable` + `Runtime.exceptionThrown` to read real console errors when something looks broken — don't guess the cause (e.g. \"must be an extension issue\") from a single symptom; enable console/network domains and read the actual error text first.\n\n**PID-scoped process termination (HARD STOP — do not kill by process name)**: when a debug-enabled\ninstance needs to be restarted, **identify its exact PID first** (`netstat -ano | grep <port>` for\nthe port owner, or the PID printed by `Start-Process`) and stop only that PID\n(`Stop-Process -Id <pid> -Force`). `Stop-Process -Name chrome -Force` kills **every** Chrome process\nunder that name — including the user's own, unrelated, already-open browser windows. A real\nincident: this exact command closed a window the user had just reopened after a cookie-copy step,\nrequiring an apology and re-open. Name-based kill is only acceptable when you have first confirmed\n(via a fresh process list) that no other Chrome instance is running.\n\n**Disconnected automatable backend → offer reconnect before degrading**: if the priority-1\nautomatable backend (chrome-devtools) is *disconnected*, do NOT silently fall to the manual\ndefault-browser path. The gap is large — chrome-devtools drives the whole token-generation UI\n(navigate → fill → click Create → snapshot-extract the token), whereas default browser is fully\nmanual (the user does every click). Surface the disconnection and offer to reconnect via `/plugin`\n(chrome-devtools) first; fall to default browser only after the user declines reconnect or it fails.\nThe static priority table picks the highest *currently-connected* backend — but a disconnected MCP is\nreconnectable, so treat \"disconnected\" as a reconnect-offer trigger, not a terminal fact, whenever\nthe flow benefits from backend automation.\n\n## Procedure (automation-first, then login-assisted)\n\n1. **Check stored credentials first (MANDATORY)** — before opening any browser, look in: skill `data/`\n   files, project memory, `.env`, the secret store (`vault kv get`, `gh secret list`). If the\n   credential already exists and is valid, **skip issuance** and go straight to `handoff`.\n2. **Try API/SDK issuance (1st)** — if the provider exposes a token-issuance API and you hold a\n   parent credential with sufficient scope, issue programmatically (no browser).\n3. **Confirm API impossibility** — verify one of: the console is the only issuance path (e.g., R2\n   \"Public Development URL\" + S3 token, PAT issuance is UI-only), the parent token lacks scope, or\n   the SDK does not expose the flow.\n4. **Login-assisted issuance (2nd)** — via the selected backend:\n   - Open `login-url` (or the service console root) in the **visible** backend.\n   - Inspect state (snapshot) — is the user already logged in, or is a login screen shown?\n   - **If login required → wait for the user.** Tell them exactly what to do: \"I opened {URL}.\n     Please sign in and {command}, then tell me the {values}.\" Provide the direct URL. **Do not close\n     the browser** while waiting (an isolated/invisible browser on a login screen is kept open, not\n     closed — let the user sign in).\n   - Wait for the user's completion signal (their message with the issued values, or an\n     AskUserQuestion answer).\n5. **Collect the result** — receive the issued access key / token / secret / endpoint / public URL\n   from the user (or scrape it from the page if the backend can read it post-issuance).\n6. **Persist (HARD STOP — before handoff)** — every issued/refreshed credential MUST land in a reusable secret store so the next session does not re-issue. Persistence comes **before** any revoke discussion. See the Service × Store matrix below; pick the row matching `service`. Missing persistence = the same browser dance every session = procedural defect.\n7. **Hand off to automation** — run `handoff` with the credential (`gh secret set`, `aws s3 cp`, `vault kv put`, etc.) only after step 6 succeeds.\n8. **Forbidden**: ending with only \"please go to {URL} and issue it yourself\" with no browser opened\n   and no follow-up.\n9. **Forbidden**: suggesting revoke/Delete on a freshly issued credential because \"it appeared in chat output\". Chat exposure is downstream of persistence — the credential's job is to be usable across sessions. Revoke is a separate explicit decision, not the default response to exposure.\n\n### Service × Store matrix (Step 6 Persist)\n\nPick the matching row before declaring step 6 complete. If your service isn't listed, default to the generic password-manager / Vault row.\n\n| Service / token type | Primary store (preferred) | Persist command | Reuse path |\n|---------------------|---------------------------|-----------------|------------|\n| GitHub PAT (classic / fine-grained) | **gh CLI keyring** (OS-native — macOS Keychain / Windows Credential Manager / libsecret) | `echo <token> \\| gh auth login --hostname github.com --with-token` | `gh auth token -u <user>` (and Docker uses `~/.docker/config.json` after `docker login` once) |\n| GitHub Actions repo/org secret | **GitHub secret store** | `gh secret set <NAME> -b<token>` | Workflow `${{ secrets.NAME }}` |\n| AWS access key / secret | **Vault** (preferred) or `~/.aws/credentials` profile | `vault kv put secret/aws/<profile> access_key=... secret_key=...` OR `aws configure --profile <name>` | `AWS_PROFILE=<name>` / `vault kv get -field=secret_key secret/aws/<profile>` |\n| Cloudflare R2 S3 token | **Vault** | `vault kv put secret/r2/<bucket> access_key=... secret_key=...` | `vault kv get -field=secret_key secret/r2/<bucket>` |\n| OCI API key | **`~/.oci/config`** (CLI-native) | append profile section + `oci_cli_rc` if needed | `OCI_CLI_PROFILE=<name>` |\n| Authentik admin token | **Vault** | `vault kv put secret/authentik/<env> token=...` | `vault kv get -field=token secret/authentik/<env>` |\n| Vault root/unseal | **External password manager** (Bitwarden / 1Password) — Vault cannot store its own root | manual entry in password manager — never plaintext in repo | password manager retrieval |\n| Slack / Discord webhook | **Vault** or `gh secret set` (if used by GH Actions) | `vault kv put secret/<provider>/webhook url=...` | `vault kv get` |\n| Anthropic / OpenAI API key | **Vault** or `.env` (chmod 600, gitignored) | `vault kv put secret/anthropic key=...` or `echo ANTHROPIC_API_KEY=... >> ~/.env` | `vault kv get` or `source ~/.env` |\n| Generic / unlisted | **Vault** (preferred) → password manager → `.env` (chmod 600, gitignored) | provider-appropriate | provider-appropriate |\n\n**Why this matrix exists**: without a per-service store, every fresh issuance is followed by either (a) the token rotting in chat history (b) re-issuance the next session. Both are procedural defects — persistence is the goal, not a side step.\n\n## Don't / Do\n\n| # | Don't | Do |\n|---|-------|-----|\n| 1 | End with \"issue it from the {service} console yourself\" + stop | Open the console in a visible backend → guide the exact steps → collect the result → run `handoff` |\n| 2 | Drive an interactive login through invisible Playwright MCP | Use chrome-devtools (real session) or the user's default browser for fresh logins. Playwright MCP only when a session already exists |\n| 3 | Close the browser when it lands on a login screen | Keep it open; ask the user to sign in, then continue |\n| 4 | Skip the stored-credential check and open a browser immediately | Check skill data / memory / `.env` / secret store first — reuse if present |\n| 5 | Collect the credential then stop (\"now you have a token\") | Run `handoff` with it + store it for reuse. Issuance is a means, the handoff is the goal |\n| 6 | Hardcode one provider's flow | Parameterize on `service` + `command`. Provider specifics go in the scenarios table / the caller's args |\n| 7 | Leave an issued secret only in chat | Persist to the secret store (`vault kv put`, `gh secret set`) so it is reusable and not lost |\n| 8 | After user login completes, delegate the **token generation steps** (clicking \"New Token\", selecting scopes, clicking \"Create\", copying value) to the user with text instructions | Once logged in, **drive the token-generation UI via the backend** (chrome-devtools `click`/`fill`/`take_snapshot`) and **extract the token from the page snapshot** programmatically. User typing/copying is a fallback when backend extraction fails (e.g., token shown as `••••` masked, password manager intercepts) |\n| 9 | Treat \"wait for user\" as applying to the entire issuance flow | \"Wait for user\" applies to **(a) interactive login** and **(b) token reveal/copy when the token is masked or only shown once outside the DOM**. Token generation form-filling and \"Create\" click are backend-automatable when the user is logged in |\n| 10 | Suggest revoke/Delete the freshly issued credential because it appeared in chat output (\"token exposed → revoke first\") | Persistence wins. Run step 6 Persist (Service × Store matrix) **before** any revoke consideration. Revoke is a separate explicit user decision; chat-exposure-triggered auto-revoke is forbidden. Reusing the token across sessions is the design goal |\n| 11 | Skip step 6 Persist (\"we'll just use it in this session\") | Persist is HARD STOP. Every credential issuance ends in the secret store, not in shell history alone. Future sessions retrieve, not re-issue |\n| 12 | Treat the matrix as PAT-only — pick gh keyring for everything | Match the row to the credential type. AWS keys → vault/`~/.aws`, OCI → `~/.oci/config`, Vault root → password manager (Vault can't store itself), Anthropic → vault/`.env`. Wrong row = unusable persistence |\n\n### Scope expansion / token refresh (HARD STOP — Settings UI first, CLI fallback)\n\n**Token-refresh / OAuth scope expansion is the same shape as new issuance** — open the provider's Settings/Tokens page via the detected backend, let the user edit scopes (or issue a new token), capture the token, hand off. Driving `gh auth refresh` from a Bash prompt is a fragile path (CLI flag mismatches across versions, multi-account switching, device-code UX in nested shells); **prefer Settings UI** for human-in-the-loop control.\n\n#### Trigger forms\n\n| Form | Example | Mapping |\n|------|---------|---------|\n| GHCR pull denied → missing `read:packages` | `docker pull ghcr.io/.../image:tag` → `denied` + `www-authenticate: Bearer scope=\"repository:.../image:pull\"` | service=`github`, command=`pat-scope-add`, args=`read:packages` (default scope-set per git.md PAT matrix) |\n| GitHub PAT scope add via Settings UI | \"PAT needs `workflow` scope to push CI yml\" | service=`github`, command=`pat-edit`, target=token id |\n| `gh auth refresh -s <scopes>` (CLI fallback) | `gh auth refresh -h github.com -s read:packages,repo,read:org,workflow,copilot` (active account only; for inactive account run `gh auth switch -u <user>` first) | service=`github`, command=`refresh-cli`, args=scope list |\n| OAuth re-authorize (3rd party app needs new scope) | Slack/Discord/Notion OAuth app scope upgrade | service=`<provider>`, command=`oauth-reauthorize` |\n| Device-code re-auth (gh, az, gcloud) | `gh auth login --web` / `az login --use-device-code` | service=`<cli>`, command=`device-auth` |\n\n#### Procedure (Settings UI first — Recommended)\n\n1. **Scope-set default** — per git.md PAT scope matrix: `read:packages,repo,read:org,workflow,copilot` (5 cumulative scopes). Narrow only when caller explicitly requires (e.g., `write:packages` only for publish operations).\n2. **Identify token** — `gh auth status` to list accounts and confirm which user/PAT needs scope expansion. For inactive accounts, do not auto-switch — surface the multi-account choice to the user first.\n3. **Open Settings UI in detected backend** — for GitHub PAT scope edit/issue:\n   - Classic PAT edit: `https://github.com/settings/tokens` (token list → select existing → Edit → check missing scopes → Update token → copy new value if regenerated)\n   - Classic PAT new: `https://github.com/settings/tokens/new?scopes=<comma-separated>&description=<note>` (pre-fills scope checkboxes)\n   - Fine-grained PAT: `https://github.com/settings/personal-access-tokens/new`\n   - Drive via cmux/wmux panel or chrome-devtools so the user sees the page in real time.\n4. **Collect token** — receive the new token value via user paste (token reveal screen is shown once). The skill's existing \"wait for user\" rule applies (Don't/Do #5).\n5. **Verify** — re-run the failed operation (e.g. `docker login ghcr.io -u <user> --password-stdin` + `docker pull <image>`) to confirm scope works. Failure = inspect `www-authenticate` header for remaining missing scope.\n6. **Persist** — store token in the secret store (`gh auth login --with-token <`, password manager, Vault) so future runs reuse it.\n7. **Handoff** — chain into the downstream command (the operation that originally hit `denied`).\n\n#### Procedure (CLI fallback — when Settings UI is blocked)\n\nUse only when (a) browser unavailable, (b) explicit user request, (c) automation pipeline. **Verify gh CLI syntax via `gh auth refresh --help` before running** — flags differ across gh versions and there is **no `-u/--user` flag on `refresh`**:\n\n```bash\n# Active account refresh — direct\ngh auth refresh -h github.com -s read:packages,repo,read:org,workflow,copilot\n\n# Inactive account — switch first, then refresh, then switch back\n# (gh auth status does not have a structured --json output for the active user\n#  as of gh 2.x; parse the human-readable output instead and verify the\n#  capture in your own shell before relying on it.)\nACTIVE_BEFORE=$(gh auth status 2>&1 | awk '/active account/{for(i=1;i<=NF;i++) if ($i ~ /^[A-Za-z0-9_-]+$/ && $i != \"active\") {print $i; exit}}')\ngh auth switch -u <target-user>\ngh auth refresh -h github.com -s read:packages,repo,read:org,workflow,copilot\n[ -n \"$ACTIVE_BEFORE\" ] && gh auth switch -u \"$ACTIVE_BEFORE\"\n```\n\nThe device-code prompt appears in the terminal; the user enters it in the browser. Wait for `gh auth status` to show the new scope list.\n\n#### Don't / Do\n\n| # | Don't | Do |\n|---|-------|-----|\n| 1 | Output `gh auth refresh ...` text to the user and stop (\"paste this yourself\") | Open the Settings UI in a visible backend; CLI fallback only when explicitly chosen |\n| 2 | Cite `-u/--user` on `gh auth refresh` (the flag does not exist) | Verify each CLI flag via `<cmd> --help` before placing it in a rule body. Cross-account refresh uses `gh auth switch` |\n| 3 | Refresh only the missing scope (`read:packages` alone) — causes future re-refresh for the next missing scope | Default to git.md PAT matrix 5-scope set; only narrow when explicitly required |\n| 4 | Treat `gh auth refresh` as \"user-only command\" outside skill scope | Skill owns the Settings UI path; CLI is the fallback layer of the same skill, not an out-of-scope shortcut |\n| 5 | Skip when `gh auth status` shows the account is \"logged in\" (assume scope is fine) | \"Logged in\" ≠ \"has required scopes\". Always cross-check the scope list against the failing operation's PAT matrix entry |\n\n### Login provider preference — GitHub SSO first for Azure/Microsoft (HARD STOP)\n\n**Azure DevOps, VS Code Marketplace publisher, Microsoft Learn, Azure portal, and other Microsoft-account-gated services that accept GitHub SSO MUST use the GitHub sign-in option** instead of direct Microsoft account login.\n\n#### Why\n\n- The user's GitHub account (e.g., `DrumRobot`) is already mapped + auth maintained (gh CLI, browser session, PAT). Reuse it instead of a separate Microsoft account login round-trip\n- Microsoft account login often triggers extra MFA prompts / phone verification / device verification. GitHub session is already authenticated on the user's browser\n- Single identity surface = easier secret rotation + audit. Azure DevOps user identity links back to GitHub (`@github` suffix), making it traceable in repo audit logs\n- vsce / ovsx publisher accounts can be linked to either, but consolidating on GitHub keeps the credential trail single-source\n\n#### Don't / Do\n\n| # | Don't | Do |\n|---|-------|-----|\n| 1 | Open `https://login.microsoftonline.com` directly and ask user to enter Microsoft account password | Open the service (Azure DevOps / Marketplace) and click **\"Sign in with GitHub\"** (or equivalent third-party SSO button) |\n| 2 | Drive Microsoft account 2FA flow through automation | Switch to GitHub SSO — fewer hurdles, reuses existing browser session |\n| 3 | Assume \"Microsoft service = Microsoft account\" without checking | Most Azure DevOps orgs accept GitHub SSO. Check the sign-in page for a \"GitHub\" button before defaulting to MS account |\n| 4 | After failed MS account login, retry MS account with different email | If MS account flow fails / requires verification, switch to GitHub SSO immediately |\n\n#### Self-check (before opening a Microsoft service login page)\n\n1. Does the target service accept GitHub SSO? — Azure DevOps ✅, VS Code Marketplace ✅, Microsoft Learn ✅, Azure portal ⚠️ (org-policy dependent — fall back to MS account)\n2. Is the user already signed into GitHub in this browser session? — If yes, GitHub SSO completes in 1-2 clicks (consent screen) vs MS account 3-5 clicks (email → password → 2FA → consent)\n3. On the sign-in page snapshot, look for \"Sign in with GitHub\" / \"Continue with GitHub\" / GitHub Octocat icon → click that, not the MS account input\n\n#### Scenarios\n\n| Service | GitHub SSO URL pattern | Notes |\n|---------|------------------------|-------|\n| Azure DevOps (PAT issuance) | `https://dev.azure.com/<org>/_usersSettings/tokens` → \"Sign in with GitHub\" button on the login screen | The same MS account email backed by GitHub appears as `user@github` in Azure DevOps |\n| VS Code Marketplace publisher | `https://marketplace.visualstudio.com/manage/publishers/<publisher>` → GitHub SSO via the same Azure DevOps identity | Publisher = Azure DevOps org membership |\n| GitHub itself | direct (no SSO needed) | gh CLI session covers it |\n\n### Boundary: login wait vs token automation\n\n| Phase | Who does it | Why |\n|-------|-------------|-----|\n| 1. Authentication (Microsoft / OAuth / SSO sign-in) | User (interactive) | Security: credentials must stay with the user; backend cannot enter passwords or pass 2FA |\n| 2. Navigation to the token issuance page | Backend (`navigate_page` / `new_page`) | Once authenticated, page navigation is automatable |\n| 3. Form fill (token name, scopes, expiration) | **Backend** (`fill` / `click`) | These are deterministic form inputs the caller decided from `command` |\n| 4. Click \"Create\" / \"Generate\" | **Backend** (`click`) | Automatable |\n| 5. Extract the issued token value | **Backend** (`take_snapshot` → parse the textbox containing the token) — fallback to user copy only when the token is masked / behind a copy-only button | The token is visible in the page after generation; extract directly. User-typed input is fragile (typos, partial paste) |\n| 6. Handoff (`gh secret set`, `vault kv put`, local CLI publish) | **Backend** (Bash) | Mandatory automation |\n| 7. Persist for reuse (skill `data/secrets/`, keychain) | **Backend** (Write) | Mandatory automation |\n\n### Backend automation failure cascade (HARD STOP — before falling back to user copy)\n\n**A single backend command failure (e.g., `eval` JS exception) is NOT permission to delegate to user.** When the primary automation matcher fails, **cycle through alternate matchers in this order** before any \"please copy the token yourself\" handoff:\n\n| Order | Matcher | Example (cmux) | Example (chrome-devtools) |\n|-------|---------|----------------|---------------------------|\n| 1 | JS evaluation (DOM query) | `cmux browser eval --script '<js>'` | `evaluate_script` |\n| 2 | CSS selector targeting | `cmux browser fill --selector '<css>'` / `click --selector '<css>'` | `fill` / `click` with `selector` |\n| 3 | Interactive snapshot (ref-based) | `cmux browser snapshot --interactive` → click `@eN` | `take_snapshot` → click ref |\n| 4 | Visual screenshot + bbox coordinates | `cmux browser screenshot` → analyze coords → `click --x --y` (if backend supports) | `take_screenshot` |\n| 5 | User copy fallback | text instructions, user pastes back | text instructions, user pastes back |\n\nOrder 1 failure (most common: `eval` cross-origin or JS exception) → try Order 2 (CSS selector) BEFORE Order 5. Each transition must show **at least one tool call** in the actual workflow — \"I tried JS, it failed, the user can do it\" with no Order 2/3/4 attempts = violation.\n\n| # | Don't | Do |\n|---|-------|-----|\n| 1 | `eval` returns \"JavaScript exception\" → conclude \"automation unavailable\" → user text instructions | `eval` failure = try `fill --selector` / `click --selector` next. Document the JS exception (likely cross-origin), then iterate matchers |\n| 2 | One snapshot returns minimal accessibility tree (form fields invisible) → \"form not automatable\" | `snapshot --interactive` or `snapshot --max-depth N` or CSS selector — extend probe before giving up |\n| 3 | \"User can do it in 30 seconds, faster than backend automation\" | Backend automation is the **mandatory contract** of credential-issue. Speed argument = boundary violation. The Phase 3-5 boundary table is not advisory |\n| 4 | Frame each automation attempt as \"let me try one more thing\" with user as fallback in the same response | The cascade is silent — try Orders 1→4 sequentially in **the same turn**, only emit a user-handoff request when Order 4 also fails |\n| 5 | Treat `open <url>` output containing `surface=`/`pane=` as a plain browser (no automation) and hand every token step to the user | The printed handle IS the automation entry point — reuse it: `cmux browser --surface <handle> snapshot --interactive` → `fill`/`click`/extract. One disconnected backend (e.g. chrome-devtools) does not prove \"no automation\" while cmux/wmux is present |\n| 6 | Drive token generation without verifying WHICH account the page session is logged in as | **Verify the logged-in identity BEFORE clicking Generate** (avatar menu snapshot / `meta[name=user-login]`) and again AFTER issuance (`gh api user` with the new token). Multi-account browsers issue under the wrong identity silently — a mis-issued credential costs a revoke + re-issuance round-trip |\n| 8 | Wrong account detected in the current backend's session → abandon the backend and open the same page in another backend (which turns out to be a blank/no-session instance → fresh-login demand + second browser window) | Account mismatch = switch accounts **inside the same backend** (GitHub `login?add_account=1` in the visible panel). Swap backends only after verifying the destination holds the correct account's live session (`list_pages` non-blank + target page not redirecting to login) |\n| 7 | Assume a CSS-selector `click` on a form submit button took effect because the command returned OK | Form submits often need the **snapshot-ref click** (`snapshot --interactive` → `click \"@eN\"`); verify the effect via URL change / API state, not the click return code |\n\n## Self-check (before opening any browser for issuance)\n\n1. Is the credential already stored (skill data / memory / `.env` / secret store)? → If yes, skip to `handoff`.\n2. Can it be issued via API/SDK with a parent credential? → If yes, do that (no browser).\n3. Console-only? → Pick the backend: chrome-devtools (real session) > default browser > wmux/cmux > Playwright (only with an existing session). **Probe every backend before concluding \"no automation\"**: `command -v cmux` / `command -v wmux`, and inspect the `open` output for a `surface=` handle (managed-surface detection above) — one disconnected MCP is not evidence that automation is absent.\n3.5. **Before any mid-flow backend switch**: does the destination backend actually hold a logged-in session for the **required account**? (`list_pages` non-blank + no login redirect). If not, stay in the current backend — an account mismatch there is solved by in-backend account switching, not by a backend swap (session-existence gate above).\n4. Does the flow need a fresh interactive login? → If yes, the backend MUST be user-visible. Never invisible Playwright.\n5. After collecting the credential, did you **complete step 6 Persist** (Service × Store matrix row matched + persist command executed + reuse path verified)? `handoff` runs only after Persist succeeds.\n6. **Login complete → token generation automation check**: once the user is signed in, did the backend drive the token-generation UI (navigate → fill → click \"Create\" → snapshot the token) instead of writing text instructions for the user to follow? If text instructions were written, that is a violation of the boundary in the table above unless the token is genuinely behind a masked / copy-only UI element.\n7. **Persist-before-revoke check**: am I about to suggest revoke/Delete the freshly issued credential because it appeared in chat? Persistence wins — step 6 first; revoke is a separate explicit user decision, not the default exposure response.\n\n### Phase 3-5 entry gate — pre-handoff tool-call count check (HARD STOP)\n\n**Before composing any AskUserQuestion or text request that asks the user to take action on the token-issuance page (sign in, click Generate, copy the token), audit the transcript for backend automation attempts.** If fewer than 3 backend tool calls in the cascade (Order 1-4 above) have been made for the current `service`, the handoff request is **forbidden** — return to the cascade.\n\n**Forcing function checklist (run BEFORE any user-facing handoff message)**:\n\n1. Count `Bash(cmux browser ...)` / `mcp__plugin_chrome-devtools-mcp_*` / equivalent tool calls in the current `service` flow within this turn\n2. Filter to ones that drove **the token-generation UI** (not just opening the URL or initial snapshot)\n3. If count < 3 across Order 1-4, the handoff message is premature — pick the next matcher and try\n4. Only when 3+ distinct matchers have been attempted (with the actual tool-call evidence) is user copy fallback (Order 5) eligible\n\n| # | Don't | Do |\n|---|-------|-----|\n| 1 | One `eval` JS exception → \"let me ask the user to do it\" | Audit tool-call count first. If <3 automation attempts on the form, try `fill`, `click`, `snapshot --interactive` |\n| 2 | Treat self-check #6 as post-action review (check after writing user instructions) | Apply self-check #6 + this gate **before** composing user-facing text. Pre-action forcing function, not post-action audit |\n| 3 | \"User mentioned the form is open, they can just fill it\" — frame as user convenience | User said the form is open ≠ user wants to fill it. Backend automation contract stays in force |\n| 4 | Justify handoff by token being one-time-shown (Phase 5 user-copy exception) | Phase 5 user-copy fallback applies only when the token is **DOM-invisible** (masked behind `••••`, behind clipboard-only API). Visible plain-text token field is backend-extractable via Order 1-4 |\n\n## Revoke flow (`command: revoke <key-id>`)\n\nRevoking an existing key/token/secret reuses the same backend selection (Step 0) and login-wait UI\npattern as issuance, but the terminal action is a **delete**, not a create — and the delete target is\nan *existing* credential (identified by the `<key-id>` embedded in `command`), not a freshly generated\none. The numbered steps below are revoke-specific and do not map 1:1 onto the issuance Procedure's\nsteps 1-7 — e.g. issuance's step 1 (\"skip to handoff if already stored\") has no revoke equivalent,\nsince there is nothing to skip to when the goal is deletion.\n\n1. **Check for an API-level revoke first** (step 2 of the main Procedure) — many providers expose a\n   revoke/delete endpoint (`gh api -X DELETE`, a cloud provider's key-management API). Prefer it over\n   a browser flow whenever a parent credential with sufficient scope is already available.\n2. **No API path → login-assisted revoke**: open the provider's key/token management console (same\n   backend-selection table as issuance) and, once signed in, **drive the actual revoke click**\n   (Order 1-4 automation cascade, same as the token-generation boundary table — do not delegate the\n   click to the user unless automation genuinely fails).\n3. **Identify the correct entry before deleting — require an exact key-ID match** — revoke targets an\n   existing row in a list. Name or creation-date matching may be used only to *locate* a candidate row;\n   names and timestamps are not guaranteed unique. Before clicking delete, confirm the row's exact key\n   ID matches the caller-supplied identifier (`command`'s `<key-id>`) — if `command` supplied only a\n   name/date and multiple rows match it, **abort and ask** rather than guessing; a wrong-row delete is\n   unrecoverable.\n3.5. **Expect a confirm-word gate on the delete dialog** — several consoles (Google Cloud's credential\n   delete among them) keep the dialog's delete button `disabled` until a literal confirmation word is\n   typed into a text field. A text-matched click (`button:has-text(\"Delete\")`, or its localized\n   equivalent) then resolves to a disabled element and **silently no-ops** — no error, no exception,\n   and the row is still there. Before clicking: read the dialog's buttons and check each one's\n   `disabled` state; if the delete button is disabled, look for the confirm field, read the exact word\n   the dialog demands **off the dialog's own text** and type that, then re-check the button became\n   enabled. Do not assume the word is `Delete`: the console localizes it, so on a non-English locale\n   the required string is the translated verb, and typing the English one leaves the button disabled.\n4. **Verify revocation before reporting success** — after the delete click, re-read the key list (or\n   the specific key's state) and confirm the exact identifier no longer appears / shows revoked. Do not\n   report success from the click alone — some consoles show a stale row until a refresh, the click can\n   silently fail, or (per 3.5) it can land on a disabled button. A no-op delete that is reported as\n   done leaves a credential the caller believes is dead — the worst outcome of this flow.\n5. **No Persist step, but clean up existing local copies** — step 6 (Persist) of the main Procedure does\n   not apply to a revoke: there is no new credential to store. However, a revoked secret must not remain\n   in any local cache (`skill data/`, memory, `.env`, a secret store) — delete or invalidate persisted\n   copies of the just-revoked credential so a stale cached copy isn't picked up on a later \"already\n   stored, skip to handoff\" issuance check. If the revoke was prompted by rotation (issuing a\n   replacement), that replacement follows the normal issuance Procedure (including Persist) as a\n   **separate**, prior or subsequent step — never skip Persist on the new credential because \"we just\n   did a revoke flow.\"\n6. **Report the revoked identifier** (key ID/name) in the completion report — the same way issuance\n   reports the issued credential's identifier — so the user can cross-check against the provider's\n   audit log.\n\n| # | Don't | Do |\n|---|-------|----|\n| 1 | Treat \"no revoke-specific scenario row exists\" as license to skip backend automation and hand the whole flow to the user | Revoke uses the same backend-sel\n\nFile v0.2.10:skill-card.md\n\n## Description:\n\nHelps agents test and inspect browser interfaces, diagnose styling inside closed shadow DOM, and assist with issuing or managing service credentials after user sign-in.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[drumrobot](https://clawhub.ai/user/drumrobot)\n\n### License/Terms of Use:\n\nMIT\n\n## Use Case:\n\nDevelopers and agents use this skill to verify web interfaces, investigate browser styling and shadow DOM behavior, and guide user-approved sign-in before issuing, storing, or revoking service credentials.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Browser captures may expose autofilled passwords or other secrets in page content or accessibility snapshots.\n\nMitigation: Require explicit approval before capturing a sign-in or credential-input screen; minimize captured content and redact sensitive values.\n\nRisk: Credential issuance, scope changes, or revocation can grant excessive access or disrupt an existing integration.\n\nMitigation: Confirm the service, account, requested scope, destination store, and any revoke or delete action with the user before acting.\n\nRisk: Temporary files, environment files, shell arguments, or chat output may expose reusable credentials.\n\nMitigation: Prefer Vault, OS keyrings, or provider secret stores; avoid command-line passwords and plaintext temporary or .env files.\n\nRisk: Browser tracing on untrusted or invalid-TLS login pages and unpinned browser tooling can endanger signed-in sessions.\n\nMitigation: Do not use the CDP trace login path on untrusted or invalid-TLS sites, and pin npm and Playwright dependencies before using logged-in sessions.\n\n## Reference(s):\n\n- [ClawHub web-browser release](https://clawhub.ai/drumrobot/skills/web-browser)\n- [UI testing guide](artifact/ui-test.md)\n- [Credential issuance guide](artifact/credential-issue.md)\n- [CDP trace guide](artifact/cdp-trace.md)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Markdown, Code, Shell commands]\n\n**Output Format:** [Markdown explanations and reports with code or shell commands as needed]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May include browser test results and credential-handling instructions; sensitive values should not appear in reports.]\n\n## Skill Version(s):\n\n0.2.10 (source: server-resolved release, CHANGELOG)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nFile v0.2.10:ui-test.md\n\n# UI Test (web-browser topic)\n\nAnalyze browser snapshots, click/fill/verify UI, diagnose page state. Part of the `web-browser` skill.\n\n> **Backend**: detect the browser backend first via **SKILL.md Step 0** (wmux/cmux panel → user-visible, plain → Playwright MCP). The **user-visibility HARD STOP** also lives in SKILL.md Step 0 — apply it before any browser launch. This topic covers the UI-testing procedure itself.\n>\n> For closed shadow DOM `::part` cascade diagnosis → [cdp-trace.md](./cdp-trace.md). For browser-login-assisted token issuance → [credential-issue.md](./credential-issue.md).\n\n## Required Setup: Playwright Registration (skip if wmux/cmux detected)\n\n**This procedure must be run before all tasks — only when Step 0 determined Playwright backend.**\n\n### Step 1: Check Registration\n\n**Check whether the plugin is already installed first** — if `mcp__playwright__*` tools already exist, code-mode registration is unnecessary:\n\n```\nToolSearch(\"select:mcp__playwright__browser_navigate\")\n```\n\nIf the result returns the `mcp__playwright__browser_navigate` schema → **plugin is installed. Skip Step 2, go to Step 3** (use the `mcp__playwright__*` tools directly).\n\nIf not returned → try registration via code-mode:\n```typescript\nmcp__code-mode__list_tools()\n```\nIf the result contains `playwright` → **go to Step 3** (via code-mode call_tool_chain)\nOtherwise → **run Step 2**\n\n### Step 2: Register Playwright\n\n```typescript\nmcp__code-mode__register_manual({\n  manual_call_template: {\n    name: \"playwright\",\n    call_template_type: \"mcp\",\n    config: {\n      mcpServers: {\n        \"playwright\": {\n          transport: \"stdio\",\n          command: \"npx\",\n          args: [\"@playwright/mcp@latest\"]\n        }\n      }\n    }\n  }\n})\n```\n\nAfter registration, verify that playwright tools appear via `mcp__code-mode__list_tools()`.\n\n### Registration Failure - Must diagnose and resolve the root cause\n\nDo not fall back to alternatives. Diagnose the problem, fix it, then retry:\n\n**Diagnostic steps:**\n```bash\n# 1. Check if npx is available\nnpx --version\n\n# 2. Check package accessibility\nnpx @playwright/mcp@latest --version 2>&1 | head -5\n\n# 3. Check for network issues\nnpm ping 2>&1\n```\n\n**Common failure causes and fixes:**\n\n| Error | Cause | Fix |\n|------|------|------|\n| `transport undefined` | Missing config | Add `\"transport\": \"stdio\"` |\n| `NODE_MODULE_VERSION` mismatch | Node version conflict | Run `npx clear-npx-cache` then retry |\n| `command not found: npx` | Node not installed | Check npx path, use absolute path |\n| Package download failure | Network/registry issue | Check npm registry connectivity |\n| `EACCES` permission error | Permission issue | Check cache directory permissions |\n| \"Another program is using the profile\" / Chrome exits immediately | Previous Playwright Chrome occupying `mcp-chrome` profile | Run **Chrome Profile Lock Recovery** procedure below |\n\n#### Chrome Profile Lock Recovery (Windows)\n\nIf Chrome only shows `about:blank` or exits immediately when launching Playwright:\n\n```bash\n# 1. Kill existing mcp-chrome process (via command-line match)\npowershell -NoProfile -Command \"Get-CimInstance Win32_Process -Filter \\\"Name='chrome.exe'\\\" | Where-Object { $_.CommandLine -like '*mcp-chrome*' } | ForEach-Object { Stop-Process -Id $_.ProcessId -Force }\"\n\n# 2. Delete profile lock files\ncmd /c \"del /F /Q \\\"%LOCALAPPDATA%\\\\ms-playwright\\\\mcp-chrome\\\\SingletonLock\\\" 2>nul\"\ncmd /c \"del /F /Q \\\"%LOCALAPPDATA%\\\\ms-playwright\\\\mcp-chrome\\\\SingletonCookie\\\" 2>nul\"\ncmd /c \"del /F /Q \\\"%LOCALAPPDATA%\\\\ms-playwright\\\\mcp-chrome\\\\SingletonSocket\\\" 2>nul\"\n\n# 3. If still failing, delete entire profile directory\ncmd /c \"rmdir /S /Q \\\"%LOCALAPPDATA%\\\\ms-playwright\\\\mcp-chrome\\\"\"\n\n# 4. If all 3 steps fail → close any open about:blank windows manually and retry\n```\n\n**Auto-detection**: If you see `browserType.launchPersistentContext: Failed to launch` error + `process did exit: exitCode=0` pattern, this is the issue.\n\n#### Fallback: Launch with a new profile path\n\nIf recovery fails, register Playwright with a temporary profile instead of `mcp-chrome`:\n\n```typescript\nmcp__code-mode__register_manual({\n  manual_call_template: {\n    name: \"playwright\",\n    call_template_type: \"mcp\",\n    config: {\n      mcpServers: {\n        \"playwright\": {\n          transport: \"stdio\",\n          command: \"npx\",\n          args: [\"@playwright/mcp@latest\", \"--user-data-dir\", \"%LOCALAPPDATA%/ms-playwright/mcp-chrome-\" + Date.now()]\n        }\n      }\n    }\n  }\n})\n```\n\nTimestamp-based profile → no lock conflicts. Note: cookies/session are reset each time.\n\nDiagnose → fix → re-register. Always resolve before proceeding.\n\n### Step 3: Using Playwright Tools\n\nThe registered Playwright is called via `mcp__code-mode__call_tool_chain`:\n\n```typescript\n// Navigate to page\nmcp__code-mode__call_tool_chain({\n  code: `\n    const result = await playwright.playwright_browser_navigate({ url: 'http://...' });\n    return result;\n  `\n})\n\n// Snapshot (primary use)\nmcp__code-mode__call_tool_chain({\n  code: `\n    const snapshot = await playwright.playwright_browser_snapshot();\n    return snapshot;\n  `\n})\n\n// Screenshot\nmcp__code-mode__call_tool_chain({\n  code: `\n    const screenshot = await playwright.playwright_browser_take_screenshot();\n    return screenshot;\n  `\n})\n\n// Click\nmcp__code-mode__call_tool_chain({\n  code: `\n    const result = await playwright.playwright_browser_click({ ref: 'e123' });\n    return result;\n  `\n})\n\n// Form input\nmcp__code-mode__call_tool_chain({\n  code: `\n    const result = await playwright.playwright_browser_type({ ref: 'e456', text: 'input text' });\n    return result;\n  `\n})\n\n// Wait\nmcp__code-mode__call_tool_chain({\n  code: `\n    const result = await playwright.playwright_browser_wait_for({ text: 'expected text' });\n    return result;\n  `\n})\n\n// Console messages\nmcp__code-mode__call_tool_chain({\n  code: `\n    const logs = await playwright.playwright_browser_console_messages();\n    return logs;\n  `\n})\n```\n\n## Forbidden: API Direct Calls\n\n**All verification must go through Playwright UI interaction.** The purpose of this skill is to test what users actually see and do in the browser.\n\n- `curl`, `fetch`, `httpie` or any direct API call is **prohibited** for verification\n- If a test requires form submission, use Playwright `browser_click` + `browser_type`\n- If a test requires data deletion, do it through the UI (click delete button, confirm dialog)\n- API calls bypass UI bugs (disabled buttons, missing dialogs, broken event handlers)\n\n## Core Responsibilities\n\n### 1. Page State Analysis\n- Take browser snapshots to understand current UI\n- Check for errors in console messages\n- Identify key interactive elements\n\n### 2. Interaction Testing\n- Click buttons, links, and other elements\n- Fill forms and submit data\n- **Auto-save drafts after filling a form (HARD STOP)** — after completing a form fill, immediately trigger the form's own draft/temporary-save action (when one exists) so the input is not lost. Only the final submit is the user's call; filling a form and leaving it unsaved risks losing the entered data on navigation/timeout.\n- Navigate between pages\n- Wait for dynamic content\n\n### 3. Error Detection\n- Check console for JavaScript errors\n- Identify missing elements or broken UI\n- Verify expected content is present\n\n## Workflow\n\n0. **Detect Environment** - Check `$WMUX` / `$CMUX_SESSION` / `$TMUX` (Step 0 above)\n1. **Setup Backend** - wmux: ready immediately / plain: Register Playwright via UTCP\n2. **Snapshot** - Acquire snapshot (wmux: `wmux browser snapshot` / plain: call_tool_chain)\n3. **Analyze** - Identify relevant elements and state from snapshot\n4. **Execute** - Perform requested interactions using the detected backend\n5. **Verify** - Confirm results and detect issues\n6. **Report** - Return concise summary (raw snapshot data prohibited)\n\n## Output Format\n\n**CRITICAL**: Never return raw snapshot data. Always summarize findings.\n\n**Verified URL must be shared**: print the tested URL as text so the user can open it in their own browser. Example: \"Open directly: http://{host}:{port}/{path}\"\n\n### Success Response\n```md\n## UI Verification Result ✅\n\n**Page:** [page title/URL]\n**Status:** OK\n\n### Verified URLs (open directly)\n- [URL 1]\n- [URL 2]\n\n### Confirmed Findings\n- [key finding 1]\n- [key finding 2]\n\n### Actions Taken\n- [action taken, if any]\n```\n\n### Error Response\n```md\n## UI Verification Result ❌\n\n**Page:** [page title/URL]\n**Issue Found**\n\n### Error Details\n- [error 1]\n- [error 2]\n\n### Console Errors\n[relevant console errors only]\n\n### Recommended Action\n- [fix suggestion]\n```\n\n## Snapshot Analysis Rules\n\nWhen analyzing snapshots:\n1. **Summarize structure** - \"Main panel shows 35 messages with tabs for Messages/Agents/Todos\"\n2. **Report key elements** - List important buttons, forms, or content areas\n3. **Identify issues** - Note missing elements, unexpected text like \"No messages\", error states\n4. **Skip irrelevant details** - Don't list every element, focus on what matters for the task\n\n### Example Summary\n❌ Bad (too long):\n```\n- generic [ref=e1]: ...\n- button [ref=e2]: ...\n(hundreds of lines)\n```\n\n✅ Good (concise):\n```\nPage: Claude Sessions (localhost:5173)\nStatus: Loaded successfully\n\nKey Elements:\n- Project list (10 projects)\n- Session viewer (35 messages)\n- Tabs: Messages (selected), Agents, Todos\n\nIssues found: None\n```\n\n## Interaction Patterns\n\n### Click Element\n```\n1. snapshot → Find element ref\n2. browser_click using ref\n3. Wait for state change\n4. snapshot again → Verify result\n5. Report summary\n```\n\n### Fill Form\n```\n1. snapshot → Identify form fields\n2. browser_type each field\n3. Submit if requested\n4. Report result\n```\n\n### Navigate\n```\n1. browser_navigate to URL\n2. Wait for load (browser_wait_for)\n3. snapshot\n4. Report page state\n```\n\n### Custom dropdown widgets (react-select and similar) — ref-based click/type unreliable\n\nMulti-instance custom-select components (react-select is the most common; the same applies to any JS-rendered listbox that isn't a native `<select>`) frequently break ref-based automation in a specific way: **the accessibility tree exposes only one \"active\" combobox node at a time, and its ref gets reused/reassigned across the page's multiple visually-distinct dropdown instances** as focus moves. Clicking a snapshot ref that visually pointed at dropdown #3 can silently reopen dropdown #2's menu instead, and `type`/`fill` on the ref's underlying input can appear to succeed (`OK` returned) while the value never actually changes (the framework's controlled-input state resets the DOM value on next render because no real keyboard event reached its event handler).\n\n**Symptoms that indicate this is happening**:\n- A `click` on a ref opens the wrong dropdown's option list (visually confirm via screenshot, not just the tool's return code)\n- `fill`/`type` returns `OK`, but `get value` on the same selector reads back empty or unchanged\n- The same ref number, reused across snapshots, corresponds to different screen positions each time\n\n| # | Don't (forbidden) | Do (correct alternative) |\n|---|-------------------|--------------------------|\n| 1 | Trust a `click`/`fill` tool call's `OK` return as proof the intended element was affected | Take a screenshot (or `get value`) immediately after and visually/programmatically confirm the actual UI state changed as expected |\n| 2 | Keep retrying the same ref-based `click`/`type` when the visible result doesn't match | Switch to direct DOM query + synthetic event dispatch via `eval` (see pattern below) |\n| 3 | Assume a filterable dropdown always needs a typed search string | Some dropdowns show a single relevant option immediately on open with no typing needed (e.g. a picker scoped to one existing resource) — screenshot after opening before assuming you must type |\n\n**Fallback pattern — locate by bounding box, then dispatch real mouse events**:\n\n```javascript\n// 1. Enumerate all instances of the control class + their screen position\n// (use whatever class the framework renders — e.g. '.react-select__control')\nconst els = Array.from(document.querySelectorAll('.react-select__control'));\nels.map(el => { const r = el.getBoundingClientRect(); return {x: r.x, y: r.y, text: el.textContent}; });\n\n// 2. Pick the instance by its rendered position/text (NOT by DOM order alone —\n//    verify against a screenshot's visible layout first), then open it with a\n//    full mousedown/mouseup/click sequence (a plain .click() does not open\n//    react-select's menu, since it listens for mousedown)\nconst target = els.find(el => el.textContent === 'Select...');\nconst r = target.getBoundingClientRect();\n['mousedown', 'mouseup', 'click'].forEach(type =>\n  target.dispatchEvent(new MouseEvent(type, {bubbles: true, cancelable: true, clientX: r.x + 5, clientY: r.y + 5}))\n);\n\n// 3. Select an option the same way — options often render as plain <div>\n// with no distinguishing class; match by trimmed text content instead\nconst opt = Array.from(document.querySelectorAll('.react-select__menu-list *'))\n  .find(el => el.children.length === 0 && el.textContent.trim() === 'desired option text');\n['mousedown', 'mouseup', 'click'].forEach(type => opt.dispatchEvent(new MouseEvent(type, {bubbles: true, cancelable: true})));\n```\n\nTyping into the underlying input (for filterable dropdowns) has the same \"looks like it worked but didn't\" trap when done via the ref-based `fill`/`type`. If typing is genuinely required, focus the actual `<input>` first (`el.querySelector('input')`), then use the backend's `type` command against a fresh CSS id selector obtained from that specific input (`input.id`) — re-derive the id after every open, since these frameworks commonly remount the input (new generated id) on each render.\n\n## Large Page Handling\n\nIf the snapshot result is too large:\n\n### 1. Query specific elements via call_tool_chain\n```typescript\nmcp__code-mode__call_tool_chain({\n  code: `\n    const result = await playwright.playwright_browser_evaluate({\n      code: \"document.querySelectorAll('button').length\"\n    });\n    return result;\n  `\n})\n```\n\n### 2. Screenshot a specific area\n```typescript\nmcp__code-mode__call_tool_chain({\n  code: `\n    const shot = await playwright.playwright_browser_take_screenshot({ element: 'specific area' });\n    return shot;\n  `\n})\n```\n\n## Virtualized Table Bulk Row Operations\n\nVirtualized tables (React-window/virtual-scroll UIs like Notion databases, large grids) only render rows near the current viewport into the DOM. This breaks the naive snapshot→ref→click loop in specific ways:\n\n### Symptom pattern\n- `browser_snapshot` frequently exceeds the token limit (50-70KB) and gets auto-saved to a file\n- Element refs go stale after 1-2 interactions because scrolling/re-render unmounts and remounts rows with new refs\n- `document.querySelectorAll` via `browser_evaluate` unreliably finds custom-rendered controls (checkboxes that aren't real `<input type=\"checkbox\">`, or don't consistently match `[role=\"checkbox\"]`)\n- Accessible-name-based locators collide when many elements share the same (often empty) label — `getByLabel('', {exact:true}).nth(N)` resolves ambiguously as the DOM shifts between calls\n\n### Procedure\n\n1. **Never assume one snapshot covers the whole dataset.** Scroll to top/middle/bottom (`el.scrollTop = ...` on the scrollable container) and snapshot at each position to cover the full row set — don't infer total row count from a single snapshot's visible rows.\n2. **When a snapshot exceeds the token limit and is saved to a file**, immediately grep/parse the saved file (`uv run python3 -c \"import re; ...\"` or `grep -n`) to extract element refs paired with their row content, rather than retrying the snapshot call hoping for a smaller result.\n3. **Extract a ref and click it immediately** — don't batch multiple snapshot→ref-extraction cycles before acting. A ref from an older snapshot is often stale by the time you get to it if the page re-rendered in between (scroll, a prior click, a dialog open/close).\n4. **After each click, verify the actual target was hit** — a successful tool call is not proof the intended row was selected. Query the live DOM state (e.g. elements with `aria-checked=\"true\"` plus their closest row's text) and confirm it matches the intended row's content signature.\n5. **For bulk multi-select + destructive action** (e.g. select N rows → delete): click each target individually via a fresh ref each time, verify the running selection-count indicator after each batch (the UI's own \"N selected\" label, in whatever language it renders), then do a final content-level verification of every selected row **before** executing the action.\n6. **When distinguishing \"old\" vs \"new\" duplicate rows by content** (e.g. after a data merge produced duplicates), verify the first pair by comparing actual field content against a known-old-value signature, then confirm any assumed ordering pattern (e.g. \"old always sorts first\") holds before relying on it for the remaining pairs.\n\n### Don't / Do\n\n| # | Don't | Do |\n|---|-------|-----|\n| 1 | Assume one snapshot at the current scroll position shows the entire table | Scroll to top/middle/bottom and snapshot at each position to cover all rows |\n| 2 | Reuse a ref from a snapshot taken several actions ago | Extract ref → click immediately. If a click fails with \"ref not found\", re-snapshot and re-extract rather than retrying the same ref |\n| 3 | Trust \"click tool returned success\" as proof the intended row was selected | Query `aria-checked=\"true\"` (or equivalent) plus closest row text after every click/batch, compare against expected content |\n| 4 | Execute a bulk delete/action right after reaching the target selection count | Verify selection count AND spot-check row content for every selected item before the destructive action |\n| 5 | Use `document.querySelectorAll(...)` text-match on custom UI controls and assume zero results means \"not found\" | Custom-rendered controls may not match simple DOM queries reliably — cross-check with the accessibility snapshot (which traverses non-standard DOM/shadow structures) before concluding an element doesn't exist |\n\n## Error Handling\n\nIf element not found:\n- Check if page is still loading\n- Try browser_wait_for\n- Report specific missing element\n\nIf action fails:\n- Check console for errors\n- Take screenshot for debugging\n- Report failure with context\n\n## Language Guidelines\n\n- Respond primarily in English\n- Keep technical terms (URLs, element names) in English\n- Use emojis for status: ✅ Success, ❌ Error, ⚠️ Warning, 🔄 In progress\n\nFile v0.2.10:LICENSE\n\nMIT License\n\nCopyright (c) 2026 es6.kr\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\nArchive v0.2.9: 9 files, 44622 bytes\n\nFiles: cdp-trace.md (7016b), CHANGELOG.md (7746b), credential-issue.md (52731b), LICENSE (1063b), scripts/cdp-trace.js (6557b), skill-card.md (3394b), SKILL.md (16226b), ui-test.md (14346b), _meta.json (130b)\n\nFile v0.2.9:SKILL.md\n\n---\nname: web-browser\nmetadata:\n  author: es6kr\n  version: \"0.1.0\"\ndescription: |\n  Environment-aware browser operations. Detects wmux/cmux/tmux and routes to the right backend\n  (wmux/cmux panel → user-visible, plain → Playwright MCP, chrome-devtools → reuse the user's\n  real logged-in session). Topics:\n  ui-test - snapshots, click/fill/verify, closed shadow DOM cascade diagnosis (cdp-trace)\n  [ui-test.md, cdp-trace.md].\n  credential-issue - open service login via detected backend → wait for user sign-in → issue\n  OR refresh an access key / token / secret / OAuth scope → hand off to follow-up automation\n  (aws-cli, gh secret set, gh auth refresh, etc.) [credential-issue.md]. Covers both new\n  issuance and existing-token scope expansion (PAT scope add, OAuth re-authorize, device-code).\n  Use for: \"UI check\", \"browser test\", \"screen verify\", \"Playwright test\", \"shadow DOM cascade\",\n  \"::part not working\", \"CDP trace\", \"issue token\", \"service credential\", \"open login screen\",\n  \"PAT refresh\", \"scope expansion\", \"device-code auth\", \"browser device-code\".\n---\n\n# Web Browser\n\nEnvironment-aware browser operations skill. Detects the runtime environment and routes to the\nappropriate browser backend, then runs one of two workflows: UI testing/verification (`ui-test`) or\nbrowser-login-assisted credential issuance (`credential-issue`).\n\n## Topics\n\n| Topic | Description | Guide |\n|-------|-------------|-------|\n| ui-test | Snapshot analysis, click/fill/verify, page-state diagnosis | [ui-test.md](./ui-test.md) |\n| cdp-trace | CDP-based closed shadow DOM cascade diagnosis (DOM.getDocument pierce:true + CSS.getMatchedStylesForNode) | [cdp-trace.md](./cdp-trace.md) |\n| credential-issue | service+command param → open login screen → wait for user login → issue access key/token/secret → hand off to automation | [credential-issue.md](./credential-issue.md) |\n\n## Topic Dependencies\n\n```\nweb-browser (Step 0: environment detection — shared by all topics)\n  ├─→ ui-test (UI verification)\n  │     └─→ cdp-trace (extends ui-test for closed shadow DOM)\n  └─→ credential-issue (browser-login-assisted token/key issuance)\n        └─→ chrome-devtools backend preferred (reuses the user's real logged-in session)\n```\n\n- **Step 0 (below) is shared** — every topic detects the backend first, then runs its workflow.\n- `ui-test`, `cdp-trace` are the UI-testing family.\n- `credential-issue` reuses the same backend routing + the user-visibility rule, generalized into a\n  service+command parameterized auth flow.\n- **Authentik SSO verification** (`sso-verify`) is **not** included in this skill — it remains in a\n  separate local-only `sso-verify` skill (user-environment specific, untracked).\n\n## CRITICAL — user visibility is the top priority (HARD STOP)\n\n**The primary purpose of browser diagnosis/verification is \"the user sees it on their own screen\"**. Screenshot capture is **supporting evidence**, not a substitute for visibility.\n\n| # | Don't | Do |\n|---|-------|-----|\n| 1 | Launch with `chromium.launch({ headless: true })` and only attach a screenshot in chat | `chromium.launch({ headless: false, slowMo: 500 })` — let the user follow in real time |\n| 2 | \"I showed the user a screenshot, so it's fine\" | screenshot ≠ visible to the user. If the user says \"show me\", open a visible browser + slowMo |\n| 3 | wmux/cmux/Playwright MCP disconnected → fall back to headless CLI | Even on CLI fallback, force `headless: false`. On a Windows desktop OS, a chromium GUI is available |\n| 4 | \"headless is faster and more stable by default\" mindset | Speed costs user visibility. If the user says \"show me\", visibility wins |\n| 5 | Playwright MCP disconnected → CLI fallback auto-selects headless | CLI fallback is also `headless: false`. headless is only for explicit non-interactive cases (e.g., CI assertion) |\n| 6 | SaaS/API task lacks credentials → fallback to manual user UI operation | Do NOT recommend manual user UI clicking when API access is available; fallback to `credential-issue` topic to issue token/key first |\n\n## API-capable environment without credentials — fallback to credential-issue (HARD STOP)\n\n**When a task can be performed via API (e.g., Google Forms API, GitHub API, AWS API), but required API tokens or access keys are missing in the environment, do NOT recommend manual user UI clicking or surrender to direct manual UI operation.** You MUST recommend `credential-issue` topic to issue the access key/token via browser login first, then proceed with backend API automation.\n\n| # | Don't | Do |\n|---|-------|----|\n| 1 | API token missing → \"Please edit/click manually on the website\" | Recommend `credential-issue` topic to issue API token/key via browser login |\n| 2 | Direct UI automation fails → fallback to manual user operation | Check if API automation is available → issue credential via `credential-issue` → execute API |\n\n### Self-check (every time before launching Playwright/chromium)\n\n1. Did the user use a visibility request keyword such as \"show me\", \"open it\", \"web-browser\", or \"browser test\"? → If yes, force `headless: false`\n2. Is this work interactive verification or diagnosis for the user? → If yes, `headless: false`\n3. headless is justified only when (a) CI assertion (b) the user explicitly said \"in headless\" (c) Playwright MCP is used (the UI shows itself)\n4. screenshot is supporting evidence — it can be attached to a chat report, but it does not replace user visibility\n\n### Violation case (2026-05-28, 1st)\n\nDuring a closed shadow DOM `ak-library` cascade investigation, used a `npx playwright` Bash invocation + `chromium.launch({ headless: true })` and only attached a screenshot in chat. The user requested \"show it via web-ui-test\" and no visible browser was provided. The user reacted angrily that the Chromium UI never appeared.\n\n## Login wall mid-capture — ask before stopping, don't silently defer (HARD STOP)\n\n**When a capture/documentation task (report evidence, purchase/registration flow guide, etc.) hits a screen that requires login, and completing that login would reveal materially different information than what's already captured (e.g., the real final price vs. a promotional pre-login price, actual post-login UI state vs. an assumption), do NOT silently stop and paper over the gap with a deferral disclaimer.** Ask the user via `AskUserQuestion` whether to continue (via interactive login in a visible backend) or whether the pre-login capture is sufficient for the purpose at hand.\n\n| # | Don't | Do |\n|---|-------|----|\n| 1 | Hit a login wall → write \"please have finance/ops enter payment details themselves for security\" and stop, without asking | Decompose the remaining flow: **payment/credential entry** should be deferred to the user/business owner, but **login + viewing the resulting screen** is often just informational — ask which is actually needed before deciding to stop |\n| 2 | Treat \"login\" and \"entering payment info\" as one bundled decision to skip together | They are different risk levels. Login-then-observe (e.g., see the real cart/checkout price) does not require entering card/account credentials — only the latter needs deferral |\n| 3 | Report a pre-login/promotional price or state as if it were final, without flagging the gap | If the login-gated final screen wasn't verified, explicitly flag it (\"actual payment screen not verified — may differ from the listed price\") instead of presenting the pre-login figure as authoritative |\n| 4 | Assume the backend can't support interactive login without checking | Check chrome-devtools connection + visibility (per `credential-issue.md` \"Fresh-login flow\") first; if visible, open the page there and have the user sign in in that same window, then continue capturing |\n| 5 | Decide unilaterally that \"this is good enough\" when the report's factual accuracy depends on the gated screen | If the gap could make a delivered report/guide factually wrong (e.g., a payment-request report citing a price that turns out incorrect), the stop-vs-continue decision belongs to the user, not the assistant |\n\n### Violation case (2026-07-22, 1st)\n\nWhile building a domain-registration payment-request report, captured the domain-search-result page (showing a promotional price) and the login screen, then stopped at the login wall with a disclaimer (\"have finance/ops enter payment details\"), never asking whether to continue via login to verify the real checkout price. The report's stated price differed from the actual payment-screen price. User feedback (paraphrased): \"don't arbitrarily skip capturing screens that require login — ask first.\"\n\n## Known Automation Limitations — SaaS Portal Action-Level CAPTCHA Gates\n\nSome SaaS portals allow full browser login automation but selectively trigger CAPTCHA challenges\non **creation/mutation actions** (not just on login). Document confirmed cases here so agents do\nnot repeat failed automation attempts.\n\n| Service | Automatable | CAPTCHA-blocked | Fallback |\n|---------|-------------|-----------------|----------|\n| **Discord Developer Portal** | Login (via persistent profile with saved credentials) | **New application creation**, bot token reset | Keep browser visible (`headless: false`); user handles hCaptcha manually; script polls `page.url()` for `/bot` URL and auto-captures token once user navigates there |\n| **Discord Developer Portal** | Reading existing app info, navigating between tabs | _(same)_ | _(same)_ |\n\n### Discord Developer Portal — specific notes (2026-08-18, 1st confirmed)\n\n- **Login**: Playwright persistent context (`launchPersistentContext`) with a saved user data directory\n  retains Discord session cookies. Navigation to `discord.com/developers/applications` succeeds\n  without re-authentication.\n- **Bot creation blocked**: Clicking \"New Application\" and submitting the modal triggers an hCaptcha\n  dialog (e.g. \"Hold on! You are human, right?\"). The `force: true` checkbox click and JS `dispatchEvent`\n  workarounds successfully activate the Create button, but Discord's backend detects the automated\n  browser and intercepts submission with CAPTCHA.\n- **Recommended hybrid flow**:\n  1. Launch Playwright with `headless: false` + `launchPersistentContext` (reuses login session).\n  2. Navigate to the applications page.\n  3. Set up a polling loop watching `page.url()` for the `/bot` path (every 2s, max ~4min timeout).\n  4. Inform user to manually create the application (handle hCaptcha) and navigate to the Bot tab.\n  5. When `/bot` URL is detected, script resumes: click \"Reset Token\" → capture `input[readonly]` value → enable `[role=\"switch\"]` intents → save changes.\n  6. Write token to a temp file → hand off to next automation (K8s Secret injection, etc.).\n\n---\n\n## Step 0: Environment Detection (MANDATORY — before any browser action)\n\nCheck environment variables AND CLI presence to determine the browser backend:\n\n```bash\n# wmux\necho \"WMUX=$WMUX\"\n# cmux — detect via ANY of these (cmux app does NOT set CMUX_SESSION; use multi-var OR)\necho \"CMUX_BUNDLE_ID=$CMUX_BUNDLE_ID\"\necho \"CMUX_PANEL_ID=$CMUX_PANEL_ID\"\necho \"CMUX_BUNDLED_CLI_PATH=$CMUX_BUNDLED_CLI_PATH\"\n# CLI fallback (env may be unset in nested shells but CLI still works)\ncommand -v cmux && echo \"cmux CLI present\"\ncommand -v wmux && echo \"wmux CLI present\"\n```\n\n### Do & Don't — Browser Backend Selection\n\n| Environment | Detect (ANY true → environment matches) | Do (use this) | Don't (forbidden) |\n|-------------|----------------------------------------|---------------|-------------------|\n| **wmux** | `$WMUX` set OR `command -v wmux` succeeds | `wmux browser open/snapshot/click/type` commands via Bash | Playwright MCP — user cannot see the invisible Playwright window |\n| **cmux** | `$CMUX_BUNDLE_ID` set OR `$CMUX_PANEL_ID` set OR `$CMUX_BUNDLED_CLI_PATH` set OR `command -v cmux` succeeds (e.g. `/Applications/cmux.app/Contents/Resources/bin/cmux`) | cmux browser panel commands | Playwright MCP — same reason |\n| **Plain / tmux** | None of wmux/cmux signals present | Playwright MCP (Step 1 below) | — |\n\n#### cmux detection — multi-var OR rationale\n\ncmux app sets several env vars when launching a shell, **but `CMUX_SESSION` is NOT one of them** (a legacy guess by analogy with `WMUX`). Real vars observed in a cmux-launched shell:\n\n- `CMUX_BUNDLE_ID` (e.g. `com.cmuxterm.app`)\n- `CMUX_PANEL_ID` (UUID per panel)\n- `CMUX_BUNDLED_CLI_PATH` (CLI absolute path)\n- `CMUX_SHELL_INTEGRATION_DIR`\n- `CMUX_AGENT_LAUNCH_*`\n- `GHOSTTY_RESOURCES_DIR` (cmux uses Ghostty-based terminal)\n\n`CMUX_SOCKET` is **set but often empty** — do not use it as the sole signal. Use the OR matrix above.\n\n| # | Don't (single-var assumption) | Do (multi-var OR) |\n|---|-------------------------------|-------------------|\n| 1 | `[ -n \"$CMUX_SESSION\" ]` only check → false negative on cmux app | OR across `CMUX_BUNDLE_ID` / `CMUX_PANEL_ID` / `CMUX_BUNDLED_CLI_PATH` |\n| 2 | Use `CMUX_SOCKET` as detection (empty in many cases) | Treat empty `CMUX_SOCKET` as no-signal; rely on the 3 vars above + CLI presence |\n| 3 | Assume cmux env var name mirrors wmux (`*_SESSION`) | Verify against actual cmux app shell environment — vars differ per terminal multiplexer |\n\n### wmux Browser Commands Reference\n\nWhen `$WMUX` is set, use these instead of Playwright MCP.\n\n**Invocation form**: the rest of this document uses the bare `wmux browser …` form, which is what runs when `wmux` is on `PATH` (the common case). If `wmux` is **not** on `PATH` in the current environment, substitute `node \"$WMUX_CLI\"` for `wmux` in every command below — `$WMUX_CLI` points to the same entry point. The two forms are interchangeable; pick whichever resolves on the current shell and use it consistently.\n\n```bash\nwmux browser open <url>          # navigate (= playwright navigate)\nwmux browser snapshot            # get accessibility tree with @eN refs\nwmux browser click @eN           # click element\nwmux browser type @eN <text>     # type into element\nwmux browser fill @eN <value>    # set input value\nwmux browser get-text            # get page text\nwmux browser screenshot          # capture screenshot\nwmux browser eval <js>           # run JavaScript\nwmux browser back                # go back\nwmux browser forward             # go forward\nwmux browser reload              # reload page\n```\n\n**Workflow**: `browser open <url>` → `browser snapshot` → read tree → `browser click/type @eN` → `browser snapshot` again.\n\n**Refs (`@e1`, `@e2`...) expire after page changes** — always re-snapshot.\n\n### Do & Don't — wmux vs Playwright Mapping\n\n| Action | wmux (Do) | Playwright MCP (Don't in wmux) |\n|--------|-----------|-------------------------------|\n| Navigate | `Bash(\"wmux browser open <url>\")` | `mcp__playwright__browser_navigate` |\n| Snapshot | `Bash(\"wmux browser snapshot\")` | `mcp__playwright__browser_snapshot` |\n| Click | `Bash(\"wmux browser click @eN\")` | `mcp__playwright__browser_click` |\n| Type | `Bash(\"wmux browser type @eN text\")` | `mcp__playwright__browser_type` |\n| Screenshot | `Bash(\"wmux browser screenshot\")` | `mcp__playwright__browser_take_screenshot` |\n| Evaluate JS | `Bash(\"wmux browser eval <js>\")` | `mcp__playwright__browser_evaluate` |\n| Wait for text | Re-snapshot + check | `mcp__playwright__browser_wait_for` |\n\n**Key difference**: wmux browser is visible to the user in real-time on the right panel. Playwright opens an invisible window the user cannot see.\n\n---\n\n\n## Quick Reference\n\nAfter Step 0 backend detection, route to the topic:\n\n| Goal | Topic | Entry |\n|------|-------|-------|\n| Verify a UI change, snapshot, click/fill | `ui-test` | [ui-test.md](./ui-test.md) |\n| Diagnose `::part` not applying / closed shadow DOM cascade | `cdp-trace` | [cdp-trace.md](./cdp-trace.md) |\n| Open a service login → wait for user login → issue access key/token | `credential-issue` | [credential-issue.md](./credential-issue.md) |\n\n**Step execution order**: Step 0 (this file — detect backend + user-visibility rule) → read the\ntarget topic `.md` → follow its procedure. The topic `.md` files hold the actual procedures; this\nfile is the shared backend-detection + index.\n\nFile v0.2.9:_meta.json\n\n{\n  \"ownerId\": \"kn74k8yfvftx6f062qa8fzyd8h8373jd\",\n  \"slug\": \"web-browser\",\n  \"version\": \"0.2.9\",\n  \"publishedAt\": 1789746869383\n}\n\nFile v0.2.9:cdp-trace.md\n\n# CDP Trace — Closed Shadow DOM Cascade Diagnosis\n\nExtract the computed style + matched CSS rules of elements inside a closed shadow DOM directly via the Chrome DevTools Protocol (CDP). Identifies which stylesheet is the actual carrier and verifies cascade entry when `::part(...)` outer-scope selectors fail to apply.\n\n## When to use\n\n- Outer `::part(<name>) { ... }` rules visually have no effect\n- You need the computed style of an element inside a closed shadow DOM\n- You need to identify the carrier (outer document vs shadow-root inject)\n- You need to verify the cascade for `[part=\"...\"]` direct selectors on a web component\n\n## Mechanism\n\n`DOM.getDocument({ depth: -1, pierce: true })` of the Chrome DevTools Protocol (Playwright `newCDPSession`) returns the full DOM tree **including closed shadow roots**. For each `nodeId`, call `CSS.getComputedStyleForNode` + `CSS.getMatchedStylesForNode` to dump the applied rules and the rules that were ignored.\n\n## Quick Reference\n\n```bash\n# 1. Install playwright into .tmp/ and pull headed chromium\ncd <repo>/.tmp && npm init -y && npm install playwright@latest\nnpx playwright install chromium\n\n# 2. Run cdp-trace.js (user-visible browser)\n# --parts (canonical, plural) and --part (legacy singular) are both accepted;\n# the script tolerates either form to match the historical Quick Reference example.\nnode scripts/cdp-trace.js --url http://<target>/<path> --parts \"app-group,card-wrapper\"\n```\n\n## Script pattern\n\n```javascript\nconst { chromium } = require('playwright');\n\n(async () => {\n  // headless: false — user-visible (per web-browser SKILL.md Step 0 user-visibility rule)\n  const browser = await chromium.launch({ headless: false, slowMo: 800 });\n  const page = await browser.newContext({ ignoreHTTPSErrors: true, viewport: null }).then(c => c.newPage());\n\n  await page.goto(URL, { waitUntil: 'domcontentloaded' });\n  // (handle login / auth as needed)\n\n  const cdp = await page.context().newCDPSession(page);\n  await cdp.send('DOM.enable');\n  await cdp.send('CSS.enable');\n\n  // pierce:true — expose closed shadow roots as well\n  const { root } = await cdp.send('DOM.getDocument', { depth: -1, pierce: true });\n\n  // Recursive walk — collect every element carrying a part attribute\n  function walk(node, found = []) {\n    if (!node) return found;\n    const attrs = node.attributes || [];\n    const partIdx = attrs.findIndex((v, i) => i % 2 === 0 && v === 'part');\n    if (partIdx >= 0) {\n      found.push({ id: node.nodeId, name: node.nodeName, part: attrs[partIdx + 1] });\n    }\n    if (node.children) node.children.forEach(c => walk(c, found));\n    if (node.shadowRoots) node.shadowRoots.forEach(s => walk(s, found));\n    return found;\n  }\n  const parts = walk(root);\n\n  for (const p of parts.filter(p => TARGET_PARTS.includes(p.part))) {\n    const cs = await cdp.send('CSS.getComputedStyleForNode', { nodeId: p.id });\n    const matched = await cdp.send('CSS.getMatchedStylesForNode', { nodeId: p.id });\n\n    console.log(`[part=\"${p.part}\"] (nodeId=${p.id}):`);\n    // computed values\n    for (const prop of ['width', 'grid-template-columns', '--app-card-min-width']) {\n      const v = cs.computedStyle.find(c => c.name === prop);\n      if (v) console.log(`  ${prop}: ${v.value}`);\n    }\n    // matched CSS rules (which sheet matched, which property won out)\n    console.log(`  matched CSS rules:`);\n    for (const r of matched.matchedCSSRules || []) {\n      console.log(`    [${r.rule.origin}] ${r.rule.selectorList.text}`);\n      for (const prop of r.rule.style.cssProperties) {\n        if (prop.disabled) continue;\n        console.log(`      ${prop.name}: ${prop.value}${prop.important ? ' !important' : ''}`);\n      }\n    }\n  }\n\n  // Let the user inspect the page and close it (X button) themselves\n  await new Promise(() => {});\n})();\n```\n\n## Interpreting the output\n\n### Matched CSS rules include the outer brand sheet = cascade entry OK\n\n```\n[part=\"app-group\"]:\n  matched CSS rules:\n    [regular] [part=\"app-group\"] (sheet=style-sheet-20984-37)  ← brand!\n      grid-template-columns: 1fr 1fr 1fr !important\n```\n\n→ The `[part=\"app-group\"] { ... }` direct selector reaches the shadow-root cascade.\n\n### Matched CSS rules show only inner shadow stylesheet = outer rule did not enter\n\n```\n[part=\"app-group\"]:\n  matched CSS rules:\n    [regular] [part=\"app-group\"] (sheet=style-sheet-20984-66)  ← inner only\n      grid-template-columns: var(--app-group-template-columns, 1fr)\n```\n\n→ Zero matches for the external `ak-library::part(app-group) { ... }` rule. **Outer `::part` does not enter the cascade** (a Chrome closed-shadow limitation). Switch to a `[part=\"...\"]` direct selector.\n\n## Don't / Do\n\n| # | Don't | Do |\n|---|-------|-----|\n| 1 | Run CDP trace with `headless: true` | `headless: false, slowMo: 800` — user-visibility rule (web-browser SKILL.md Step 0) |\n| 2 | `::part(...)` doesn't work → only report \"no visible effect\" | Use CDP `getMatchedStylesForNode` to confirm whether the cascade is entered and identify the carrier |\n| 3 | Retry the same selector form 5+ times | 0 matched CSS rules = evidence of spec/runtime divergence. Switch carrier immediately (`:host` / `[part=\"...\"]` direct) |\n| 4 | Leave `DOM.getDocument` with `pierce: false` (default) | Set `pierce: true` explicitly — exposes closed shadow roots too |\n| 5 | Inspect only by `nodeId` + computed style | Also dump `matchedCSSRules` — check which sheet, which property, and `!important` |\n\n## Self-check (every time before running a CDP trace)\n\n1. Is the browser launched with `headless: false`? (user-visibility rule)\n2. Did you pass `pierce: true` to `DOM.getDocument`?\n3. Did you call `CSS.getMatchedStylesForNode` after identifying the target element's `nodeId`?\n4. Does the output print every matched rule's sheet id + selector text + property + `!important` flag?\n\n## Exceptions\n\n- The user says \"I just want a quick visual check\" → skip CDP, screenshot only\n- Inspecting a non-closed-shadow element → CDP is overkill; `getComputedStyle` is enough\n\n## Case study (2026-05-28)\n\nCard layout work on an IdP `ak-library` page. The attempt `ak-library::part(app-group) { grid-template-columns: ... !important }` produced zero visual effect. A CDP trace confirmed zero matched rules → outer `::part` could not enter the cascade → switched to `[part=\"app-group\"] { ... }` direct selector → applied immediately. After 5 verification rounds the right carrier was found; this topic was added so future investigations cost fewer rounds.\n\n## References\n\n- [Chrome DevTools Protocol — DOM domain](https://chromedevtools.github.io/devtools-protocol/tot/DOM/)\n- [CDP — CSS.getMatchedStylesForNode](https://chromedevtools.github.io/devtools-protocol/tot/CSS/#method-getMatchedStylesForNode)\n- [Playwright — context.newCDPSession()](https://playwright.dev/docs/api/class-browsercontext#browser-context-new-cdp-session)\n- `~/.agents/rules/authentik-customization.md` — IdP brand CSS carrier selection (one application domain for this topic)\n\nFile v0.2.9:CHANGELOG.md\n\n# Changelog\n\n## [0.2.9](https://github.com/es6kr/skills/compare/web-browser-v0.2.8...web-browser-v0.2.9) (2026-09-18)\n\n\n### Bug Fixes\n\n* **cleanup:** make the session-end report table self-sufficient ([#487](https://github.com/es6kr/skills/issues/487)) ([c4a0255](https://github.com/es6kr/skills/commit/c4a02557fb8de3b32cf337c549f62535dabf824b))\n\n## [0.2.8](https://github.com/es6kr/skills/compare/web-browser-v0.2.7...web-browser-v0.2.8) (2026-08-26)\n\n\n### Bug Fixes\n\n* accumulate 16 patch-level bug fixes and guard enhancements across skills ([d214e5d](https://github.com/es6kr/skills/commit/d214e5dcc7fac1bc07baf3b6cec62999aea732f0))\n* **core:** align workflow steps, next suggestion patterns, and browser topics ([c68d489](https://github.com/es6kr/skills/commit/c68d489d01a79862b8933b4a0542168cf676cd3a))\n* promote next-fix batch (consolidate fabrication guard, session rewind, config-driven PR base) ([7ca0ccb](https://github.com/es6kr/skills/commit/7ca0ccbf13cefafedc33a16a7361756c95f8b8f6))\n\n## [0.2.7](https://github.com/es6kr/skills/compare/web-browser-v0.2.6...web-browser-v0.2.7) (2026-08-17)\n\n\n### Bug Fixes\n\n* promote next-fix staging (30 fixes across 14 skills) ([ee467c0](https://github.com/es6kr/skills/commit/ee467c045d779d7b80d30f160763ec3534a9742b))\n* **web-browser:** credential-issue backend routing — session-existence gate + account-mismatch rule ([e5a9098](https://github.com/es6kr/skills/commit/e5a90986812c90b101a24554f3de9038a59906b4))\n* **web-browser:** document virtualized table bulk row operation pattern ([c5bc8f0](https://github.com/es6kr/skills/commit/c5bc8f0610263a963e8a75bfd30f36f2a5dafa76))\n* **wip:** cross-ref PR-URL and TaskCreate subject repo-qualifier rules ([#186](https://github.com/es6kr/skills/issues/186)) ([4982364](https://github.com/es6kr/skills/commit/49823641a7b08123ebd0325273892bee41bc3280))\n\n## [0.2.6](https://github.com/es6kr/skills/compare/web-browser-v0.2.5...web-browser-v0.2.6) (2026-08-09)\n\n\n### Bug Fixes\n\n* **consolidate:** address CodeRabbit/Copilot review findings on PR [#270](https://github.com/es6kr/skills/issues/270) ([3b11a73](https://github.com/es6kr/skills/commit/3b11a730b5ad68803d35a8264eda540e48265d75))\n* promote accumulated next-fix fixes to main ([95656e9](https://github.com/es6kr/skills/commit/95656e9b551ee0bb77904a0a571d49c53bc01cc9))\n* **web-browser:** add revoke command type to credential-issue topic ([306cf63](https://github.com/es6kr/skills/commit/306cf63752638d2cae7b924978cb036467382b00))\n* **web-browser:** add revoke command type to credential-issue topic ([cbd7121](https://github.com/es6kr/skills/commit/cbd712145d6faf293409df956706b1afae8ef969))\n* **web-browser:** clarify revoke command shape and step mapping ([ed6ce6e](https://github.com/es6kr/skills/commit/ed6ce6e2ec36fea2b4125d67baceacdba3761dcf))\n* **web-browser:** correct stale priority-1 row reference to priority-3 ([#250](https://github.com/es6kr/skills/issues/250)) ([dd823e7](https://github.com/es6kr/skills/commit/dd823e7596fc4a4d1fb51f6fe81ddac4eda06602))\n\n## [0.2.5](https://github.com/es6kr/skills/compare/web-browser-v0.2.4...web-browser-v0.2.5) (2026-08-05)\n\n\n### Bug Fixes\n\n* promote next-fix staging (38 fixes across 16 skills) ([94f8c33](https://github.com/es6kr/skills/commit/94f8c33800ce411ae63e22c5259cdae8435508a4))\n* **web-browser:** CDP-hostile escalation overrides recorded browser preference ([13a13a0](https://github.com/es6kr/skills/commit/13a13a0d6de9a0a9057dbdad1acb9b600710153e))\n* **web-browser:** check locally-recorded preferred browser before naming one in OS-level open ([2b9eef2](https://github.com/es6kr/skills/commit/2b9eef283560c0f0b5b8ec40aab79af9f2629fff))\n* **web-browser:** preferred-browser check + credential-issue fallback for API tasks ([a5a7859](https://github.com/es6kr/skills/commit/a5a7859d6c0c9ebfc2a7a6417b30e6a6299fd3da))\n* **web-browser:** recommend credential-issue instead of manual UI clicking when API access is available ([b127109](https://github.com/es6kr/skills/commit/b127109bcd1fd11c809d1f861c88fcc2cd6c072a))\n* **web-browser:** verify app's actual CLI support before borrowing another tool's flag syntax ([7ce4a28](https://github.com/es6kr/skills/commit/7ce4a28937e1c07e95eecfc511802c445385ee6b))\n* **wip:** cross-ref PR-URL and TaskCreate subject repo-qualifier rules ([#186](https://github.com/es6kr/skills/issues/186)) ([951c1e6](https://github.com/es6kr/skills/commit/951c1e6871e78e226757c6a7ae5ae53efeb7bfb0))\n\n## [0.2.4](https://github.com/es6kr/skills/compare/web-browser-v0.2.3...web-browser-v0.2.4) (2026-07-23)\n\n\n### Bug Fixes\n\n* **next-fix:** accumulate bug fixes for docxport, wip, fix-plan, and hook-kit ([6eec083](https://github.com/es6kr/skills/commit/6eec083b7fbc429bdabcfcc89d7778b185dd7497))\n* skills body bundle — consolidate/fix/hook-kit refinements + English-clean guard hooks (Ralph-loop bypass) ([2e26f41](https://github.com/es6kr/skills/commit/2e26f412a5b963984898e13caaca186c3617ca08))\n* **web-browser:** don't abandon automation on a single login-block signal ([fed542e](https://github.com/es6kr/skills/commit/fed542e600d83979d3e31fde597da13e0ce0e18e))\n\n## [0.2.3](https://github.com/es6kr/skills/compare/web-browser-v0.2.2...web-browser-v0.2.3) (2026-07-07)\n\n\n### Bug Fixes\n\n* **fix,web-browser:** publish-scope edit gate + managed-surface backend detection ([2e28fc4](https://github.com/es6kr/skills/commit/2e28fc4d39953ee6a88f64bd8c8d20907ba01e39))\n* **skills:** review-feedback bundle — consolidate trigger wording + fix wiki paths ([784854e](https://github.com/es6kr/skills/commit/784854e3e07696ca8d16274215004488861862d1))\n\n## [0.2.2](https://github.com/es6kr/skills/compare/web-browser-v0.2.1...web-browser-v0.2.2) (2026-06-30)\n\n\n### Bug Fixes\n\n* **skills:** add procedural guards + standardize description scalar ([#66](https://github.com/es6kr/skills/issues/66)) ([fcc921f](https://github.com/es6kr/skills/commit/fcc921fba3928aad7421ecff888d5dcee5ae5655))\n\n## [0.2.1](https://github.com/es6kr/skills/compare/web-browser-v0.2.0...web-browser-v0.2.1) (2026-06-19)\n\n\n### Bug Fixes\n\n* bundle skill patches across 7 scopes ([f18f47c](https://github.com/es6kr/skills/commit/f18f47c2d05f13b8e3f3ad42675a2dabbb31c824))\n* **credential:** add PAT scope matrix + Settings UI procedure + Service × Store persist matrix ([c61525b](https://github.com/es6kr/skills/commit/c61525b1a2d886b677c85d2638d39a1e2311c142))\n* **web-browser:** compress SKILL.md description + replace credential-issue placeholder ([4fb1148](https://github.com/es6kr/skills/commit/4fb114800991d757c07eb63d1a3d3b8fc19bde4a))\n\n## [0.2.0](https://github.com/es6kr/skills/compare/web-browser-v0.1.0...web-browser-v0.2.0) (2026-06-12)\n\n\n### Features\n\n* decompose workflow/git rules + rename web-ui-test→web-browser ([#50](https://github.com/es6kr/skills/issues/50)) ([e10d48f](https://github.com/es6kr/skills/commit/e10d48fea4e507b95888de44812b53484d32128d))\n\n## [0.1.0] (2026-06-09)\n\nInitial release. `web-browser` is the environment-aware browser-operations skill, succeeding the\nlegacy `web-ui-test` skill (which is retained, local-only, for `sso-verify`).\n\n### Features\n\n* **ui-test**: snapshot analysis, click/fill/verify UI, page-state diagnosis (migrated from web-ui-test).\n* **cdp-trace**: CDP-based closed shadow DOM cascade diagnosis (migrated from web-ui-test).\n* **credential-issue**: new topic — take a service + command as parameters, open the service login\n  screen via the detected backend, wait for the user to sign in, then issue the requested access\n  key / token / secret and hand the result to follow-up automation (aws-cli upload, gh secret set,\n  etc.). chrome-devtools backend preferred for real-session reuse.\n* Shared **Step 0** environment detection (wmux/cmux/Playwright) + user-visibility HARD STOP.\n\nFile v0.2.9:credential-issue.md\n\n# Credential Issue (web-browser topic)\n\nTake a **service** + **command** as parameters, open the service's login screen via the detected\nbrowser backend, wait for the user to sign in, then on a completion signal **issue the requested\naccess key / token / secret** and hand the result to follow-up automation (aws-cli upload, `gh secret\nset`, terraform var injection, etc.).\n\nThis generalizes the \"open the page + user interaction + collect the result\" pattern into a reusable\nparameterized flow. It is the credential-issuance counterpart to [ui-test.md](./ui-test.md).\n\n## Parameters\n\n| Param | Meaning | Example |\n|-------|---------|---------|\n| `service` | The provider whose console issues the credential | `google-forms`, `cloudflare-r2`, `github`, `oci`, `aws`, `authentik` |\n| `command` | What to issue / do once logged in | `issue Google API OAuth token`, `issue R2 S3 token`, `issue fine-grained PAT`, `revoke <key-id>` (see \"Revoke flow\" below) |\n| `login-url` | Direct URL to the issuance page (when known) | `https://console.cloud.google.com/apis/credentials`, `https://dash.cloudflare.com/?to=/:account/r2/api-tokens` |\n| `handoff` | Follow-up automation to run with the issued credential | `gcloud auth print-access-token`, `aws s3 cp`, `gh secret set` |\n\n## Backend selection (Step 0 + credential-specific preference)\n\nDetect the backend via **SKILL.md Step 0** first. For credential issuance the preference order\ndiffers from ui-test, because the user must **sign in** and reusing their real logged-in session is\nfastest:\n\n| Priority | Backend | Why | When |\n|----------|---------|-----|------|\n| 1 | **chrome-devtools** (real session) | Reuses the user's already-\n\nArchive v0.2.8: 9 files, 44416 bytes\n\nFiles: cdp-trace.md (7016b), CHANGELOG.md (7412b), credential-issue.md (52731b), LICENSE (1063b), scripts/cdp-trace.js (6557b), skill-card.md (3195b), SKILL.md (16226b), ui-test.md (14346b), _meta.json (130b)\n\nArchive v0.2.7: 9 files, 43172 bytes\n\nFiles: cdp-trace.md (7016b), CHANGELOG.md (6760b), credential-issue.md (52731b), LICENSE (1063b), scripts/cdp-trace.js (6557b), skill-card.md (2948b), SKILL.md (14107b), ui-test.md (14346b), _meta.json (130b)\n\nArchive v0.2.6: 9 files, 40155 bytes\n\nFiles: cdp-trace.md (7016b), CHANGELOG.md (5909b), credential-issue.md (49512b), LICENSE (1063b), scripts/cdp-trace.js (6557b), skill-card.md (3126b), SKILL.md (14107b), ui-test.md (10493b), _meta.json (130b)\n\nArchive v0.2.5: 9 files, 37909 bytes\n\nFiles: cdp-trace.md (7016b), CHANGELOG.md (4736b), credential-issue.md (45178b), LICENSE (1063b), scripts/cdp-trace.js (6557b), skill-card.md (2548b), SKILL.md (14107b), ui-test.md (10493b), _meta.json (130b)\n\nArchive v0.2.4: 9 files, 34740 bytes\n\nFiles: cdp-trace.md (7016b), CHANGELOG.md (3322b), credential-issue.md (38143b), LICENSE (1063b), scripts/cdp-trace.js (6557b), skill-card.md (2903b), SKILL.md (13049b), ui-test.md (10493b), _meta.json (130b)\n\nArchive v0.2.3: 9 files, 30659 bytes\n\nFiles: cdp-trace.md (7016b), CHANGELOG.md (2656b), credential-issue.md (31506b), LICENSE (1063b), scripts/cdp-trace.js (6557b), skill-card.md (3047b), SKILL.md (10243b), ui-test.md (10493b), _meta.json (130b)\n\nArchive v0.2.2: 9 files, 29487 bytes\n\nFiles: cdp-trace.md (7016b), CHANGELOG.md (2178b), credential-issue.md (28823b), LICENSE (1063b), scripts/cdp-trace.js (6557b), skill-card.md (3036b), SKILL.md (10243b), ui-test.md (10493b), _meta.json (130b)\n\nArchive v0.2.1: 9 files, 27545 bytes\n\nFiles: cdp-trace.md (7016b), CHANGELOG.md (1842b), credential-issue.md (24535b), LICENSE (1063b), scripts/cdp-trace.js (6557b), skill-card.md (2368b), SKILL.md (10244b), ui-test.md (10493b), _meta.json (130b)","readmeExcerpt":"Skill: web-browser Owner: drumrobot Summary: Environment-aware browser operations. Detects wmux/cmux/tmux and routes to backend. Topics — ui-test (snapshots, click/fill, shadow DOM), credential-issue (login via backend -> wait sign-in -> issue/refresh token/secret). Use when: \"browser\", \"web-browser\", \"ui-test\", \"credential-issue\", \"playwright\", \"chrome-devtools\", \"UI check\", \"browser test\", \"screen verify\", \"Playwri","codeSnippets":[],"executableExamples":[{"language":"text","snippet":"web-browser (Step 0: environment detection — shared by all topics)\n  ├─→ ui-test (UI verification)\n  │     └─→ cdp-trace (extends ui-test for closed shadow DOM)\n  └─→ credential-issue (browser-login-assisted token/key issuance)\n        └─→ chrome-devtools backend preferred (reuses the user's real logged-in session)"},{"language":"bash","snippet":"# WSL detection — either signal is sufficient\n[[ -n \"$WSL_DISTRO_NAME\" ]] && echo \"host=wsl\"\ngrep -qi microsoft /proc/version 2>/dev/null && echo \"host=wsl\""},{"language":"bash","snippet":"# wmux check\n[[ -n \"$WMUX\" ]] || command -v wmux >/dev/null 2>&1\n\n# cmux check (detect via ANY of these; CMUX_SESSION is NOT set by cmux app)\n[[ -n \"$CMUX_BUNDLE_ID\" || -n \"$CMUX_PANEL_ID\" || -n \"$CMUX_BUNDLED_CLI_PATH\" ]] || command -v cmux >/dev/null 2>&1"},{"language":"bash","snippet":"wmux browser open <url>          # navigate (= playwright navigate)\nwmux browser snapshot            # get accessibility tree with @eN refs\nwmux browser click @eN           # click element\nwmux browser type @eN <text>     # type into element\nwmux browser fill @eN <value>    # set input value\nwmux browser get-text            # get page text\nwmux browser screenshot          # capture screenshot\nwmux browser eval <js>           # run JavaScript\nwmux browser back                # go back\nwmux browser forward             # go forward\nwmux browser reload              # reload page"},{"language":"bash","snippet":"# 1. Install playwright into .tmp/ and pull headed chromium\ncd <repo>/.tmp && npm init -y && npm install playwright@latest\nnpx playwright install chromium\n\n# 2. Run cdp-trace.js (user-visible browser)\n# --parts (canonical, plural) and --part (legacy singular) are both accepted;\n# the script tolerates either form to match the historical Quick Reference example.\nnode scripts/cdp-trace.js --url http://<target>/<path> --parts \"app-group,card-wrapper\""},{"language":"javascript","snippet":"const { chromium } = require('playwright');\n\n(async () => {\n  // headless: false — user-visible (per web-browser SKILL.md Step 0 user-visibility rule)\n  const browser = await chromium.launch({ headless: false, slowMo: 800 });\n  const page = await browser.newContext({ ignoreHTTPSErrors: true, viewport: null }).then(c => c.newPage());\n\n  await page.goto(URL, { waitUntil: 'domcontentloaded' });\n  // (handle login / auth as needed)\n\n  const cdp = await page.context().newCDPSession(page);\n  await cdp.send('DOM.enable');\n  await cdp.send('CSS.enable');\n\n  // pierce:true — expose closed shadow roots as well\n  const { root } = await cdp.send('DOM.getDocument', { depth: -1, pierce: true });\n\n  // Recursive walk — collect every element carrying a part attribute\n  function walk(node, found = []) {\n    if (!node) return found;\n    const attrs = node.attributes || [];\n    const partIdx = attrs.findIndex((v, i) => i % 2 === 0 && v === 'part');\n    if (partIdx >= 0) {\n      found.push({ id: node.nodeId, name: node.nodeName, part: attrs[partIdx + 1] });\n    }\n    if (node.children) node.children.forEach(c => walk(c, found));\n    if (node.shadowRoots) node.shadowRoots.forEach(s => walk(s, found));\n    return found;\n  }\n  const parts = walk(root);\n\n  for (const p of parts.filter(p => TARGET_PARTS.includes(p.part))) {\n    const cs = await cdp.send('CSS.getComputedStyleForNode', { nodeId: p.id });\n    const matched = await cdp.send('CSS.getMatchedStylesForNode', { nodeId: p.id });\n\n    console.log(`[part=\"${p.part}\"] (nodeId=${p.id}):`);\n    // computed values\n    for (const prop of ['width', 'grid-template-columns', '--app-card-min-width']) {\n      const v = cs.computedStyle.find(c => c.name === prop);\n      if (v) console.log(`  ${prop}: ${v.value}`);\n    }\n    // matched CSS rules (which sheet matched, which property won out)\n    console.log(`  matched CSS rules:`);\n    for (const r of matched.matchedCSSRules || []) {\n      console.log(`    [${r.rule.origin}] ${r.rule.selectorList.t"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: web-browser\nmetadata:\n  author: es6kr\n  version: \"0.1.0\"\ndescription: |\n  Environment-aware browser operations. Detects wmux/cmux/tmux and routes to backend. Topics — ui-test (snapshots, click/fill, shadow DOM), credential-issue (login via backend -> wait sign-in -> issue/refresh token/secret). Use when: \"browser\", \"web-browser\", \"ui-test\", \"credential-issue\", \"playwright\", \"chrome-devtools\", \"UI check\", \"browser test\", \"screen verify\", \"Playwright test\", \"shadow DOM cascade\", \"::part not working\", \"CDP trace\", \"issue token\", \"service credential\", \"open login screen\", \"PAT refresh\", \"scope expansion\", \"device-code auth\", \"browser device-code\", \"GitHub social login\".\n---\n\n# Web Browser\n\nEnvironment-aware browser operations skill. Detects the runtime environment and routes to the\nappropriate browser backend, then runs one of two workflows: UI testing/verification (`ui-test`) or\nbrowser-login-assisted credential issuance (`credential-issue`).\n\n## Topics\n\n| Topic | Description | Guide |\n|-------|-------------|-------|\n| ui-test | Snapshot analysis, click/fill/verify, page-state diagnosis | [ui-test.md](./ui-test.md) |\n| cdp-trace | CDP-based closed shadow DOM cascade diagnosis (DOM.getDocument pierce:true + CSS.getMatchedStylesForNode) | [cdp-trace.md](./cdp-trace.md) |\n| credential-issue | service+command param → open login screen → wait for user login → issue access key/token/secret → hand off to automation | [credential-issue.md](./credential-issue.md) |\n\n## Topic Dependencies\n\n```\nweb-browser (Step 0: environment detection — shared by all topics)\n  ├─→ ui-test (UI verification)\n  │     └─→ cdp-trace (extends ui-test for closed shadow DOM)\n  └─→ credential-issue (browser-login-assisted token/key issuance)\n        └─→ chrome-devtools backend preferred (reuses the user's real logged-in session)\n```\n\n- **Step 0 (below) is shared** — every topic detects the backend first, then runs its workflow.\n- `ui-test`, `cdp-trace` are the UI-testing family.\n- `credential-issue` reuses the same backend routing + the user-visibility rule, generalized into a\n  service+command parameterized auth flow.\n- **Authentik SSO verification** (`sso-verify`) is **not** included in this skill — it remains in a\n  separate local-only `sso-verify` skill (user-environment specific, untracked).\n\n## CRITICAL — capturing a credential-input screen requires an explicit ask (HARD STOP)\n\n**Before capturing a sign-in / credential-input screen — accessibility snapshot, screenshot, or any\nfull page-content read — call `AskUserQuestion` and get explicit approval.** Applies to every topic\nin this skill and to every backend.\n\nThe reason is not privacy etiquette, it is a measured leak path: a browser profile's saved-password\nautofill populates the password field, and the accessibility tree renders that field's **value in\nplaintext**. The capture therefore carries a live credential into the transcript even though nothing\nwas typed and no screenshot of characters was taken. `document.bo"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn74k8yfvftx6f062qa8fzyd8h8373jd\",\n  \"slug\": \"web-browser\",\n  \"version\": \"0.2.10\",\n  \"publishedAt\": 1791273349382\n}"},{"path":"cdp-trace.md","content":"# CDP Trace — Closed Shadow DOM Cascade Diagnosis\n\nExtract the computed style + matched CSS rules of elements inside a closed shadow DOM directly via the Chrome DevTools Protocol (CDP). Identifies which stylesheet is the actual carrier and verifies cascade entry when `::part(...)` outer-scope selectors fail to apply.\n\n## When to use\n\n- Outer `::part(<name>) { ... }` rules visually have no effect\n- You need the computed style of an element inside a closed shadow DOM\n- You need to identify the carrier (outer document vs shadow-root inject)\n- You need to verify the cascade for `[part=\"...\"]` direct selectors on a web component\n\n## Mechanism\n\n`DOM.getDocument({ depth: -1, pierce: true })` of the Chrome DevTools Protocol (Playwright `newCDPSession`) returns the full DOM tree **including closed shadow roots**. For each `nodeId`, call `CSS.getComputedStyleForNode` + `CSS.getMatchedStylesForNode` to dump the applied rules and the rules that were ignored.\n\n## Quick Reference\n\n```bash\n# 1. Install playwright into .tmp/ and pull headed chromium\ncd <repo>/.tmp && npm init -y && npm install playwright@latest\nnpx playwright install chromium\n\n# 2. Run cdp-trace.js (user-visible browser)\n# --parts (canonical, plural) and --part (legacy singular) are both accepted;\n# the script tolerates either form to match the historical Quick Reference example.\nnode scripts/cdp-trace.js --url http://<target>/<path> --parts \"app-group,card-wrapper\"\n```\n\n## Script pattern\n\n```javascript\nconst { chromium } = require('playwright');\n\n(async () => {\n  // headless: false — user-visible (per web-browser SKILL.md Step 0 user-visibility rule)\n  const browser = await chromium.launch({ headless: false, slowMo: 800 });\n  const page = await browser.newContext({ ignoreHTTPSErrors: true, viewport: null }).then(c => c.newPage());\n\n  await page.goto(URL, { waitUntil: 'domcontentloaded' });\n  // (handle login / auth as needed)\n\n  const cdp = await page.context().newCDPSession(page);\n  await cdp.send('DOM.enable');\n  await cdp.send('CSS.enable');\n\n  // pierce:true — expose closed shadow roots as well\n  const { root } = await cdp.send('DOM.getDocument', { depth: -1, pierce: true });\n\n  // Recursive walk — collect every element carrying a part attribute\n  function walk(node, found = []) {\n    if (!node) return found;\n    const attrs = node.attributes || [];\n    const partIdx = attrs.findIndex((v, i) => i % 2 === 0 && v === 'part');\n    if (partIdx >= 0) {\n      found.push({ id: node.nodeId, name: node.nodeName, part: attrs[partIdx + 1] });\n    }\n    if (node.children) node.children.forEach(c => walk(c, found));\n    if (node.shadowRoots) node.shadowRoots.forEach(s => walk(s, found));\n    return found;\n  }\n  const parts = walk(root);\n\n  for (const p of parts.filter(p => TARGET_PARTS.includes(p.part))) {\n    const cs = await cdp.send('CSS.getComputedStyleForNode', { nodeId: p.id });\n    const matched = await cdp.send('CSS.getMatchedStylesForNode', { nodeId: p.id });\n\n    console.log(`[part=\"${p.part}\""},{"path":"CHANGELOG.md","content":"# Changelog\n\n## [0.2.10](https://github.com/es6kr/skills/compare/web-browser-v0.2.9...web-browser-v0.2.10) (2026-10-04)\n\n\n### Bug Fixes\n\n* **web-browser:** resolve host OS layer before backend, scope CDP-hostile table per host ([#571](https://github.com/es6kr/skills/issues/571)) ([1bd3d66](https://github.com/es6kr/skills/commit/1bd3d662c7b29a82322476f36c9aba7dfb485295))\n\n## [0.2.9](https://github.com/es6kr/skills/compare/web-browser-v0.2.8...web-browser-v0.2.9) (2026-09-18)\n\n\n### Bug Fixes\n\n* **cleanup:** make the session-end report table self-sufficient ([#487](https://github.com/es6kr/skills/issues/487)) ([c4a0255](https://github.com/es6kr/skills/commit/c4a02557fb8de3b32cf337c549f62535dabf824b))\n\n## [0.2.8](https://github.com/es6kr/skills/compare/web-browser-v0.2.7...web-browser-v0.2.8) (2026-08-26)\n\n\n### Bug Fixes\n\n* accumulate 16 patch-level bug fixes and guard enhancements across skills ([d214e5d](https://github.com/es6kr/skills/commit/d214e5dcc7fac1bc07baf3b6cec62999aea732f0))\n* **core:** align workflow steps, next suggestion patterns, and browser topics ([c68d489](https://github.com/es6kr/skills/commit/c68d489d01a79862b8933b4a0542168cf676cd3a))\n* promote next-fix batch (consolidate fabrication guard, session rewind, config-driven PR base) ([7ca0ccb](https://github.com/es6kr/skills/commit/7ca0ccbf13cefafedc33a16a7361756c95f8b8f6))\n\n## [0.2.7](https://github.com/es6kr/skills/compare/web-browser-v0.2.6...web-browser-v0.2.7) (2026-08-17)\n\n\n### Bug Fixes\n\n* promote next-fix staging (30 fixes across 14 skills) ([ee467c0](https://github.com/es6kr/skills/commit/ee467c045d779d7b80d30f160763ec3534a9742b))\n* **web-browser:** credential-issue backend routing — session-existence gate + account-mismatch rule ([e5a9098](https://github.com/es6kr/skills/commit/e5a90986812c90b101a24554f3de9038a59906b4))\n* **web-browser:** document virtualized table bulk row operation pattern ([c5bc8f0](https://github.com/es6kr/skills/commit/c5bc8f0610263a963e8a75bfd30f36f2a5dafa76))\n* **wip:** cross-ref PR-URL and TaskCreate subject repo-qualifier rules ([#186](https://github.com/es6kr/skills/issues/186)) ([4982364](https://github.com/es6kr/skills/commit/49823641a7b08123ebd0325273892bee41bc3280))\n\n## [0.2.6](https://github.com/es6kr/skills/compare/web-browser-v0.2.5...web-browser-v0.2.6) (2026-08-09)\n\n\n### Bug Fixes\n\n* **consolidate:** address CodeRabbit/Copilot review findings on PR [#270](https://github.com/es6kr/skills/issues/270) ([3b11a73](https://github.com/es6kr/skills/commit/3b11a730b5ad68803d35a8264eda540e48265d75))\n* promote accumulated next-fix fixes to main ([95656e9](https://github.com/es6kr/skills/commit/95656e9b551ee0bb77904a0a571d49c53bc01cc9))\n* **web-browser:** add revoke command type to credential-issue topic ([306cf63](https://github.com/es6kr/skills/commit/306cf63752638d2cae7b924978cb036467382b00))\n* **web-browser:** add revoke command type to credential-issue topic ([cbd7121](https://github.com/es6kr/skills/commit/cbd712145d6faf293409df956706b1afae8ef9"},{"path":"credential-issue.md","content":"# Credential Issue (web-browser topic)\n\nTake a **service** + **command** as parameters, open the service's login screen via the detected\nbrowser backend, wait for the user to sign in, then on a completion signal **issue the requested\naccess key / token / secret** and hand the result to follow-up automation (aws-cli upload, `gh secret\nset`, terraform var injection, etc.).\n\nThis generalizes the \"open the page + user interaction + collect the result\" pattern into a reusable\nparameterized flow. It is the credential-issuance counterpart to [ui-test.md](./ui-test.md).\n\n## Parameters\n\n| Param | Meaning | Example |\n|-------|---------|---------|\n| `service` | The provider whose console issues the credential | `google-forms`, `cloudflare-r2`, `github`, `oci`, `aws`, `authentik` |\n| `command` | What to issue / do once logged in | `issue Google API OAuth token`, `issue R2 S3 token`, `issue fine-grained PAT`, `revoke <key-id>` (see \"Revoke flow\" below) |\n| `login-url` | Direct URL to the issuance page (when known) | `https://console.cloud.google.com/apis/credentials`, `https://dash.cloudflare.com/?to=/:account/r2/api-tokens` |\n| `handoff` | Follow-up automation to run with the issued credential | `gcloud auth print-access-token`, `aws s3 cp`, `gh secret set` |\n\n## Backend selection (Step 0 + credential-specific preference)\n\nDetect the backend via **SKILL.md Step 0** first. For credential issuance the preference order\ndiffers from ui-test, because the user must **sign in** and reusing their real logged-in session is\nfastest:\n\n| Priority | Backend | Why | When |\n|----------|---------|-----|------|\n| 1 | **chrome-devtools** (real session) | Reuses the user's already-logged-in browser session — often no login needed | `chrome-devtools-mcp` connected **AND the instance actually holds a logged-in session** (see session-existence gate below) |\n| 2 | **Default browser** (`Start-Process <url>` / `open <url>`) | Opens the user's real browser (real session, fully interactive) | login-required + chrome-devtools absent |\n| 3 | **wmux/cmux panel** | User-visible panel, interactive | `$WMUX` / `$CMUX_SESSION` set |\n| 4 | Playwright MCP | **Last resort** — invisible window, user cannot log in interactively | only when a persisted/automated session already exists (no fresh login needed) |\n\n**Session-existence gate (HARD STOP — the priority column is conditional routing, not a fixed\nranking)**: each priority's \"Why\" is its **applicability condition**. chrome-devtools ranks 1st\n*because* it reuses a real logged-in session — an MCP-launched instance whose `list_pages` shows only\n`about:blank` (or whose target page redirects to a login screen) has **no session to reuse**, so the\nrank-1 rationale is void and a backend that *does* hold a session (e.g., an already-open cmux panel)\noutranks it. Before switching backends mid-flow, verify the destination backend actually holds a\nlogged-in session; if it does not, the switch buys nothing and costs the user a fresh login plus a\nsecond br"}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":2124,"uniquenessScore":42,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T05:13:16.327Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T05:13:16.327Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T10:42:28.109Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}