← ClerkCONTENT HISTORY

Update to Clerk

Snapshot Sep 30, 2026 · 23:09 UTC · version 0.1.0

Collection source: not recorded for this historical snapshot.

WHAT CHANGED · RULE-BASED ANALYSIS

First saved snapshot

No earlier snapshot is available to establish a change.

Compare saved observations

Download comparison JSON
Full technical diff · 0 changed fields
Full snapshot data
{
  "name": "clerk-chrome-extension-patterns",
  "description": "Chrome Extension auth with @clerk/chrome-extension -- popup/sidepanel setup, syncHost for OAuth/SAML via web app, createClerkClient for service workers and headless extensions, stable CRX ID. Triggers on: Chrome extension auth, Plasmo clerk, popup sign-in, syncHost, background service worker token, createClerkClient, headless extension.",
  "included_files": [
    {
      "relative_path": "evals/evals.json",
      "size_in_bytes": 4935
    },
    {
      "relative_path": "references/content-scripts.md",
      "size_in_bytes": 3204
    },
    {
      "relative_path": "references/create-clerk-client.md",
      "size_in_bytes": 3992
    },
    {
      "relative_path": "references/headless-extension.md",
      "size_in_bytes": 3811
    },
    {
      "relative_path": "references/sync-host.md",
      "size_in_bytes": 4223
    },
    {
      "relative_path": "templates/chrome-ext-basic-auth/package.json",
      "size_in_bytes": 450
    },
    {
      "relative_path": "templates/chrome-ext-basic-auth/src/popup.tsx",
      "size_in_bytes": 1138
    },
    {
      "relative_path": "templates/chrome-ext-basic-auth/tsconfig.json",
      "size_in_bytes": 163
    }
  ],
  "skill_md_contents": "---\nname: clerk-chrome-extension-patterns\ndescription: 'Chrome Extension auth with @clerk/chrome-extension -- popup/sidepanel\n  setup, syncHost for OAuth/SAML via web app, createClerkClient for service workers\n  and headless extensions, stable CRX ID. Triggers on: Chrome extension auth, Plasmo\n  clerk, popup sign-in, syncHost, background service worker token, createClerkClient,\n  headless extension.'\nlicense: MIT\nallowed-tools: WebFetch\ncompatibility: Requires PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY (Plasmo prefix for public env vars) and CLERK_FRONTEND_API.\nmetadata:\n  author: clerk\n  version: 2.0.0\n  references:\n  - references/sync-host.md\n  - references/create-clerk-client.md\n  - references/content-scripts.md\n  - references/headless-extension.md\n---\n\n# Chrome Extension Patterns\n\n## CRITICAL RULES\n\n1. OAuth (Google, GitHub, etc.) and SAML are NOT supported in popups or side panels -- use `syncHost` to delegate auth to your web app\n2. Email links (magic links) don't work in popups -- the popup closes when the user clicks outside, resetting sign-in state\n3. Side panels don't auto-refresh auth state -- users must close and reopen the side panel after signing in via the web app\n4. Service workers and content scripts have NO access to Clerk React hooks -- use `createClerkClient()` or message passing\n5. Extension URLs use `chrome-extension://` not `http://` -- all redirect URLs must use `chrome.runtime.getURL('.')`\n6. Without a stable CRX ID, every rebuild breaks auth -- configure `key` in manifest BEFORE deploying\n7. Content scripts cannot use Clerk directly due to origin restrictions -- Clerk enforces strict allowed origins\n8. Bot protection must be DISABLED in Clerk Dashboard -- Cloudflare bot detection is not supported in extension environments\n\n## Authentication Options\n\n| Method | Popup | Side Panel | syncHost (with web app) |\n|--------|-------|------------|------------------------|\n| Email + OTP | Yes | Yes | Yes |\n| Email + Link | No | No | Yes |\n| Email + Password | Yes | Yes | Yes |\n| Username + Password | Yes | Yes | Yes |\n| SMS + OTP | Yes | Yes | Yes |\n| OAuth (Google, GitHub, etc.) | **NO** | **NO** | **YES** |\n| SAML | **NO** | **NO** | **YES** |\n| Passkeys | Yes | Yes | Yes |\n| Google One Tap | No | No | Yes |\n| Web3 | No | No | Yes |\n\n## Quick Start (Plasmo)\n\n```bash\nnpx create-plasmo --with-tailwindcss --with-src my-extension\ncd my-extension\nnpm install @clerk/chrome-extension\n```\n\nEnable **Native API** in Clerk Dashboard under Native applications. Required for all extension integrations.\n\n`.env.development`:\n```\nPLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...\nCLERK_FRONTEND_API=https://your-app.clerk.accounts.dev\n```\n\n`src/popup.tsx`:\n```tsx\nimport { ClerkProvider, Show, SignInButton, SignUpButton, UserButton } from '@clerk/chrome-extension'\n\nconst PUBLISHABLE_KEY = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY\nconst EXTENSION_URL = chrome.runtime.getURL('.')\n\nif (!PUBLISHABLE_KEY) {\n  throw new Error('Missing PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY')\n}\n\nfunction IndexPopup() {\n  return (\n    <ClerkProvider\n      publishableKey={PUBLISHABLE_KEY}\n      afterSignOutUrl={`${EXTENSION_URL}/popup.html`}\n      signInFallbackRedirectUrl={`${EXTENSION_URL}/popup.html`}\n      signUpFallbackRedirectUrl={`${EXTENSION_URL}/popup.html`}\n    >\n      <Show when=\"signed-out\">\n        <SignInButton mode=\"modal\" />\n        <SignUpButton mode=\"modal\" />\n      </Show>\n      <Show when=\"signed-in\">\n        <UserButton />\n      </Show>\n    </ClerkProvider>\n  )\n}\n\nexport default IndexPopup\n```\n\nUse `mode=\"modal\"` for `SignInButton` -- navigating to a separate page breaks the popup flow.\n\n## syncHost -- Sync Auth with Web App\n\nUse this when you need OAuth, SAML, or want the extension to reflect sign-in from your web app.\n\n**How it works**: The extension reads the Clerk session cookie from your web app's domain via `host_permissions`.\n\n**Step 1 -- Environment variables:**\n\n`.env.development`:\n```\nPLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...\nCLERK_FRONTEND_API=https://your-app.clerk.accounts.dev\nPLASMO_PUBLIC_CLERK_SYNC_HOST=http://localhost\n```\n\n`.env.production`:\n```\nPLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...\nCLERK_FRONTEND_API=https://clerk.your-domain.com\nPLASMO_PUBLIC_CLERK_SYNC_HOST=https://clerk.your-domain.com\n```\n\n**Step 2 -- Add `syncHost` prop:**\n\n```tsx\nconst SYNC_HOST = process.env.PLASMO_PUBLIC_CLERK_SYNC_HOST\n\n<ClerkProvider\n  publishableKey={PUBLISHABLE_KEY}\n  syncHost={SYNC_HOST}\n  afterSignOutUrl=\"/\"\n  routerPush={(to) => navigate(to)}\n  routerReplace={(to) => navigate(to, { replace: true })}\n>\n```\n\n**Step 3 -- Configure `host_permissions` in `package.json`:**\n\n```json\n{\n  \"manifest\": {\n    \"key\": \"$CRX_PUBLIC_KEY\",\n    \"permissions\": [\"cookies\", \"storage\"],\n    \"host_permissions\": [\n      \"$PLASMO_PUBLIC_CLERK_SYNC_HOST/*\",\n      \"$CLERK_FRONTEND_API/*\"\n    ]\n  }\n}\n```\n\n**Step 4 -- Add extension ID to web app's allowed origins via Clerk API:**\n\n```bash\ncurl -X PATCH https://api.clerk.com/v1/instance \\\n  -H \"Authorization: Bearer YOUR_SECRET_KEY\" \\\n  -H \"Content-type: application/json\" \\\n  -d '{\"allowed_origins\": [\"chrome-extension://YOUR_EXTENSION_ID\"]}'\n```\n\n**Hide unsupported auth methods in popup when using syncHost:**\n\n```tsx\n<SignIn\n  appearance={{\n    elements: {\n      socialButtonsRoot: 'plasmo-hidden',\n      dividerRow: 'plasmo-hidden',\n    },\n  }}\n/>\n```\n\nFull guide: `references/sync-host.md`\n\n## createClerkClient() for Vanilla JS / Service Workers\n\nImport from `@clerk/chrome-extension/client` (not `@clerk/chrome-extension`).\n\n**Background service worker** (`src/background/index.ts`):\n\n```typescript\nimport { createClerkClient } from '@clerk/chrome-extension/client'\n\nconst publishableKey = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY\n\nasync function getToken(): Promise<string | null> {\n  const clerk = await createClerkClient({\n    publishableKey,\n    background: true,\n  })\n  if (!clerk.session) return null\n  return await clerk.session.getToken()\n}\n\nchrome.runtime.onMessage.addListener((request, sender, sendResponse) => {\n  getToken()\n    .then((token) => sendResponse({ token }))\n    .catch((error) => {\n      console.error('[Background] Error:', JSON.stringify(error))\n      sendResponse({ token: null })\n    })\n  return true\n})\n```\n\nThe `background: true` flag keeps sessions fresh even when popup/sidepanel is closed. Without it, tokens expire after 60 seconds.\n\n**Popup with vanilla JS** (`src/popup.ts`):\n\n```typescript\nimport { createClerkClient } from '@clerk/chrome-extension/client'\n\nconst EXTENSION_URL = chrome.runtime.getURL('.')\nconst POPUP_URL = `${EXTENSION_URL}popup.html`\n\nconst clerk = createClerkClient({ publishableKey })\n\nclerk.load({\n  afterSignOutUrl: POPUP_URL,\n  signInForceRedirectUrl: POPUP_URL,\n  signUpForceRedirectUrl: POPUP_URL,\n  allowedRedirectProtocols: ['chrome-extension:'],\n}).then(() => {\n  clerk.addListener(render)\n  render()\n})\n```\n\nFull guide: `references/create-clerk-client.md`\n\n## Headless Extension (no popup, no side panel)\n\nFor extensions that run entirely in the background and sync with a web app.\n\nUses `syncHost` + `createClerkClient` with `background: true` to read auth state from the web app's cookies.\n\n```typescript\nimport { createClerkClient } from '@clerk/chrome-extension/client'\n\nconst publishableKey = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY\nconst syncHost = process.env.PLASMO_PUBLIC_CLERK_SYNC_HOST\n\nasync function getAuthenticatedUser() {\n  const clerk = await createClerkClient({\n    publishableKey,\n    syncHost,\n    background: true,\n  })\n  return clerk.user\n}\n```\n\nRequires `host_permissions` for the sync host domain in `package.json`.\n\nFull guide: `references/headless-extension.md`\n\n## Content Scripts\n\nContent scripts run in an isolated JavaScript world injected into web pages. **Clerk cannot be used directly** -- origin restrictions prevent it.\n\nUse message passing to request auth state from the background service worker:\n\n```typescript\n// content.ts\nasync function getToken(): Promise<string | null> {\n  return new Promise((resolve) => {\n    chrome.runtime.sendMessage({ type: 'GET_TOKEN' }, (response) => {\n      resolve(response?.token ?? null)\n    })\n  })\n}\n\nasync function main() {\n  const token = await getToken()\n  if (!token) return\n  // use token for authenticated API calls\n}\n\nmain()\n```\n\nFull guide: `references/content-scripts.md`\n\n## Stable CRX ID\n\nWithout a pinned key, Chrome derives the CRX ID from a random key at build time. This rotates every rebuild, breaking allowed origins.\n\n**Option A -- Plasmo Itero (recommended):**\n1. Visit [Plasmo Itero Generate Keypairs](https://itero.plasmo.com/ext/generate-keypairs)\n2. Click \"Generate KeyPairs\" -- save Private Key securely, copy Public Key and CRX ID\n\n**Option B -- OpenSSL:**\n```bash\nopenssl genrsa -out key.pem 2048\n# Use Plasmo Itero to convert or extract the public key in correct format\n```\n\n**`.env.chrome`:**\n```\nCRX_PUBLIC_KEY=\"<PUBLIC KEY from Itero>\"\n```\n\n**`package.json`:**\n```json\n{\n  \"manifest\": {\n    \"key\": \"$CRX_PUBLIC_KEY\",\n    \"permissions\": [\"cookies\", \"storage\"],\n    \"host_permissions\": [\n      \"http://localhost/*\",\n      \"$CLERK_FRONTEND_API/*\"\n    ]\n  }\n}\n```\n\nAdd `chrome-extension://YOUR_STABLE_CRX_ID` to Clerk Dashboard > Allowed Origins.\n\n## Token Cache (persist across popup closes)\n\n```tsx\nconst tokenCache = {\n  async getToken(key: string) {\n    const result = await chrome.storage.local.get(key)\n    return result[key] ?? null\n  },\n  async saveToken(key: string, token: string) {\n    await chrome.storage.local.set({ [key]: token })\n  },\n  async clearToken(key: string) {\n    await chrome.storage.local.remove(key)\n  },\n}\n\n<ClerkProvider publishableKey={PUBLISHABLE_KEY} tokenCache={tokenCache}>\n```\n\n| Storage type | Scope | Clears on |\n|---|---|---|\n| `chrome.storage.local` | Device | Uninstall or manual clear |\n| `chrome.storage.session` | Session | Browser close |\n| `chrome.storage.sync` | All devices | Uninstall (size-limited, 8KB) |\n| `localStorage` | Popup only | Popup close -- do not use for auth |\n\n## Common Pitfalls\n\n| Symptom | Cause | Fix |\n|---------|-------|-----|\n| Redirect loop on sign-in | Missing CRX URL in ClerkProvider props | Set `afterSignOutUrl`, `signInFallbackRedirectUrl` |\n| OAuth button not working | OAuth not supported in popup | Use `syncHost` to delegate to web app |\n| Auth state stale after web app sign-in | `syncHost` not configured | Add `syncHost` prop + `host_permissions` |\n| Side panel shows signed-out after web sign-in | Known limitation | User must close and reopen the side panel |\n| Background can't get token after 60s | Session expired, no background refresh | Use `createClerkClient({ background: true })` |\n| Content script can't access Clerk | Isolated world + origin restrictions | Use message passing to background service worker |\n| Auth breaks after rebuild | CRX ID rotated | Configure stable key via `.env.chrome` |\n| `PLASMO_PUBLIC_` var undefined | Wrong env file | Use `.env.development`, not `.env` |\n| Bot protection errors | Cloudflare not supported in extensions | Disable bot protection in Clerk Dashboard |\n| Token cache not persisting | Using `localStorage` in popup | Use `chrome.storage.local` or pass `tokenCache` prop |\n\n## Plan Requirements\n\n| Feature | Plan |\n|---------|------|\n| Basic popup auth (email/password, OTP) | Free |\n| Passkeys | Free |\n| syncHost | Requires Pro (custom domain) |\n| OAuth through syncHost | Pro + OAuth configured on web app |\n| SAML through syncHost | Enterprise |\n| Bot protection | N/A -- must be disabled for extensions |\n\n## See Also\n\n- `clerk-setup` - Initial Clerk install\n- `clerk-custom-ui` - Custom flows & appearance\n"
}

SHA-256: 7845062a90d5ef189177c942fe577d17880f4cc502a52ba887f9c409a9e9c95d