') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); @clerk/chrome-extension bundles remote UI script loader, rejected by Chrome Web Store under Manifest V3 · Issue #8770 · clerk/javascript · GitHub
Skip to content

@clerk/chrome-extension bundles remote UI script loader, rejected by Chrome Web Store under Manifest V3 #8770

Description

@devlarabar

Preliminary Checks

Reproduction

n/a

Publishable key

n/a

Description

Package: @clerk/chrome-extension@3.1.32

@clerk/chrome-extension ships a runtime CDN loader that injects @clerk/ui's ui.browser.js from Clerk's servers via a dynamically created <script> tag.

The Chrome Web Store now rejects Manifest V3 extensions that merely contain this code, regardless of whether it ever runs. The extension was rejected for it on June 5 2026 despite the loader never executing (the extension renders no Clerk UI components); I created a plugin that stripped this code at build, re-submitted, and the extension was flagged again on June 6 2026, this time for another piece of Clerk's code.

This is remotely hosted code by Google's definition, and we (extension creators) have no supported way to remove it from the bundle, except for digging for it and stripping it ourselves. Seems as though even URLs flagged as "RHC" in the source maps will trigger Web Store rejection, too.

I believe the offending code lives in @clerk/shared:

Can we get a no-RHC variant of this for the chrome extension? There's already a @clerk/ui/no-rhc, makes sense to drop the remote loader that contradicts it.

Chrome store rejections:

Violation reference ID: Blue Argon
Technical Requirements - Additional Requirements for Manifest V3:
Violation: Including remotely hosted code in a Manifest V3 item.
Violating Content:
Code snippet: assets/App-DOB_oaUF.js: "const w = document.createElement("script"); l && w.setAttribute("crossorigin", l), w.async = r || !1, w.defer = n || !1, w.addEventListener("load", () => { w.remove(), b(w) }), w.addEventListener("error", v => { var S; w.remove(), k((S = v.error) != null ? S : new Error(`failed to load script: ${e}`)) }), w.src = e, w.nonce = u, s == null || s(w), document.body.appendChild(w) })"
Violation reference ID: Blue Argon
Technical Requirements - Additional Requirements for Manifest V3:
Violation: Including remotely hosted code in a Manifest V3 item.
Violating Content:
Code snippet: assets/App-Bl-jAqh8.js: return a || `https://${i_({publishableKey:l,proxyUrl:n,domain:s})}/npm/@clerk/ui@${Z9(r,"1.3.0")}/dist/ui.browser.js`

Potential solution

Adding a build-time gate to loadClerkUIScript should fix the problem (it currently has a runtime-only gate); this is what's already done for loadClerkJSScript.

Other issues related to RHC

I've found some other updates that fix RHC issues (4899, 4886, 7983, 6754, 6179) but not one for this.

Steps to reproduce:

  1. Create a new Chrome extension using Manifest V3
  2. Install and implement @clerk/chrome-extension
  3. Build and submit to Chrome Web Store
  4. Receive rejection due to remote code loading

Expected behavior:

The Clerk authentication should work in Chrome extension without loading remote code.

Actual behavior:

The bundled code contains references to remote URLs, which violates Chrome's MV3 policies.
These URLs appear to be coming from the bundled @clerk/ui package.

Environment

System:
OS: macOS 26.5
CPU: (12) arm64 Apple M3 Pro
Memory: 159.45 MB / 18.00 GB
Shell: 5.9 - /bin/zsh
Binaries:
Node: 24.13.0
Yarn: 1.22.22
npm: 11.6.2
pnpm: 10.33.0
bun: 1.3.14
Watchman: 2026.03.09.00
Browsers:
Chrome: 148.0.7778.216
Firefox: 147.0.4
Safari: 26.5
npmPackages:
@cipherstash/protect: 10.2.1 => 10.2.1 @commitlint/cli: ^19.3.0 => 19.8.1 @commitlint/config-conventional: ^19.2.2 => 19.8.1 @octokit/rest: ^22.0.1 => 22.0.1 @pulumi/pulumi: ^3.150.0 => 3.167.0 @sanity/eslint-plugin-i18n: ^1.1.0 => 1.1.0 @semantic-release/commit-analyzer: ^13.0.1 => 13.0.1 @semantic-release/exec: ^7.1.0 => 7.1.0 @semantic-release/github: ^12.0.2 => 12.0.2 @semantic-release/npm: ^12.0.1 => 12.0.2 @semantic-release/release-notes-generator: ^14.1.0 => 14.1.0 @typescript/native-preview: 7.0.0-dev.20260122.4 => 7.0.0-dev.20260122.4 @vitest/coverage-v8: ^2.1.9 => 2.1.9 concurrently: ^9.1.0 => 9.1.2 dependency-cruiser: ^17.3.8 => 17.3.9 dotenv-cli: ^11.0.0 => 11.0.0 esbuild: 0.19.2 => 0.19.2 expo-font: ^14.0.9 => 14.0.9 husky: ^9.1.7 => 9.1.7 intl-pluralrules: ^1.3.1 => 1.3.1 lint-staged: ^16.1.2 => 16.1.2 oxfmt: ^0.37.0 => 0.37.0 oxlint: ^1.52.0 => 1.56.0 semantic-release: ^25.0.2 => 25.0.2 slackify-markdown: ^5.0.0 => 5.0.0 sst: ^3.19.3 => 3.19.3 turbo: ^2.5.6 => 2.5.6 typescript: 5.9.3 => 5.9.3 zod: ^4.1.12 => 4.1.12

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageA ticket that needs to be triaged by a team member

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions