{"id":50621061,"url":"https://github.com/yolo-labz/chrome-bridge","last_synced_at":"2026-06-06T12:01:40.316Z","repository":{"id":361050849,"uuid":"1232521441","full_name":"yolo-labz/chrome-bridge","owner":"yolo-labz","description":"Trusted-event Chrome MV3 automation bridge. Extension + Python relay on 127.0.0.1:9224. Cross-platform.","archived":false,"fork":false,"pushed_at":"2026-06-05T20:45:13.000Z","size":234,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-05T22:20:32.928Z","etag":null,"topics":["automation","chrome","claude-code","cross-platform","extension","mv3","python","relay"],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yolo-labz.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-08T02:35:27.000Z","updated_at":"2026-06-05T20:45:17.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yolo-labz/chrome-bridge","commit_stats":null,"previous_names":["yolo-labz/chrome-bridge"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/yolo-labz/chrome-bridge","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yolo-labz%2Fchrome-bridge","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yolo-labz%2Fchrome-bridge/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yolo-labz%2Fchrome-bridge/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yolo-labz%2Fchrome-bridge/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yolo-labz","download_url":"https://codeload.github.com/yolo-labz/chrome-bridge/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yolo-labz%2Fchrome-bridge/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33981125,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-06T02:00:07.033Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","chrome","claude-code","cross-platform","extension","mv3","python","relay"],"created_at":"2026-06-06T12:01:39.368Z","updated_at":"2026-06-06T12:01:40.299Z","avatar_url":"https://github.com/yolo-labz.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cpicture\u003e\n  \u003csource media=\"(prefers-color-scheme: dark)\" srcset=\"docs/assets/hero-dark.svg\"\u003e\n  \u003csource media=\"(prefers-color-scheme: light)\" srcset=\"docs/assets/hero-light.svg\"\u003e\n  \u003cimg alt=\"chrome-bridge: trusted-event Chrome MV3 automation bridge\" src=\"docs/assets/hero-dark.svg\"\u003e\n\u003c/picture\u003e\n\n# chrome-bridge\n\n\u003e Trusted-event Chrome automation bridge for the yolo-labz fleet — MV3 first-party extension loaded into a dedicated **Profile-Auto** Chrome instance, paired with a localhost relay daemon (`cb` CLI) that any sibling plugin (`claude-mac-chrome`, `lcc`, `wa`) can shell out to.\n\nSolves the `isTrusted=true` event-trust gate on Vue/React SPAs and the JA4+ TLS fingerprinting gate on regulated sites — without exposing Pedro's daily Chrome to CDP attach (catastrophic blast radius), without sending his cookies to a third-party SaaS like Browserbase (principle IX violation), and without the Patchright bundled-Chromium signature that LinkedIn Q1 2026 BrowserGate scanner now flags.\n\n## Capability\n\n**Pattern.** Trusted-event Chrome MV3 automation bridge — an unpacked MV3 extension is loaded into a dedicated **Profile-Auto** Chromium instance; its service worker long-polls a Python stdlib relay daemon on `127.0.0.1:9224`, dispatches jobs to `chrome.cookies` / `chrome.scripting` / `chrome.debugger.Input`, and returns results as JSON.\n\n**Trade-off.** Requires an unbranded Chromium binary (Chrome 137+ `DisableLoadExtensionCommandLineSwitch` policy is now compiled in, so Google-branded Chrome silently drops `--load-extension`) and a one-time extension install — in exchange for `isTrusted=true` events that CDP-driven Playwright/Puppeteer cannot synthesize, and a relay surface that works on Linux where AppleScript is unavailable.\n\n**Use when.** A Claude Code plugin or shell pipeline needs to inject trusted DOM events into a Vue/React SPA from a Linux or macOS workstation, with the daily Chrome profile untouched and stealth domains (LinkedIn, banking) hard-blocked in the extension source.\n\n```bash\ngit clone https://github.com/yolo-labz/chrome-bridge\n./launch/profile-auto.sh\n./cli/cb dispatch \u003ccommand\u003e\n```\n\n## Demo\n\nA 16-second screen-record of the bridge on a fresh stealth profile — `./launch/profile-auto.sh --bg` brand-guards the Chromium candidate, spawns Profile-Auto with the unpacked MV3 extension loaded, registers the relay on `127.0.0.1:9224`, and round-trips three `cb` verbs (`ping`, `tabs`, `debug-click`) including a trusted-event click returning `isTrusted: true`:\n\n![chrome-bridge demo: profile-auto launch, brand-guard probe, relay handshake, cb dispatch round-trip with isTrusted=true](docs/assets/bridge-demo.gif)\n\nThe capture runs against a fresh `--user-data-dir` with no real cookies, no real authentication, and no real PII; every value (`PID 184932`, the user-agent string, the timestamp) is a literal token rendered into the demo, not a leaked artifact from Pedro's daily session.\n\n## How `chrome-bridge` compares\n\nClosest peers in the Chrome automation ecosystem, scored against the use case (cross-platform trusted-event injection from a Python CLI):\n\n| Capability | `chrome-bridge` | [`playwright`](https://playwright.dev) | [`puppeteer`](https://pptr.dev) | [`selenium`](https://www.selenium.dev) | [`claude-mac-chrome`](https://github.com/yolo-labz/claude-mac-chrome) |\n|---|:---:|:---:|:---:|:---:|:---:|\n| First-party trusted-event injection (`isTrusted=true`) | yes (MV3 + relay) | no (CDP — synthetic events) | no (CDP — synthetic events) | no (WebDriver — synthetic events) | no (AppleScript — OS-level) |\n| Cross-platform (Linux + macOS) | yes | yes | yes | yes | macOS only |\n| Pure stdlib Python CLI | yes | no (npm) | no (npm) | no (java/python+selenium) | n/a (Bash) |\n| SLSA L2 + dual SBOM (CycloneDX + SPDX) + cosign | yes | depends on consumer | depends on consumer | depends on consumer | SLSA L3 |\n| MV3 extension (vanilla JS, no React/build step) | yes | n/a | n/a | n/a | n/a |\n| Chrome 137+ `DisableLoadExtensionCommandLineSwitch` handling | documented (brand-guard launcher) | not handled | not handled | not handled | n/a |\n| Daily Chrome untouched (isolated `--user-data-dir`) | yes (Profile-Auto) | depends on caller | depends on caller | depends on caller | depends on profile |\n| STEALTH_DOMAINS hard-block in extension source | yes (defense in depth) | n/a | n/a | n/a | manual |\n\nFor headless scraping at scale, prefer `playwright` or `puppeteer`; for stealth-critical sites where the daily Chrome profile must drive interactively, prefer `claude-mac-chrome` on macOS. `chrome-bridge` fills the cross-platform, trusted-event, Python-CLI niche the other tools do not.\n\n## Architecture (one-page)\n\n```\n┌─────────────────────┐                      ┌─────────────────────────┐\n│  Sibling plugin /   │   shell out          │  cli/cb (Python stdlib) │\n│  Claude Code task   │ ────────────────────▶│  127.0.0.1:9224 relay   │\n└─────────────────────┘                      └─────────────────────────┘\n                                                       │\n                                                       │ long-poll JSON\n                                                       ▼\n                                  ┌──────────────────────────────────────────┐\n                                  │  Profile-Auto Chrome (CfT 145)           │\n                                  │  ─────────────────────────────────       │\n                                  │  ext/service-worker.js  ◀── polls relay  │\n                                  │  ext/main-world-capture.js (capture)     │\n                                  │  chrome.cookies / scripting / debugger   │\n                                  └──────────────────────────────────────────┘\n                                                       │\n                                                       │ trusted events / Bearer auth\n                                                       ▼\n                                  ┌──────────────────────────────────────────┐\n                                  │  Target site (Upwork / blog / dokku)     │\n                                  └──────────────────────────────────────────┘\n```\n\n**Daily Chrome** — Pedro's main browsing profile — is **not touched**. Profile-Auto runs in a separate `--user-data-dir` and is only launched when automation work is pending.\n\n## Why an unbranded Chromium\n\nChrome 137+ stable / beta / dev / canary all hard-block `--load-extension` regardless of `--disable-features` flags (the `DisableLoadExtensionCommandLineSwitch` policy is now compiled in, not toggleable). Only **unbranded** Chromium builds — open-source Chromium proper, or Chrome for Testing (CfT) — still honour `--load-extension`.\n\nThe launcher (`launch/profile-auto.sh`) probes a per-OS candidate chain and brand-guards each candidate by parsing `--version` output, refusing anything that identifies as `Google Chrome` (without `for Testing`).\n\n### macOS install\n\nTwo supported paths, first match wins:\n\n**Option 1 — Homebrew cask (recommended, smallest install):**\n\n```sh\nbrew install --cask chromium\n# Clear Gatekeeper quarantine on first install (ad-hoc signed):\nxattr -dr com.apple.quarantine /Applications/Chromium.app\n```\n\n**Option 2 — Chrome for Testing via Patchright (heavier, but Microsoft+Google co-signed, no quarantine surgery):**\n\n```sh\nuv tool install patchright\nuvx --from patchright patchright install chromium\n```\n\nThe launcher resolves candidates in this order: `/Applications/Chromium.app` → `~/Library/Caches/ms-playwright/chromium-1208/...` (CfT) → `/Applications/Google Chrome for Testing.app` → glob-fallback for any `chromium-1NNN/` CfT version drift under `~/Library/Caches/ms-playwright/`.\n\nOverride with `CB_CHROME_BIN=/path/to/binary ./launch/profile-auto.sh`.\n\n\u003e macOS launcher changes in this section have not been smoke-tested on macOS in this session — desktop is x86_64-linux. Verify on macbook-pro before relying on the candidate chain. Linux path is smoke-tested.\n\n### Linux install\n\nUse the Nixpkgs `chromium` derivation (open-source Chromium, brand string `Chromium`, honours `--load-extension`):\n\n```sh\n# NixOS / nix-darwin: add to systemPackages or home-manager packages\nenvironment.systemPackages = [ pkgs.chromium ];\n```\n\nOr any distro-packaged `chromium` binary on `$PATH`. Google-branded `google-chrome` is rejected by the launcher's brand-guard.\n\n## Three-tier surface taxonomy\n\n| Tier | Example | Mechanism via chrome-bridge |\n|------|---------|------------------------------|\n| **A — stealth-critical** (LinkedIn primary, banking, G\u0026P-visible) | `cliclick` + clipboard-handoff. **Never** `chrome.debugger`, **never** automation. STEALTH_DOMAINS list in `service-worker.js` refuses jobs targeting these even if relay sent them. |\n| **B — semi-stealth** (Upwork primary, Calendly admin, Twitter secondary) | `cb gql` direct GraphQL via `chrome.cookies` Bearer first; `cb debug-click` second when surface insists on UI. Behavioural jitter from `cb debug-type`. |\n| **C — Pedro-owned infra** (sonarqube / dokku / infisical / ProxMox) | Direct REST/SSH first (no chrome-bridge needed). `cb` only when admin auth lives in a browser cookie. |\n| **D — escape hatch** (public scraping, no account at risk) | Patchright + residential proxy in a separate repo. Not a `chrome-bridge` consumer. |\n\n## Quick start\n\n```sh\n# 1. Launch Profile-Auto Chrome with the extension loaded.\n#    Auto-starts the cb relay daemon on 127.0.0.1:9224.\n./launch/profile-auto.sh --bg\n\n# 2. Health check.\n./cli/cb ping\n\n# 3. Cookies.\n./cli/cb cookies https://www.upwork.com/\n\n# 4. Direct GraphQL with Bearer extracted from a cookie.\n./cli/cb gql https://www.upwork.com/api/graphql/v1 \\\n  --alias findSkills \\\n  --query \"query searchSkillsByPrefLabel(\\$query: String!, \\$type: OntologyEntityType!, \\$status: OntologyEntityStatus!, \\$ordering: String!, \\$limit: Int!) { ontologyElementsSearchByPrefLabel(filter: {preferredLabel_any: \\$query, type: \\$type, entityStatus_eq: \\$status, sortOrder: \\$ordering, limit: \\$limit}) { id preferredLabel } }\" \\\n  --vars '{\"query\":\"AI Agent Development\",\"type\":\"SKILL\",\"status\":\"ACTIVE\",\"ordering\":\"match-start\",\"limit\":5}' \\\n  --bearer-cookie profile_vv_gql_token\n\n# 5. Capture XHRs in MAIN-world.\n./cli/cb capture-install \u003ctabId\u003e --pattern \"graphql\"\n# ... user clicks something ...\n./cli/cb capture-drain \u003ctabId\u003e\n\n# 6. Trusted click via chrome.debugger.Input.\n./cli/cb debug-click \u003ctabId\u003e 659 402\n\n# 7. Trusted typing with humanesque jitter.\n./cli/cb debug-type \u003ctabId\u003e \"hello world\"\n```\n\n## Verb reference\n\n| `cb` verb | What it does | Behind the scenes |\n|-----------|--------------|--------------------|\n| `cb daemon` | start the localhost relay | `http.server` on 127.0.0.1:9224 |\n| `cb ping` | health-check the bridge | sends `kind: \"ping\"` |\n| `cb cookies \u003curl\u003e` | list cookies for url | `chrome.cookies.getAll` |\n| `cb cookies-bearer \u003curl\u003e \u003cname\u003e` | print just one cookie's value | for shelling into `--bearer` |\n| `cb gql \u003curl\u003e --query ... --vars ... [--bearer / --bearer-cookie]` | POST GraphQL with Bearer auth | service-worker `fetch` with `credentials: \"include\"` |\n| `cb tabs` | list open tabs | `chrome.tabs.query` |\n| `cb tab-update \u003ctabId\u003e --url ... --active true` | navigate a tab | `chrome.tabs.update` |\n| `cb eval \u003ctabId\u003e \u003ccode\u003e [--world MAIN/ISOLATED]` | run JS in tab, get result | `chrome.scripting.executeScript` |\n| `cb capture-install \u003ctabId\u003e [--pattern STR_OR_re:REGEX]` | install MAIN-world fetch+XHR interceptor | injects `main-world-capture.js` |\n| `cb capture-drain \u003ctabId\u003e` | get + clear captured requests | reads `window.__cbCapturedRequests` |\n| `cb debug-click \u003ctabId\u003e \u003cx\u003e \u003cy\u003e` | trusted click at viewport coords | `chrome.debugger.attach` + `Input.dispatchMouseEvent` |\n| `cb debug-type \u003ctabId\u003e \u003ctext\u003e` | trusted typing with 30-110ms jitter | `chrome.debugger.attach` + `Input.insertText` |\n\n## STEALTH_DOMAINS guard (defense in depth)\n\n`extension/service-worker.js` hard-blocks any job whose URL hostname matches the `STEALTH_DOMAINS` set. Currently `linkedin.com` is hard-coded. Even if the relay sent a job to `cb gql --url https://www.linkedin.com/...`, the service worker returns `{ok:false, error:\"stealth-blocked\"}` before the fetch ever leaves the extension. This is intentional belt-and-suspenders for Pedro's principle IX — adding a Tier-A site to the bridge requires editing the extension source, never just the relay.\n\n## What's been validated\n\n- Extension loads cleanly into CfT 145, SW polls relay every 0.4s\n- `cb cookies` / `cb gql` / `cb scripting.eval` / `cb capture.install` / `cb debugger.click` all round-trip end-to-end against Pedro's logged-in Upwork session\n- 20 Upwork skill ontology UIDs resolved via `findSkills` (5 not-in-ontology gaps documented)\n- 26 Upwork mutations identified by JS chunk grepping; canonical bodies captured for `updateTalentProfileTitle`, `updateTalentProfileDescription`, `updateTalentProfileHourlyRate`, `updateTalentProfileSkills`, `updateShowProjectOnProfile`, `updateTalentLanguageRecords`, `addPortfolioProject` (partial — relies on imported fragments)\n- `updateTalentProfileTitle` mutation verified working (idempotent test returned `status:true` + persisted state matched)\n- **Found:** `updateTalentProfileSkills` is a silent-no-op for Pedro's profile pre-28/05/2026 (Upwork specialized-profiles deprecation transition). See `captures/upwork-skills-pipeline.md`.\n\n## Future phases\n\n- **P2** — capture portfolio + catalog mutations (currently rely on imported fragments — need to resolve the webpack chunk that defines `n.g`, `n.a`, `n.i` for `createTalentPortfolio`)\n- **P3** — Haiku 4.5 LLM-fallback resolver — `cb resolve --ax-tree` reads accessibility tree, asks Haiku \"which element is the Save button?\", returns coords. Gated by `--budget-usd 0.10` per call.\n- **P4** — Profile management — manifest.lock pattern matching `wa` plugin (SessionStart hook diffs bundled vs installed extension, reinstalls on drift)\n- **P5** — generalize to Instagram + future sites (per the synthesis doc roadmap)\n\n## Supply chain \u0026 verification\n\nEvery tagged release ships with:\n\n- `chrome-bridge-\u003ctag\u003e.tar.gz` — deterministic source archive (`git archive`)\n- `chrome-bridge-\u003ctag\u003e.tar.gz.sha256` — SHA-256 manifest\n- `sbom.cdx.json` — CycloneDX 1.7 SBOM\n- `sbom.spdx.json` — SPDX 2.3 SBOM\n- A single SLSA v1 build provenance attestation (signed via Sigstore, logged to Rekor) covering all three artifacts as subjects\n\nThe build runs on a GitHub-hosted runner from a tag-triggered workflow at `.github/workflows/release.yml`, with every action pinned by 40-char commit SHA. Provenance `builder.id` resolves to `…/release.yml@refs/tags/\u003ctag\u003e`, `runnerEnvironment=github-hosted`, `sourceRepositoryVisibilityAtSigning=public`.\n\n### Verify a release\n\n```sh\nTAG=v0.1.0\nmkdir -p /tmp/cb-verify \u0026\u0026 cd /tmp/cb-verify\ngh release download \"$TAG\" --repo yolo-labz/chrome-bridge\n\n# 1. Hash check.\nsha256sum -c \"chrome-bridge-${TAG}.tar.gz.sha256\"\n\n# 2. Attestation check (all three artifacts share one Rekor entry).\nfor f in chrome-bridge-${TAG}.tar.gz sbom.cdx.json sbom.spdx.json; do\n  gh attestation verify \"$f\" --repo yolo-labz/chrome-bridge \\\n    \u0026\u0026 echo \"  ✓ $f\"\ndone\n```\n\n`gh attestation verify` exits 0 silently on success. Add `--format json` to inspect the full DSSE envelope, Rekor `logIndex`, and `sourceRepositoryDigest`.\n\n### Pin from a NixOS / nix-darwin derivation\n\nDownstream consumers (e.g. `claude-mac-chrome`, `wa`, the home-manager fleet) should pin chrome-bridge by `git rev` + `sha256` of the release tarball, then **verify the attestation at build time**. Minimal pattern:\n\n```nix\n{ pkgs, ... }:\nlet\n  tag = \"v0.1.0\";\n  src = pkgs.fetchurl {\n    url = \"https://github.com/yolo-labz/chrome-bridge/releases/download/${tag}/chrome-bridge-${tag}.tar.gz\";\n    # sha256 from the published .sha256 manifest:\n    sha256 = \"6f8cd6fe2e171bee29db641a0dd831dc699b4777923d1d8641d78f8bc2ac2fd8\";  # v0.1.0 — verify with `nix-prefetch-url \u003curl\u003e`\n  };\nin pkgs.stdenvNoCC.mkDerivation {\n  pname = \"chrome-bridge\";\n  version = tag;\n  inherit src;\n  sourceRoot = \"chrome-bridge-${tag}\";\n\n  # Optional: gate the build on attestation verification.\n  # Requires `gh` + network; only viable for impure --option sandbox relaxed builds\n  # or as a CI-side check that runs before pushing the resulting store path.\n  # buildInputs = [ pkgs.gh ];\n  # preConfigure = ''\n  #   gh attestation verify $src --repo yolo-labz/chrome-bridge --denylist-org=\"\"\n  # '';\n\n  installPhase = ''\n    mkdir -p $out/{libexec,bin}\n    cp -r cli extension launch scripts $out/libexec/\n    install -Dm755 cli/cb $out/bin/cb\n    install -Dm755 launch/profile-auto.sh $out/bin/cb-launch-profile-auto\n  '';\n\n  meta = with pkgs.lib; {\n    description = \"Trusted-event Chrome automation bridge\";\n    homepage = \"https://github.com/yolo-labz/chrome-bridge\";\n    license = licenses.mit;\n    platforms = platforms.unix;\n  };\n}\n```\n\nFor a flake input, prefer `inputs.chrome-bridge.url = \"github:yolo-labz/chrome-bridge/v0.1.0\"` (immutable tag ref) — the flake lock will pin the exact commit `narHash`, and you can still run `gh attestation verify` against a separately-fetched release tarball as a parallel CI gate.\n\n## License\n\nMIT. See `LICENSE`.\n\n---\n\n## Services\n\nCompliance-grade AI architecture for regulated workloads — async-first, USD-denominated, LATAM-based / EN-fluent. See [blog.home301server.com.br/services](https://blog.home301server.com.br/services/).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyolo-labz%2Fchrome-bridge","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyolo-labz%2Fchrome-bridge","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyolo-labz%2Fchrome-bridge/lists"}