Clerk Chrome Extension Patterns
ClerkOfficialCreated by clerk
- Skill
- Tool integration
- Software development
- MIT
- Low risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This guide provides essential patterns for integrating Clerk authentication into Chrome extensions. It covers handling authentication in popups, side panels, background service workers, and headless environments. It addresses critical constraints such as origin restrictions, session management, and the necessity of stable CRX IDs for production deployments.
What it does
- Defines patterns for popup and side panel authentication flows.
- Explains how to sync authentication state between a web app and an extension using syncHost.
- Provides methods for using Clerk in background service workers and vanilla JS environments.
- Outlines secure message-passing techniques for content scripts.
- Details configuration requirements for stable extension IDs and manifest permissions.
- Lists supported and unsupported authentication methods for different extension contexts.
When to use it
- When building a Chrome extension that requires user authentication.
- When you need to share authentication state between your web application and your extension.
- When implementing authentication logic within background service workers or content scripts.
- When troubleshooting common extension auth issues like redirect loops or stale sessions.
Advantages
- Ensures secure and reliable authentication across different extension contexts.
- Prevents common pitfalls like session expiration and origin-based access blocks.
- Provides clear guidance on handling complex flows like OAuth and SAML in extensions.
- Offers standardized patterns for Plasmo-based development.
Usage examples
- “How do I keep my extension's session active in the background?”
- Use createClerkClient with the background: true flag to ensure the session token is refreshed continuously even when the UI is closed.
- “Can I use OAuth in my Chrome extension popup?”
- No, OAuth is not supported in popups. You must use syncHost to delegate the authentication process to your web application.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/clerk/clerk-chrome-extension-patterns/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip clerk-chrome-extension-patterns.zip -d .agents/skills
Documentation by clerk
Chrome Extension Patterns
CRITICAL RULES
OAuth (Google, GitHub, etc.) and SAML are NOT supported in popups or side panels -- use
syncHostto delegate auth to your web appEmail links (magic links) don't work in popups -- the popup closes when the user clicks outside, resetting sign-in state
Side panels don't auto-refresh auth state -- users must close and reopen the side panel after signing in via the web app
Service workers and content scripts have NO access to Clerk React hooks -- use
createClerkClient()or message passingExtension URLs use
chrome-extension://nothttp://-- all redirect URLs must usechrome.runtime.getURL('.')Without a stable CRX ID, every rebuild breaks auth -- configure
keyin manifest BEFORE deployingContent scripts cannot use Clerk directly due to origin restrictions -- Clerk enforces strict allowed origins
Bot protection must be DISABLED in Clerk Dashboard -- Cloudflare bot detection is not supported in extension environments
Authentication Options
| Method | Popup | Side Panel | syncHost (with web app) |
|---|---|---|---|
| Email + OTP | Yes | Yes | Yes |
| Email + Link | No | No | Yes |
| Email + Password | Yes | Yes | Yes |
| Username + Password | Yes | Yes | Yes |
| SMS + OTP | Yes | Yes | Yes |
| OAuth (Google, GitHub, etc.) | NO | NO | YES |
| SAML | NO | NO | YES |
| Passkeys | Yes | Yes | Yes |
| Google One Tap | No | No | Yes |
| Web3 | No | No | Yes |
Quick Start (Plasmo)
npx create-plasmo --with-tailwindcss --with-src my-extension
cd my-extension
npm install @clerk/chrome-extensionEnable Native API in Clerk Dashboard under Native applications. Required for all extension integrations.
.env.development:
PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_FRONTEND_API=https://your-app.clerk.accounts.devsrc/popup.tsx:
import { ClerkProvider, Show, SignInButton, SignUpButton, UserButton } from '@clerk/chrome-extension'
const PUBLISHABLE_KEY = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY
const EXTENSION_URL = chrome.runtime.getURL('.')
if (!PUBLISHABLE_KEY) {
throw new Error('Missing PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY')
}
function IndexPopup() {
return (
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
afterSignOutUrl={`${EXTENSION_URL}/popup.html`}
signInFallbackRedirectUrl={`${EXTENSION_URL}/popup.html`}
signUpFallbackRedirectUrl={`${EXTENSION_URL}/popup.html`}
>
<Show when="signed-out">
<SignInButton mode="modal" />
<SignUpButton mode="modal" />
</Show>
<Show when="signed-in">
<UserButton />
</Show>
</ClerkProvider>
)
}
export default IndexPopupUse mode="modal" for SignInButton -- navigating to a separate page breaks the popup flow.
syncHost -- Sync Auth with Web App
Use this when you need OAuth, SAML, or want the extension to reflect sign-in from your web app.
How it works: The extension reads the Clerk session cookie from your web app's domain via host_permissions.
Step 1 -- Environment variables:
.env.development:
PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_FRONTEND_API=https://your-app.clerk.accounts.dev
PLASMO_PUBLIC_CLERK_SYNC_HOST=http://localhost.env.production:
PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_FRONTEND_API=https://clerk.your-domain.com
PLASMO_PUBLIC_CLERK_SYNC_HOST=https://clerk.your-domain.comStep 2 -- Add syncHost prop:
const SYNC_HOST = process.env.PLASMO_PUBLIC_CLERK_SYNC_HOST
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
syncHost={SYNC_HOST}
afterSignOutUrl="/"
routerPush={(to) => navigate(to)}
routerReplace={(to) => navigate(to, { replace: true })}
>Step 3 -- Configure host_permissions in package.json:
{
"manifest": {
"key": "$CRX_PUBLIC_KEY",
"permissions": ["cookies", "storage"],
"host_permissions": [
"$PLASMO_PUBLIC_CLERK_SYNC_HOST/*",
"$CLERK_FRONTEND_API/*"
]
}
}Step 4 -- Add extension ID to web app's allowed origins via Clerk API:
curl -X PATCH https://api.clerk.com/v1/instance \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-type: application/json" \
-d '{"allowed_origins": ["chrome-extension://YOUR_EXTENSION_ID"]}'Hide unsupported auth methods in popup when using syncHost:
<SignIn
appearance={{
elements: {
socialButtonsRoot: 'plasmo-hidden',
dividerRow: 'plasmo-hidden',
},
}}
/>Full guide: references/sync-host.md
createClerkClient() for Vanilla JS / Service Workers
Import from @clerk/chrome-extension/client (not @clerk/chrome-extension).
Background service worker (src/background/index.ts):
import { createClerkClient } from '@clerk/chrome-extension/client'
const publishableKey = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY
async function getToken(): Promise<string | null> {
const clerk = await createClerkClient({
publishableKey,
background: true,
})
if (!clerk.session) return null
return await clerk.session.getToken()
}
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
getToken()
.then((token) => sendResponse({ token }))
.catch((error) => {
console.error('[Background] Error:', JSON.stringify(error))
sendResponse({ token: null })
})
return true
})The background: true flag keeps sessions fresh even when popup/sidepanel is closed. Without it, tokens expire after 60 seconds.
Popup with vanilla JS (src/popup.ts):
import { createClerkClient } from '@clerk/chrome-extension/client'
const EXTENSION_URL = chrome.runtime.getURL('.')
const POPUP_URL = `${EXTENSION_URL}popup.html`
const clerk = createClerkClient({ publishableKey })
clerk.load({
afterSignOutUrl: POPUP_URL,
signInForceRedirectUrl: POPUP_URL,
signUpForceRedirectUrl: POPUP_URL,
allowedRedirectProtocols: ['chrome-extension:'],
}).then(() => {
clerk.addListener(render)
render()
})Full guide: references/create-clerk-client.md
Headless Extension (no popup, no side panel)
For extensions that run entirely in the background and sync with a web app.
Uses syncHost + createClerkClient with background: true to read auth state from the web app's cookies.
import { createClerkClient } from '@clerk/chrome-extension/client'
const publishableKey = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY
const syncHost = process.env.PLASMO_PUBLIC_CLERK_SYNC_HOST
async function getAuthenticatedUser() {
const clerk = await createClerkClient({
publishableKey,
syncHost,
background: true,
})
return clerk.user
}Requires host_permissions for the sync host domain in package.json.
Full guide: references/headless-extension.md
Content Scripts
Content scripts run in an isolated JavaScript world injected into web pages. Clerk cannot be used directly -- origin restrictions prevent it.
Use message passing to request auth state from the background service worker:
// content.ts
async function getToken(): Promise<string | null> {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ type: 'GET_TOKEN' }, (response) => {
resolve(response?.token ?? null)
})
})
}
async function main() {
const token = await getToken()
if (!token) return
// use token for authenticated API calls
}
main()Full guide: references/content-scripts.md
Stable CRX ID
Without a pinned key, Chrome derives the CRX ID from a random key at build time. This rotates every rebuild, breaking allowed origins.
Option A -- Plasmo Itero (recommended):
Click "Generate KeyPairs" -- save Private Key securely, copy Public Key and CRX ID
Option B -- OpenSSL:
openssl genrsa -out key.pem 2048
# Use Plasmo Itero to convert or extract the public key in correct format.env.chrome:
CRX_PUBLIC_KEY="<PUBLIC KEY from Itero>"package.json:
{
"manifest": {
"key": "$CRX_PUBLIC_KEY",
"permissions": ["cookies", "storage"],
"host_permissions": [
"http://localhost/*",
"$CLERK_FRONTEND_API/*"
]
}
}Add chrome-extension://YOUR_STABLE_CRX_ID to Clerk Dashboard > Allowed Origins.
Token Cache (persist across popup closes)
const tokenCache = {
async getToken(key: string) {
const result = await chrome.storage.local.get(key)
return result[key] ?? null
},
async saveToken(key: string, token: string) {
await chrome.storage.local.set({ [key]: token })
},
async clearToken(key: string) {
await chrome.storage.local.remove(key)
},
}
<ClerkProvider publishableKey={PUBLISHABLE_KEY} tokenCache={tokenCache}>| Storage type | Scope | Clears on |
|---|---|---|
chrome.storage.local | Device | Uninstall or manual clear |
chrome.storage.session | Session | Browser close |
chrome.storage.sync | All devices | Uninstall (size-limited, 8KB) |
localStorage | Popup only | Popup close -- do not use for auth |
Common Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Redirect loop on sign-in | Missing CRX URL in ClerkProvider props | Set afterSignOutUrl, signInFallbackRedirectUrl |
| OAuth button not working | OAuth not supported in popup | Use syncHost to delegate to web app |
| Auth state stale after web app sign-in | syncHost not configured | Add syncHost prop + host_permissions |
| Side panel shows signed-out after web sign-in | Known limitation | User must close and reopen the side panel |
| Background can't get token after 60s | Session expired, no background refresh | Use createClerkClient({ background: true }) |
| Content script can't access Clerk | Isolated world + origin restrictions | Use message passing to background service worker |
| Auth breaks after rebuild | CRX ID rotated | Configure stable key via .env.chrome |
PLASMO_PUBLIC_ var undefined | Wrong env file | Use .env.development, not .env |
| Bot protection errors | Cloudflare not supported in extensions | Disable bot protection in Clerk Dashboard |
| Token cache not persisting | Using localStorage in popup | Use chrome.storage.local or pass tokenCache prop |
Plan Requirements
| Feature | Plan |
|---|---|
| Basic popup auth (email/password, OTP) | Free |
| Passkeys | Free |
| syncHost | Requires Pro (custom domain) |
| OAuth through syncHost | Pro + OAuth configured on web app |
| SAML through syncHost | Enterprise |
| Bot protection | N/A -- must be disabled for extensions |
See Also
clerk-setup- Initial Clerk installclerk-custom-ui- Custom flows & appearance
Files
- ATTRIBUTION.mdAdded by AutoGineer887 B
- SKILL.md11.4 KiB
- evals/evals.json4.8 KiB
- references/content-scripts.md3.1 KiB
- references/create-clerk-client.md3.9 KiB
- references/headless-extension.md3.7 KiB
- references/sync-host.md4.1 KiB
- templates/chrome-ext-basic-auth/package.json450 B
- templates/chrome-ext-basic-auth/src/popup.tsxScript1.1 KiB
- templates/chrome-ext-basic-auth/tsconfig.json163 B
references/sync-host.md
syncHost -- Sync Auth with Web App
When to Use
Use syncHost when you need:
OAuth (Google, GitHub, etc.)
SAML
Email magic links
The extension to reflect auth state from your web app without the user signing in again
Without syncHost, the extension popup can only do email/password, OTP, and passkeys.
How It Works
The extension reads Clerk's session cookie from your web app's domain using host_permissions. The syncHost prop tells ClerkProvider which domain to sync from.
Step 1 -- Environment Variables
Use separate files for dev vs prod so Plasmo passes the right values to each build.
.env.development:
PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_FRONTEND_API=https://your-app.clerk.accounts.dev
PLASMO_PUBLIC_CLERK_SYNC_HOST=http://localhost.env.production:
PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_live_...
CLERK_FRONTEND_API=https://clerk.your-domain.com
PLASMO_PUBLIC_CLERK_SYNC_HOST=https://clerk.your-domain.comThe production value of PLASMO_PUBLIC_CLERK_SYNC_HOST is the domain your Clerk Frontend API runs on (e.g., https://clerk.your-domain.com), not your app's main domain.
Step 2 -- ClerkProvider with syncHost
import { ClerkProvider, Show, UserButton } from '@clerk/chrome-extension'
import { Link, Outlet, useNavigate } from 'react-router-dom'
const PUBLISHABLE_KEY = process.env.PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY
const SYNC_HOST = process.env.PLASMO_PUBLIC_CLERK_SYNC_HOST
if (!PUBLISHABLE_KEY || !SYNC_HOST) {
throw new Error('Missing PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY or PLASMO_PUBLIC_CLERK_SYNC_HOST')
}
export function RootLayout() {
const navigate = useNavigate()
return (
<ClerkProvider
publishableKey={PUBLISHABLE_KEY}
syncHost={SYNC_HOST}
afterSignOutUrl="/"
routerPush={(to) => navigate(to)}
routerReplace={(to) => navigate(to, { replace: true })}
>
<Outlet />
</ClerkProvider>
)
}Step 3 -- Manifest host_permissions
In package.json, configure host_permissions to grant the extension access to the sync host domain:
{
"manifest": {
"key": "$CRX_PUBLIC_KEY",
"permissions": ["cookies", "storage"],
"host_permissions": [
"$PLASMO_PUBLIC_CLERK_SYNC_HOST/*",
"$CLERK_FRONTEND_API/*"
]
}
}Plasmo interpolates env vars in package.json at build time. In dev this resolves to http://localhost/*.
Step 4 -- Register Extension ID in Clerk
Clerk must explicitly allow requests from the extension's origin. Run this once per environment:
curl -X PATCH https://api.clerk.com/v1/instance \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-H "Content-type: application/json" \
-d '{"allowed_origins": ["chrome-extension://YOUR_EXTENSION_ID"]}'Replace YOUR_SECRET_KEY with your Clerk Secret Key (sk_test_... or sk_live_...) and YOUR_EXTENSION_ID with your stable CRX ID.
If your extension ID changes (unstable key), you must re-run this command. Configure a stable CRX ID to avoid repeating this step.
Hide Unsupported Auth Methods in Popup
When using syncHost, your web app may have OAuth enabled but the popup itself can't do OAuth flows. Hide those buttons in the popup:
import { SignIn, SignUp } from '@clerk/chrome-extension'
function SignInPage() {
return (
<SignIn
appearance={{
elements: {
socialButtonsRoot: 'plasmo-hidden',
dividerRow: 'plasmo-hidden',
},
}}
/>
)
}
function SignUpPage() {
return (
<SignUp
appearance={{
elements: {
socialButtonsRoot: 'plasmo-hidden',
dividerRow: 'plasmo-hidden',
},
}}
/>
)
}This way the popup shows only the methods it supports (email/password, OTP) while the web app exposes all methods including OAuth.
Side Panel Limitation
syncHost does not fully support side panels. If a user signs in via the web app, the side panel will not automatically update its auth state. The user must close and reopen the side panel to reflect the new auth status. This is a known limitation of the SDK.
Docs
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Needs network access, Needs Node.js
The author says: Requires PLASMO_PUBLIC_CLERK_PUBLISHABLE_KEY (Plasmo prefix for public env vars) and CLERK_FRONTEND_API.
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- clerk
- Publisher
- Clerk (Official)
- Source
- clerk/skills · skills/clerk-chrome-extension-patterns · commit c5d2c0d
- Licence
- MIT License (redistribution allowed)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by clerk or Clerk.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download clerk-chrome-extension-patterns.zip