| name | browserclaw |
| description | Use the browserclaw CLI to capture browser traffic, infer API endpoints, generate Python clients, OR decrypt + inject Chrome cookies for session reuse. Use when asked to reverse-engineer a website, generate an API client from a HAR, or reuse a logged-in Chrome session in a Playwright browser. |
| agent_scope | Any AI agent (Codex, Claude Code, Hermes, AO) |
browserclaw
What this skill covers
The browserclaw CLI (a single Python binary installed via pip install -e .) has two halves:
- Traffic capture + client generation — record a Chromium session to a HAR, infer the API endpoints, emit a
requests-based Python client + MCP tool definitions.
- Cookie decrypt + inject (introduced in PR #6) — read the user's logged-in cookies out of the local Chrome/Brave/Edge SQLite DB, decrypt the AES-128 ciphertext using the macOS Keychain password, and inject them into a fresh Playwright Chrome so an agent can act as that user without re-authenticating.
This file documents the cookies subcommands. The capture / infer / generate flow is documented in ~/.claude/skills/browserclaw/SKILL.md (legacy) and the upstream browserclaw/SKILL.md.
⚡ AUTH-GATED SHARE LINK — FIRE THIS ON FIRST REFUSAL (added 2026-07-20)
Trigger (read this BEFORE running any anonymous fetch): the user asks you to read / save / summarize / extract / ingest / archive / make a PR from the content of an auth-gated share link — share.gemini.google/..., chatgpt.com/share/..., docs.google.com/document/d/.../edit (restricted), notion.so/... shared page, vendor AI share dialog — AND an anonymous fetch (curl, web_extract, browser_navigate with no cookies) returns the vendor's sign-in shell with the content loaded client-side only after auth.
Action (do NOT post an unblock menu, do NOT ask the user to paste the content):
- Run
browserclaw cookies decrypt on the user's Chrome Default profile (or sweep Profile 1 / Profile 2 / Aside / Brave / Edge if 0 cookies — see references/multi-profile-cookie-scan.md).
- Run
browserclaw cookies inject with --browser-channel chromium --headless --wait-after-load 12 --print-text 100000 to dump the page text.
- If
--print-text truncates mid-sentence (Gemini share pages lazy-load), re-extract via Playwright + scroll-to-bottom with the same cookies.
- Verify the page loaded as the user (expected first-user message in the dump, NOT the vendor sign-in form). Then proceed with the user's actual ask against the captured content.
The 5-step recipe is verified end-to-end at references/gemini-share-link-as-user.md (Gemini share Td7fA4pzuvMs, 2026-07-20, 79 Google cookies decrypted, 169KB page text extracted, campaign module saved via PR $GITHUB_REPOSITORY#8483). The full session analysis — including the user's verbatim correction — is at references/gemini-share-link-stopping-pattern.md.
Why this rule exists: posting a 4-option unblock menu ("paste the content / re-share publicly / Doc export / proceed from title alone") on first refusal was the failure mode the user explicitly corrected in Slack thread C0AH3RY3DK6/p1784580748.125749 (verbatim: "use /browser or /browserclaw headless next time without asking"). Doing the recipe costs ~5 seconds of browserclaw time; asking the user costs them a full reply round-trip and breaks their flow. Bias toward running the recipe.
Companion contract test: tests/test_browser_command_mentions_browserclaw.py in ~/jleechanclaw (PR jleechanorg/jleechanclaw#788) fails if ~/.claude/commands/browser.md drops the browserclaw reference or the auth-gate recipe pointer. SOUL.md ## COMMIT: read-auth-gated-share-links-with-browserclaw (53rd COMMIT, added 2026-07-20) is the trigger-based contract that enforces this on every session. The contract test + the SOUL.md COMMIT + this SKILL.md section are the three layers of the durable fix.
When to use cookies decrypt + cookies inject
Use this pair when:
- The site is behind a login that requires MFA / SSO / WebAuthn (programmatic re-auth is impractical).
- The user has Chrome open and already logged into the target site.
- You need a Playwright session that appears as the user — same cookies, same session, same auth tokens.
- A token rotation /
invalid_auth incident requires you to read what cookies Chrome currently has so you can decide whether to re-decrypt or escalate.
- The user asks you to "read" or "save" content from an auth-gated share link (Gemini share
https://share.gemini.google/…, ChatGPT shared conversation https://chatgpt.com/share/…, Google Doc with restricted access, Notion shared page, etc.) and the page returns a sign-in shell when fetched anonymously. Don't ask the user to paste the content — reach for this pair on the first refusal and read the page as them, headless. Verified recipe at references/gemini-share-link-as-user.md.
Do not use this for:
- Sites you do not have permission to access. The skill trusts the user's local keychain; if you are not the user, this is unauthorized access.
- Bypassing 2FA, CAPTCHAs, or anti-bot protections. The skill reuses an existing logged-in session — it does not break new ones.
- Linux/Windows browsers. The PBKDF2 + Keychain parameters are macOS-only (salt
saltysalt, iterations 1003, IV = 16 spaces). Linux Chromium uses peanuts / 1 iteration and is not supported by this CLI yet.
Prerequisites
pip install -e '.[dev]'
python -m playwright install chromium
- macOS only (uses
security find-generic-password against the Login keychain).
- Chrome (default) or Brave / Edge with explicit
--keychain-service / --keychain-account flags.
- The user must have already logged into the target site in Chrome at least once (the SQLite Cookies DB must contain the encrypted values).
- Chrome must have been closed OR the file must be readable — the CLI copies the DB to a temp file before opening, so live read is safe even while Chrome is running, but a value that exists only in Chrome's in-memory cache may not yet be flushed to disk.
Subcommand: browserclaw cookies decrypt
Read the local Chrome Cookies SQLite DB, decrypt every cookie value with the Keychain-derived AES key, and write a Playwright-compatible JSON.
Synopsis
browserclaw cookies decrypt \
--db ~/Library/Application\ Support/Google/Chrome/Default/Cookies \
--output ./cookies.json \
[--domain-filter '%slack.com%'] \
[--keychain-service 'Chrome Safe Storage'] \
[--keychain-account 'Chrome'] \
[--summary]
Arguments
| Flag | Type | Default | Required | Purpose |
|---|
--db | path | — | yes | Absolute path to the Chromium-format Cookies SQLite. The CLI copies it to a temp file before opening, so passing the live Chrome-locked path is safe. |
--output, -o | path | — | yes | Destination for the decrypted JSON (Playwright storage_state shape: {"cookies": [...], "origins": []}). |
--domain-filter | str | % | no | SQL LIKE pattern against host_key. Use %slack.com% to grab only Slack cookies, %.google.com% to grab all subdomains. |
--keychain-service | str | Chrome Safe Storage | no | Override for Brave (Brave Safe Storage) or Edge (Microsoft Edge Safe Storage). |
--keychain-account | str | Chrome | no | Override for Brave (Brave) or Edge (Microsoft Edge). |
--summary | flag | off | no | Print a one-line domain name len=N per cookie instead of full values. Use when debugging to avoid logging tokens. |
Outputs
- Writes
--output as a Playwright storage_state JSON: {"cookies": [{"name","value","domain","path","expires","secure","httpOnly","sameSite"}, ...], "origins": []}.
- Prints either the JSON summary (default) or the per-cookie summary lines (
--summary).
- Exit code
0 on success, non-zero on CookieDecryptError (missing DB, empty file, no meta.version row, Keychain lookup failure).
Example — extract Slack cookies for an agent run
browserclaw cookies decrypt \
--db ~/Library/Application\ Support/Google/Chrome/Default/Cookies \
--output /tmp/slack-cookies.json \
--domain-filter '%slack.com%' \
--summary
Example — Brave browser
browserclaw cookies decrypt \
--db ~/Library/Application\ Support/BraveSoftware/Brave-Browser/Default/Cookies \
--output /tmp/brave-cookies.json \
--keychain-service 'Brave Safe Storage' \
--keychain-account 'Brave'
Example — Aside browser (2026-06-27, primary browser)
Aside uses the same Chromium v24 cookie schema but its own Keychain entry (Aside Safe Storage). The DB path is ~/Library/Application Support/Aside/Default/Cookies.
browserclaw cookies decrypt \
--db ~/Library/Application Support/Aside/Default/Cookies \
--output /tmp/aside-cookies.json \
--keychain-service 'Aside Safe Storage' \
--keychain-account 'Aside'
Keychain prerequisite: the Aside Safe Storage entry is only created after Aside writes its first cookie to disk. If you see Keychain lookup failed for service='Aside Safe Storage' account='Aside', log into a site in Aside once (any login), then re-run. Verify the entry exists:
security find-generic-password -s 'Aside Safe Storage' -a 'Aside' -w
Cross-browser portability warning: the underlying PBKDF2 password is per-browser (each browser has its own Keychain entry). Even after cookies decrypt succeeds, cookies decrypted from Aside cannot be replayed into a Chrome session and vice versa without re-encryption under the target's password — browserclaw does NOT do this re-encryption. Use the cookies decrypt + cookies inject cycle only on the same browser that produced the cookies.
Aside cookies decrypt but Slack auth rejects them (verified 2026-07-19, mcp_agent_mail OAuth fix): Aside's d cookie decrypts to a hex string (Slack's newer encrypted cookie format) rather than the legacy xoxd-... URL-encoded format Chrome uses. Direct auth.test against the Aside-decrypted cookies returns {"ok":false,"error":"not_authed"} — the values are well-formed but Slack's session validation doesn't accept the newer format. For Slack targets specifically, prefer Chrome Default cookies over Aside cookies. Verified end-to-end: decrypting Chrome Default's d (legacy xoxd-...) and feeding that into cookies inject → Playwright session authenticated to https://app.slack.com/app-settings/... as the user.
Edge cases / failure modes
| Symptom | Likely cause | Fix |
|---|
CookieDecryptError: Cookie DB not found | Wrong path or the browser is not installed | Confirm path with ls "$HOME/Library/Application Support/Google/Chrome/Default/Cookies" |
CookieDecryptError: Cookie DB is empty | Chrome is currently writing to it; live copy is zero bytes | Quit Chrome fully, then re-run |
CookieDecryptError: is not a Chromium Cookies DB (no meta.version row) | Wrong file — it's not a Chromium Cookies SQLite | Check --db is a Cookies file, not Cookies-journal or Login Data |
Keychain lookup failed for service='Chrome Safe Storage' account='Chrome' | User clicked "Deny" on the keychain prompt, or the entry was removed | Open Keychain Access.app, search for Chrome Safe Storage, ensure it exists; re-run and click "Always Allow" |
| Output JSON has 0 cookies | --domain-filter is too narrow, or the user is not logged into that site in this profile | Drop --domain-filter to %, or switch --db to the correct profile (Profile 1, Profile 2, …) |
Output JSON has N>0 cookies but every value is "" (length fields populated) | Chrome v120+ App-Bound Encryption (v20 prefix) — Safe Storage decrypt is necessary but no longer sufficient; cookies.py only handles v10/v11 | Use the v20 bypass recipe in references/cdp-decrypt-via-headless-browser.md — launch headless Chrome with --remote-debugging-port, call CDP Network.getAllCookies, Chrome decrypts v20 in-process. Verified 2026-07-18. |
browserclaw --help returns empty / ModuleNotFoundError: No module named 'browserclaw.cli' (verified 2026-07-11) | Editable install's source worktree was deleted but the .pth still points to it. pip show browserclaw shows Editable project location: <some-path>; that directory no longer exists |
Subcommand: browserclaw cookies inject
Open a Playwright Chrome (or chromium), inject the cookies from a previously-decrypted JSON, navigate to a target URL, and optionally screenshot / dump page text.
Synopsis
browserclaw cookies inject \
--cookies ./cookies.json \
--goto https://app.slack.com/client \
[--browser-channel chrome] \
[--headless] \
[--wait-after-load 5] \
[--screenshot /tmp/slack-home.png] \
[--print-text 500]
Arguments
| Flag | Type | Default | Required | Purpose |
|---|
--cookies | path | — | yes | Path to a cookies JSON. Either the output of cookies decrypt or any Playwright storage_state file. |
--goto | URL | — | yes | Where to navigate after injection. Must be a URL whose domain matches the cookies (cookies are domain-scoped). |
--browser-channel | str | chrome | no | Playwright channel. Use chrome for the real installed Chrome, chromium for the headless test build, msedge for Edge. |
--headless | flag | off | no | Run without a visible window. Combine with --screenshot for evidence capture. |
--wait-after-load | float | 5.0 | no | Seconds to wait after page.goto returns, before printing diagnostics. Increase for SPAs that hydrate slowly. |
--screenshot | path | off | no | If set, save a full-page screenshot after navigation. Great for evidence + Slack thread attachments. |
--print-text | int | 0 | no | If > 0, print the first N characters of document.body.innerText after navigation. Useful for verifying the page loaded as the expected user. |
Outputs
- Opens (or spawns) a real browser window — visible by default unless
--headless.
- Prints the final URL, page title, and any
--print-text content to stdout.
- Writes screenshot to
--screenshot path if provided.
- Exit code
0 on successful navigation; non-zero if Playwright cannot launch, cookies JSON is empty, or page.goto errors.
Example — drive Slack web as the user
browserclaw cookies inject \
--cookies /tmp/slack-cookies.json \
--goto https://app.slack.com/client \
--browser-channel chrome \
--wait-after-load 5 \
--print-text 800 \
--screenshot /tmp/slack-home.png
Example — headless evidence capture
browserclaw cookies inject \
--cookies /tmp/slack-cookies.json \
--goto https://app.slack.com/client \
--browser-channel chromium \
--headless \
--wait-after-load 3 \
--screenshot ./evidence/slack-after-login.png
Edge cases / failure modes
| Symptom | Likely cause | Fix |
|---|
No cookies found in <path> | JSON is empty or has no cookies key | Re-run cookies decrypt and confirm the file has {"cookies": [...]} |
| Navigation lands on login page despite valid cookies | Domain mismatch — --goto is on a domain that does not match the cookie's host_key | Use a URL whose host matches at least one cookie domain |
| Navigation lands on login page despite valid cookies AND correct domain | Browser fingerprinting — site rejects Playwright-launched Chrome even with valid session cookies (LinkedIn, Twitter/X, Threads, Facebook, Dev.to observed 2026-07-05). The cookies are accepted by the HTTP layer but the site's JS anti-bot detects the Playwright Chrome fingerprint and redirects. browserclaw cookies inject is NOT a universal login bypass — it only works on sites that don't fingerprint-auth the session. | (1) Check if the site works in a normal Chrome tab with the same cookies (yes → fingerprint issue). (2) Try --browser-channel chromium instead of chrome (different fingerprint, sometimes passes). (3) For sites that always reject Playwright, use aside repl instead — Aside's persistent daemon has a real Chrome fingerprint and may pass. (4) If all fail, the user must log in manually in the target browser first, then re-decrypt. |
channel='chrome' opens a visible Chrome window during launch even with --headless flag (verified 2026-07-18, multi-portal tax drive) | Playwright's channel='chrome' spawns the system Chrome binary which has different headless-launch behavior than bundled Chromium-for-Testing — it briefly opens a visible window before transitioning to headless, AND the user's existing Chrome session can gain new tabs that pollute their work. This is the user's #1 complaint ("use headless browser, stop doing normal browser", "use headless chrome, it keeps opening up"). | Default: --browser-channel chromium (the bundled Chromium-for-Testing) — it's always headless and never opens a visible window, even though its TLS fingerprint fails more bot-detection checks. Only fall back to channel='chrome' when (a) the user has explicitly opted in ("show browser" / "headed mode" / "I want to see the window") AND (b) the target site blocks bundled Chromium's fingerprint. If is unavoidable, use AND verify with and that no new visible window appeared. |
Poll-until-cookies-appear pattern
When the browserclaw prerequisite is not yet met (user has never visited the target SPA in their browser, so the cookies-to-decrypt don't exist yet), do NOT just stop and ask the user to "go log in manually." Instead, register a recurring hermes cron that polls for the cookies and automatically drives the headless repro the moment they appear.
Verified worked example (2026-07-12, your-project.com SPA, campaign a1OGXHNxNdw1Id0iRfpR):
The user said "use /browserclaw to log in as me and get the repro." They had 81 valid *.google.com cookies but ZERO worldarchitecture-ai cookies. After confirming they hadn't visited the SPA in Chrome, I registered a recurring 5-min poll:
hermes cron create \
--name "wa-cookies-poll #8353 (5m) recurring" \
--schedule "every 5m" \
--repeat 99999 \
--deliver "slack:<channel-id>:<thread-ts>" \
--model MiniMax-M3 --provider minimax \
--prompt '<see template below>'
The cron prompt (template — fill in the campaign-specific context for your run):
You are a polling watcher for the <SPA_NAME> post-auth repro.
EVERY tick:
1. env -i HOME="$HOME" PATH="$HOME/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" \
"$(which browserclaw)" cookies decrypt \
--db "$HOME/Library/Application Support/Google/Chrome/Default/Cookies" \
--output /tmp/wa-cookies.json \
--domain-filter '%<SPA_DOMAIN>%' --summary 2>&1 | head -10
2. If output starts with "Wrote 0 cookies":
- If this is tick #4, #8, #12, ... (every ~20m) → post ONE line: ":hourglass: still polling for <SPA_DOMAIN> cookies (tick N)"
- Else → EXIT SILENTLY
- If total ticks > 24 (~2h) without cookies appearing → post "polling timed out after 2h" then `cronjob action=remove job_id=$CRON_JOB_ID`. EXIT.
3. If output starts with "Wrote N cookies" (N>0) → COOKIES APPEARED:
a. Sanity check: `python3 -c "import json; d=json.load(open('/tmp/wa-cookies.json')); print(len(d.get('cookies',[])))"`
b. Modify /tmp/repro-<id>/probe_with_real_session.py to point at wa-cookies.json instead of google-cookies.json.
c. Run it with --browser-channel chromium --headless; capture screenshot, DOM probe, console, network failures.
d. For each candidate campaign id (the user's reported one + any sibling-affected ones), navigate separately and capture symptom.
e. POST result in this Slack thread:
```
:unlock: /repro post-auth captured!
- <SPA_DOMAIN> cookies: N present
- Campaign <CID_1>: [loaded | 500 stuck | other symptom text]
- Evidence: /tmp/repro-<id>/post_auth_*.{png,txt}
- Verdict: REPRO | RELATED | NON-REPRO per /repro §4
```
f. Append evidence to the GitHub issue (`gh issue comment <N> --body-file ...` using env -i wrapper).
g. CRON SELF-CANCEL: `hermes cron remove $CRON_JOB_ID` (NOT `cronjob action=remove` — that CLI was renamed in 2026-07-13; `cronjob` is no longer on PATH and the old form fails with `FileNotFoundError`). See babysit-ao-pr-loop v1.3.1 changelog.
**How to determine your tick number N** (the "tick #4 / #8 / #12 ..." gate above):
The cron prompt is told to post hourglass updates at every 4th tick (~20 min), but does NOT know its own tick count. Three reliable ways to derive it, in order of preference:
1. **`jobs.json` counters** (authoritative — read at the top of every tick):
```bash
JOB_ID="<your cron job id — usually $CRON_JOB_ID from launchd env>"
python3 -c "
import json, datetime
d = json.load(open('$HOME/.hermes/cron/jobs.json'))
j = next((j for j in d['jobs'] if j['id'] == '$JOB_ID'), None)
if j:
completed = j.get('repeat', {}).get('completed', 0)
next_run = j.get('next_run_at')
created = j.get('created_at')
print(f'completed={completed} next_run_at={next_run} created_at={created}')
# If completed counter is reliable, tick = completed + 1
"
Caveat (verified 2026-07-12, cron 1f0822aae664 wa-cookies-poll): on the very first tick, repeat.completed = 0 AND last_run_at = null AND the JSON file may not have been flushed yet — jobs.json is updated by the gateway AFTER the prompt fires. Use the file mtime fallback below in that case.
-
File mtime fallback (works on tick #1, when jobs.json is stale):
stat -f '%Sm' /tmp/repro-<id>/wa-cookies.json
grep -A1 '"created_at"' ~/.hermes/cron/jobs.json | head -2 | grep -oE '20[0-9-]+T[0-9:.]+'
-
Compute from next_run_at + interval (works regardless of jobs.json freshness):
python3 -c "
import json, datetime
j = json.load(open('$HOME/.hermes/cron/jobs.json'))
target = next(j for j in j['jobs'] if '<your_job_name_substring>' in j['name'])
created = datetime.datetime.fromisoformat(target['created_at'])
interval = target['schedule']['minutes']
# current tick = ceil((now - created) / interval)
now = datetime.datetime.now(created.tzinfo)
print(f'tick ≈ {((now - created).total_seconds() / 60 / interval + 1):.0f}')
"
Absolute PATH note for hermes cron remove: under the env -i wrapper (per bashrc-profile-xapp-drift-blocks-launchd memory), which hermes returns $HOME/.local/bin/hermes. The cron prompt's self-cancel must use this absolute path or it will fail:
env -i HOME="$HOME" PATH="$HOME/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" \
"$HOME/.local/bin/hermes" cron remove "$CRON_JOB_ID"
Verified worked example (2026-07-12, cron 1f0822aae664): tick #1 detected via file mtime of /tmp/repro-a1OGXH/wa-cookies.json (20:47:12) vs cron created_at (20:41:08), interval 5 min → tick = ⌈(20:47:12 − 20:41:08) / 5⌉ + 1 = 2 (conservative; the first tick ran ~20:46). jobs.json repeat.completed = 0 and last_run_at = null confirmed this was an early tick where the gateway hadn't yet flushed the run counter.
Hard constraints:
- ALWAYS env -i ... PATH=... for gh / browserclaw / git (per bashrc-profile-xapp-drift-blocks-launchd memory).
- NEVER open the draft PR without explicit user "go" in this thread.
- NEVER log full cookie values to Slack — use --summary only.
- If browserclaw errors with Keychain lookup → post the exact stderr.
**When to use this pattern:**
- The user has 0 cookies for the SPA domain but they are signed into Google/GitHub/etc. in Chrome
- The bug is per-campaign (not auth) and the fix path doesn't strictly require browser access
- You have other work to do while waiting (don't block the user)
**When NOT to use this pattern:**
- The cookies would never appear (e.g. wrong Chrome profile, no path forward to manual visit)
- The bug requires an interactive flow that cookies can't capture (drag-drop, OAuth callback, payment)
## Security implications (READ BEFORE USING)
Cookie decryption is **sensitive**. The keychain password unlocks every cookie Chrome has stored for the user — Slack `d` tokens, GitHub `user_session`, Google `SID`, banking sessions, etc. Operators MUST:
1. **Treat decrypted cookies like raw passwords.** Do not log full cookie values to Slack, GitHub PR comments, gist attachments, or any external surface. Prefer `--summary` when sharing evidence.
2. **Never commit the output JSON.** Add `cookies.json` and `*-cookies.json` to `.gitignore` immediately. Delete after use with `shred -u cookies.json` if the disk is encrypted, otherwise `rm` is sufficient on APFS.
3. **Run in a private context.** If you invoke this from an AO worker, treat the worker session as if it has root-level access to the user's identity. Do not dispatch this skill into shared / multi-tenant AO lanes.
4. **Respect the trust model.** This skill reads only the *current user's own* Chrome profile, using *that user's* macOS Keychain. It does not (and cannot) read another user's cookies without their Keychain password. Do not extend it to do so.
5. **No bypass claims.** This skill does not bypass 2FA, CAPTCHA, or anti-bot systems. It reuses an existing logged-in session. Do not advertise it as a "login bypass."
6. **Audit logging.** Every invocation should appear in the agent's session log. If you are dispatching this via AO, include `--summary` output in the worker report and never the raw cookie values.
## Trust model diagram
```text
┌──────────────┐ security find-generic-password ┌──────────────────┐
│ user │ ─────────────────────────────────▶ │ macOS Keychain │
│ (you) │ ◀─────── "Chrome Safe Storage" ─── │ Login keychain │
└──────┬───────┘ └──────────────────┘
│ grants access once per session
▼
┌──────────────┐ PBKDF2-HMAC-SHA1 (1003 iters) ┌──────────────────┐
│ browserclaw │ ─────────────────────────────────▶ │ AES-128-CBC key │
│ cookies.py │ └────────┬─────────┘
└──────┬───────┘ │
│ reads Cookies SQLite (copy to tmp) │
▼ ▼
┌──────────────┐ v10 || AES(plaintext) ┌──────────────┐
│ cookies │ ──────decrypt──────────────────────────▶ │ JSON output │
│ .json │ │ (Playwright) │
└──────────────┘ └──────────────┘
Test script
A bash regression test lives at tests/test_browserclaw_skill.sh (this repo). It verifies:
- This SKILL.md exists and is non-empty.
browserclaw cookies decrypt is documented with a real example.
browserclaw cookies inject is documented with a real example.
- The security warning section is present.
- The installed
browserclaw CLI exposes the cookies subcommand (real binary surface, not just docs).
- An
--help invocation of cookies decrypt returns exit code 0.
Run it from this repo:
bash tests/test_browserclaw_skill.sh
Eval criteria — "working" looks like
browserclaw cookies decrypt --db <path> --output /tmp/c.json --domain-filter '%slack.com%' exits 0 and writes a JSON with >=1 cookie for an actively-used Slack profile.
browserclaw cookies inject --cookies /tmp/c.json --goto https://app.slack.com/client --browser-channel chromium --headless --wait-after-load 5 --screenshot /tmp/proof.png exits 0, writes a non-zero PNG, and prints a non-empty --print-text if requested.
- End-to-end (decrypt → inject → screenshot) is captured as evidence for PR /es layer-2 verification on PR #6.
Cross-references
- Source repo: github.com/jleechanorg/browserclaw
- PR introducing the feature: #6 — feat(cookies): Chrome cookie decrypt + Playwright inject for reuse of logged-in sessions
- Module source:
src/browserclaw/cookies.py (dataclasses, AES decrypt, Keychain lookup, JSON I/O)
- CLI source:
src/browserclaw/cli.py (argparse for cookies decrypt / cookies inject)
- Tests (10 cases, all green on macOS):
tests/test_cookies_decrypt.py
- Reference Chromium source:
os_crypt_mac.mm (PBKDF2 params, AES key derivation)
- Inspiration: pycookiecheat (MIT) — adapted for Python 3.11+ and DB v24+ SHA256(host) prefix.
- Related Hermes skills:
~/.claude/skills/browser-testing (Playwright MCP for localhost UI testing, headless) — use this skill for session-less UI testing, not for "act as user X" flows.
- v2 design (validated 2026-07-30, NOT YET IMPLEMENTED):
references/browserclaw-v2-design-2026-07-30.md — autonomous outcome-driven control plane, supersedes the "manual auth only" / "no auth bypass" ceiling. Captures the operator's three preference signals (no cookie promotion into BrowserClaw storage, env-configurable notifications defaulting to Hermes DM, automatic auth with manual fallback only after bounded recovery exhaustion). Read this before proposing any browserclaw feature work.
Version / metadata
- Last updated: 2026-07-18 (v0.3.0 — added v20/ABE bypass via headless Chrome + CDP
Network.getAllCookies recipe; verified on TaxDome drive recovering 247 unique tax-domain cookies across Chrome Default + Aside Default)
- browserclaw version: 0.1.0 (cookies added in commit
9320bc0 on branch feat/cookie-decrypt-inject)
- Tracking PR: #6
- Hermes skill location:
skills/browserclaw/SKILL.md (this file); also mirrored at ~/.claude/skills/browserclaw/SKILL.md
Multi-profile cookie scan — when the default Chrome profile returns 0 cookies
Before concluding "no session exists," sweep every local browser profile + non-Chrome browser. Sites like Venmo, niche SaaS tools, and personal banking may be logged in on Profile 1 / Profile 3 / Aside / Brave / Edge while Default has nothing. Full recipe, sweep loop, env -i wrapper, and the Gmail-as-data-fallback pattern in references/multi-profile-cookie-scan.md. TL;DR:
for prof in "Default" "Profile 1" "Profile 3"; do
env -i HOME="$HOME" PATH="$HOME/.local/orch-venv/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" \
browserclaw cookies decrypt --db "$HOME/Library/Application Support/Google/Chrome/$prof/Cookies" \
--output "/tmp/${prof// /}-cookies.json" --domain-filter '%<target>%' --summary 2>&1 | head -3
done
If all return 0, run gog gmail search --account $USER@gmail.com "from:<domain> OR subject:<keyword>" to check whether the data is already emailed as monthly activity statements — many fintech / SaaS sites ship history emails. If Gmail also doesn't have it, the task needs a vendor-support / manual / alternate-source path; don't pretend headless injection will work.
When cookies decrypt returns empty values — v20 (App-Bound Encryption) bypass
When Chrome v120+ writes cookies with the v20 prefix, the Safe Storage + PBKDF2 key from the Keychain alone is insufficient — browserclaw cookies decrypt (verified 2026-07-18) returns cookies with length(value) populated but value="" for v20 entries, and silently skips the rest. The v20 prefix means App-Bound Encryption (ABE): the encrypted blob is wrapped with a key only the running Chrome process can decrypt.
Symptom: --summary shows non-zero lengths for hundreds of cookies, but cookies.json has value="" everywhere. --domain-filter '%<real-domain>%' returns N>0 cookies but no usable auth tokens.
Fix (verified 2026-07-18, TaxDome drive, 247 unique tax-domain cookies recovered): launch a headless Chrome with --remote-debugging-port=N pointing at a copy of the user's profile, then call CDP Network.getAllCookies — Chrome decrypts v20 in-process and returns plaintext. Full recipe in references/cdp-decrypt-via-headless-browser.md. TL;DR:
TMPDATA=/tmp/job-chrome-debug
mkdir -p "$TMPDATA/Default"
SRC="$HOME/Library/Application Support/Google/Chrome/Default"
for f in Cookies 'Local State' Preferences 'Secure Preferences' 'Web Data' 'Login Data'; do
cp -L "$SRC/$f" "$TMPDATA/Default/$f" 2>/dev/null
done
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--headless=new --disable-gpu --no-sandbox \
--remote-debugging-port=9222 \
--user-data-dir="$TMPDATA" --profile-directory=Default \
about:blank > /tmp/job-cdp.log 2>&1 &
Run multiple headless instances in parallel for Chrome + Aside + Brave + Edge — distinct ports (9222/9223/9224/...) and distinct --user-data-dir=. Verified 247 cookies across Chrome Default + Aside Default on 2026-07-18.
Then convert the JSONL output to Playwright storage_state and use cookies inject as normal, OR feed directly into curl --cookie-jar for backend API drives.
What this bypass does NOT solve: Cloudflare Turnstile on the first page load rejects headless Chrome UA. Once you have decrypted session cookies, drive via CDP Network.setCookie against the user's existing visible Chrome (or chrome --remote-debugging-port=9222 keeping their tabs) rather than a fresh headless instance.
Cookie freshness caveat: Cloudflare _abck / Akamai bm_sz rotate every 5-10 min. Drive portals within that window after dumping, or re-dump before each Cloudflare-fronted navigation.
Pair with
references/gemini-share-link-stopping-pattern.md — the 2026-07-20 incident that prompted this rule; full failure-mode analysis + 3-layer closeout (skill + SOUL.md COMMIT + contract test in jleechanclaw PR #788). Read this whenever you're tempted to post an "unblock options" menu on auth-gate refusal.
references/gemini-share-link-as-user.md — verified 5-step recipe (env-isolated browserclaw cookies decrypt → cookies inject with --browser-channel chromium --headless) for reading auth-gated share links as the user.
references/multi-profile-cookie-scan.md — full multi-profile sweep loop + Gmail-as-data-fallback (2026-07-14)
references/cdp-decrypt-via-headless-browser.md — v20 (App-Bound Encryption) bypass when cookies decrypt returns empty values; headless-Chrome + CDP Network.getAllCookies recipe (2026-07-18)
references/multi-portal-cookie-replay.md — verified 10-portal pattern (filter → drive → mirror → cron-poll) from the 2026-07-18 tax-return drive; per-portal bot detection matrix + TaxDome end-to-end path
references/gmail-as-fallback-when-sso-clickthrough-blocked.md — when same *.google.com cookies decrypt successfully but cookies inject against the vendor site (e.g. Monarch "Continue with Google") lands on the login form because SSO needs an interactive click-through headless can't drive. Verified 2026-07-22: Gmail via the same cookies works, vendor alert emails carry structured data (budget overruns, "Access Expired" notices, transaction amounts) for real analysis. Run the recipe first; do not propose an unblock menu.
references/direct-graphql-stolen-cookie-auth.md — escape hatch when SSO-gated UI blocks Playwright: POST directly to the site's protected GraphQL/REST with stolen cookies + X-CSRFToken + correct Origin/Referer headers. Verified 2026-07-22 against https://api.monarch.com/graphql returning +$335,375.10 net worth (37 accounts) when Playwright headless couldn't drive Google SSO. Covers CSRF-header variants (Django/Laravel/ASP.NET/Express), error-code→cause matrix, and the P6 lesson that live API data can contradict earlier in-thread claims — always re-fetch and present the verified numbers plus a diff vs the prior claim.
references/data-extraction-recipes-from-stolen-cookies.md — when auth is solved and you need the right operation names + query shapes to extract the actual data. Verified 2026-07-22 against Monarch: 4 GraphQL queries ( / / / ) that produced 389 daily snapshots, 13-month per-accountType decomposition, and monthly cashflow in one session. Covers the typed-filter-input shape ( instead of top-level vars), the requirement, the quirk (which gates which accounts appear in the dashboard trend), and how to detect single-day feed glitches ($552K plunge + $543K bounce in 24h = not real).