# Add Clerk authentication to a React app with the Clerk CLI

**How do I add Clerk authentication to a React app with the Clerk CLI?**

Run `clerk init --starter --framework react` to scaffold a Vite + React 19 [single-page app](https://clerk.com/glossary.md#software-development-kit-sdk) with `@clerk/react` v6, [`<ClerkProvider>`](https://clerk.com/glossary.md#clerkprovider) wired in `main.tsx`, `<Show when="signed-in">` gating, and populated [environment variables](https://clerk.com/glossary.md#environment-variables) — one command, no dashboard round-trip. Then run `clerk env pull` to write keys, `clerk doctor` to verify the setup, and `clerk config patch` to enable [passkeys](https://clerk.com/glossary.md#passkeys), sign-in methods, session lifetime, and lockout policy as version-controllable JSON. The walkthrough below covers the full sequence end-to-end, signing up a real user, and inspecting the result with `clerk api`.

Not covered here: building your own backend to verify Clerk-issued [tokens](https://clerk.com/glossary.md#token), React Router or TanStack Router integration (the CLI starter ships neither), and Next.js App Router patterns (the Next CLI article covers those). If you need server-side rendering or [middleware](https://clerk.com/glossary.md#middleware)-based route protection, [Next.js](https://clerk.com/glossary.md#next-js) or TanStack Start is the better starting point.

## Why the Clerk CLI for React

React is the broadest surface Clerk ships for. The [pre-CLI path](https://clerk.com/docs/react/getting-started/quickstart.md) — install `@clerk/react`, hand-wire [`<ClerkProvider>`](https://clerk.com/glossary.md#clerkprovider) in `main.tsx`, paste keys from a dashboard tab into `.env.local`, and hope you got the provider placement right — worked, but it was fragile. Every tutorial had to re-explain the wiring, and AI coding agents couldn't reliably reproduce the setup without stepping out to a browser.

The CLI changes the answer. `clerk init --starter --framework react` scaffolds a Vite + React 19 project with `@clerk/react` v6, a wired `<ClerkProvider>`, `<Show when="signed-in">` gating, and populated [environment variables](https://clerk.com/glossary.md#environment-variables). One command. The same CLI also unlocks three things the dashboard-era tutorials never could:

- `clerk config patch` — apply an instance configuration diff as reviewable JSON, with `--dry-run` and `--yes` flags so changes flow through code review, not a toggle.
- `clerk api` — call any Backend or Platform API endpoint with your authenticated context already resolved.
- `clerk doctor` — pre-flight your local setup and surface missing keys, drifted skill versions, or configuration mismatches.

Taken together, the CLI is the first Clerk surface that treats a React app setup as a scriptable, auditable pipeline. That is the shape AI coding agents need and the shape a serious team wants in CI. This guide assumes you have decided a client-rendered React SPA is the right shape for your product. If you are not sure — or you know you will need server-side [sessions](https://clerk.com/glossary.md#session), middleware, or server-only secrets in route handlers — jump to the Next.js or TanStack Start entries in this cluster before you start.

## Prerequisites

You need:

- [ ] Node.js 20 or newer (the CLI and the generated Vite project both require it).
- [ ] A package manager: `pnpm` is used throughout this guide, but `npm`, `yarn`, and `bun` all work.
- [ ] A free Clerk account at [clerk.com](https://clerk.com/).
- [ ] A terminal, a browser, and an email address you can receive code to.
- [ ] Optional: an existing Clerk application ID (the `app_…` prefix visible in the dashboard URL). If you do not have one, the CLI creates one for you.

You do not need Git initialized, a deployment target, or [OAuth](https://clerk.com/glossary.md#oauth) provider credentials to follow this walkthrough. OAuth social providers work in development with Clerk's shared credentials; production OAuth requires your own credentials, which the "Pull environment variables" section flags.

## Install or update the Clerk CLI

The [Clerk CLI README](https://github.com/clerk/cli#installation) advertises two install methods: a Homebrew tap for macOS and Linux, and an npm package that works anywhere Node.js is installed (including Windows). Pick one — whichever you pick, updates happen in-place with `clerk update`:

**Homebrew**

filename: terminal
```sh
# macOS and Linux (Homebrew tap)
brew install clerk/stable/clerk
```

**npm**

filename: terminal
```sh
# Any platform with Node.js 20+ (cross-platform, including Windows)
npm install -g clerk
```

The Homebrew tap lives at [github.com/clerk/homebrew-stable](https://github.com/clerk/homebrew-stable) and pulls the matching prebuilt binary from the [CLI's GitHub releases](https://github.com/clerk/cli/releases). The npm package is published as `clerk` (not `@clerk/cli`) — a small wrapper that downloads the right native binary on install. Both paths land the same `clerk` executable on your `PATH`.

Verify the install and update if needed:

```bash
clerk --version
clerk update --yes
```

Anything older than the 2026-04-22 release lacks `clerk init`, `clerk config`, `clerk skill`, and the `--mode agent` flag. `clerk update --yes` upgrades to the latest stable; `clerk update --channel canary` opts into pre-release builds if you want to track fixes between stable releases.

> `clerk doctor` is a good habit. Run it any time the CLI or your project feels off — it checks authentication state, env files, skill currency, and more in one pass. `clerk doctor --spotlight` formats the output for pasting into a bug report.

## Install the Clerk agent skill and update existing Clerk skills

If you are using an AI coding agent (Claude Code, Codex, Aider, Gemini CLI, or similar), install the Clerk skill bundle once per machine:

```bash
clerk skill install -y
```

This installs Clerk-maintained agent skills into your global agent config (for Claude Code, `~/.claude/skills/`). The bundle covers framework-specific guidance for React, Next.js, TanStack Start, Expo, Astro, and others, plus shared skills like `clerk-setup`, `clerk-webhooks`, and `clerk-custom-ui`. The agent loads the right skill by topic, so you do not have to remember names.

> Skills drift as Clerk ships. The content your agent sees is cached locally; new features, deprecations, and breaking changes only surface after a skill update. Re-run `clerk skill install -y` after each Clerk major release (Core 3 in March 2026 is the most recent) and any time an agent-generated snippet looks suspicious. `clerk doctor` calls out out-of-date skills when it sees them.

If you have hand-authored Clerk skills from earlier experiments, review them after the install — the bundled skills supersede the stale ones, and keeping both causes the agent to mix old and new APIs in the same file.

## Log in and pick an application

The CLI holds credentials in your OS keychain after a browser-based OAuth flow:

```bash
clerk auth login
clerk whoami
```

`clerk auth login` opens your browser to Clerk, you approve the CLI, and the token lands in the keychain. `clerk whoami` prints your email and which application is currently selected. If no app is selected, the next command asks you to pick one:

```bash
clerk apps list
```

The list shows each app's human name and `app_…` ID. You will pass the ID to `clerk init` (or `clerk link`) in the next sections — copy it.

## Scaffold a React app with `clerk init --starter`

From an empty parent directory, run:

```bash
clerk init --starter --framework react --pm pnpm
```

The CLI prompts for a project name (defaults to `my-clerk-react-app`), creates the directory, installs dependencies, links the project to a Clerk application (prompting you to pick from `clerk apps list` or to create a new one), and writes `.env.local`.

Here is what lands in the directory:

```
my-clerk-react-app/
├── .env.local               # VITE_CLERK_PUBLISHABLE_KEY + CLERK_SECRET_KEY
├── index.html
├── package.json             # vite ^8.0, react ^19.2, @clerk/react ^6.4
├── src/
│   ├── App.tsx              # sample page
│   ├── main.tsx             # <ClerkProvider> wrapper, <Show> gating
│   └── ...
├── tsconfig.json
└── vite.config.ts
```

Three things matter for this guide:

1. **The underlying tool is Vite 8.** Dev server runs on [port 5173](https://vite.dev/config/server-options) by default, env vars use the `VITE_` prefix, and `pnpm build` runs `tsc -b && vite build`. If you are familiar with Vite, this is the same Vite.
2. **The Clerk package is `@clerk/react`, not `@clerk/clerk-react`.** [Core 3 (March 2026)](https://clerk.com/changelog/2026-03-03-core-3.md) renamed the SPA SDK. Legacy snippets that import from `@clerk/clerk-react` predate Core 3 and will not work against the current types.
3. **The starter ships no router and no protected route.** It is a single-page `App.tsx` with `<Show when="signed-in">` / `<Show when="signed-out">` gating the header. Adding a router (React Router, TanStack Router) is your call and out of scope for the CLI scaffold.

> Passing `--pm pnpm` tells the CLI to run `pnpm install`, but the generated lockfile for the React starter is still `package-lock.json` at the time of writing. `pnpm dev`, `pnpm build`, and `pnpm install` all work because `package.json` scripts are generic — the lockfile name is cosmetic, not functional. If this bothers you, delete `package-lock.json` and run `pnpm install` to regenerate `pnpm-lock.yaml`.

## Link to an existing Clerk app (optional)

If `clerk init` linked you to the wrong application, or you want to point the project at production later, unlink and relink:

```bash
clerk unlink
clerk link --app app_xxx
```

`clerk unlink` clears the local link (in `.clerk/config.json` inside the project). `clerk link --app <id>` associates the project with a different application. Re-running `clerk env pull` after a relink rewrites `.env.local` with the new app's keys, so the dev server picks up the change on next restart.

Linking is per-project, not per-machine. A single machine can have multiple projects pointed at different apps (staging, prod, per-client instances) without interference.

## Pull environment variables

The starter already pulled env vars during `clerk init`. If you later add a teammate, move to a new machine, or rotate a key, pull again:

```bash
clerk env pull
cat .env.local
```

Expected output:

```bash
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
```

`VITE_CLERK_PUBLISHABLE_KEY` is the client-exposed [publishable key](https://clerk.com/glossary.md#publishable-key). Vite [inlines every variable prefixed with `VITE_`](https://vite.dev/guide/env-and-mode) into the client bundle at build time, which is expected for this key — publishable keys identify your Clerk instance to the Frontend API and carry no authority beyond that.

`CLERK_SECRET_KEY` is the server-side [secret key](https://clerk.com/glossary.md#secret-key). Vite does not expose unprefixed variables to the client bundle, so it stays out of your browser-shipped JavaScript. The CLI writes it anyway so the same `.env.local` works when (not if) you add a backend service or use tooling like `clerk doctor`. Treat the presence of the secret key in the file as a placeholder for the future, not as an invitation to read it from React code.

> Never rename `CLERK_SECRET_KEY` to `VITE_CLERK_SECRET_KEY`. Doing so tells Vite to inline the secret into the client bundle, which means anyone who loads your site can read it in the browser devtools. A 2024 write-up of a real breach ([Sprocket Security](https://www.sprocketsecurity.com/blog/hunting-secrets-in-javascript-at-scale-how-a-vite-misconfiguration-lead-to-full-ci-cd-compromise)) traced full CI/CD compromise to exactly this Vite misconfiguration on an unrelated app. The `VITE_` prefix is the only meaningful client/server boundary in a Vite project — do not cross it.

If you need production keys on a CI server or in a deploy pipeline, pass `--instance prod` to `clerk env pull`. Development and production keys are different and non-overlapping; the CLI surfaces whichever one the linked application's instance selector points at.

## Run `clerk doctor` the first time (it should fail)

Before you start the dev server, prove the safety net works. Move the env file out of the way and run:

```bash
mv .env.local .env.local.bak
clerk doctor
```

Expected output:

```
! No .env.local or .env file found
    Run `clerk env pull` to create one with your Clerk keys.
```

`clerk doctor` is doing its job: it spotted the missing env file before the dev server would have handed you an opaque "publishable key is required" error. Two unrelated warnings may show up in the same output (missing `~/.clerk/config.json`, shell completion not installed) — both are noisy-but-not-broken and do not affect the app.

> Whenever the dev server fails to render Clerk UI, try `clerk doctor` before opening devtools. Missing env files, drifted skill versions, an unselected application, and `@clerk/react` version mismatches all surface here first.

## Run `clerk doctor` after env pull (green)

Restore the env file and re-run:

```bash
mv .env.local.bak .env.local
clerk doctor
```

Expected output:

```
✓ .env.local contains VITE_CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY (development instance)
```

Green. Now you can start the dev server.

## Start the dev server and sign up a test user

Run:

```bash
pnpm dev
```

Vite prints a URL like `http://localhost:5173/`. Open it. The starter renders a header with a sign-in button and a sign-up button. Click **Sign up**, enter an email address, complete the one-time code, and the header flips to show `<UserButton />` — Clerk's prebuilt account menu with sign-out, account management, and (if enabled) organization switching.

A real sign-up creates a real user in your Clerk development instance. You can confirm with `clerk api /users` from the project directory — the new user's record comes back as JSON.

> If the starter's `main.tsx` reads `publishableKey` implicitly from the environment and you hit a TypeScript build error like `TS2741: Property 'publishableKey' is missing`, pass the prop explicitly:
>
> ```tsx
> import { createRoot } from 'react-dom/client'
> import { ClerkProvider } from '@clerk/react'
> import App from './App'
>
> createRoot(document.getElementById('root')!).render(
>   <ClerkProvider publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}>
>     <App />
>   </ClerkProvider>,
> )
> ```
>
> The provider auto-reads the env var at runtime, but the TypeScript prop is non-optional in the current types. The explicit form works everywhere and is safer to copy into your own `main.tsx`.

## React-specific: protecting a route and understanding SPA auth considerations

React SPA + Clerk is a specific shape, and a few of its details only matter in this shape. This section covers them all in one place so you can decide up front whether a pure SPA is the right fit.

### Gating UI with `<Show>`

In Clerk Core 3, the React SDK unifies every conditional render behind one component. The legacy `<SignedIn>` / `<SignedOut>` components were removed from `@clerk/react` v6 — the current primitive is [`<Show>`](https://clerk.com/docs/react/reference/components/control/show.md) with a typed `when` prop:

```tsx
// src/App.tsx
import { Show, UserButton } from '@clerk/react'

function Dashboard() {
  return (
    <section>
      <h2>Dashboard</h2>
      <p>Signed-in-only content.</p>
    </section>
  )
}

export default function App() {
  return (
    <main>
      <header>
        <Show when="signed-in">
          <UserButton />
        </Show>
        <Show when="signed-out">
          <p>Sign in to see the dashboard.</p>
        </Show>
      </header>

      <Show when="signed-in">
        <Dashboard />
      </Show>
    </main>
  )
}
```

The `when` prop takes `"signed-in"`, `"signed-out"`, or an authorization object (role, permission, feature, plan) — one component for both authentication and authorization checks. `<Show>` is purely visual: it only controls what renders. Any data the signed-in UI depends on still needs to come from an API call that a backend has independently verified.

### Route gating without a router

The CLI starter does not ship a router. You have two options:

- **Conditional render only** — wrap the component (or the whole page) in `<Show when="signed-in">` as above. Fine for a single-page dashboard.
- **Add a router** — install `react-router-dom` (or TanStack Router), define routes, and gate them with `<Show>` or a custom guard. The pattern is the same either way: client-side guards are UX, not security.

For a custom guard, `useAuth()` from `@clerk/react` returns `{ isLoaded, isSignedIn, userId, getToken }`. Wrap any route or component in a conditional that waits for `isLoaded` (to avoid rendering during the initial auth resolve) and then branches on `isSignedIn`:

```tsx
import { useAuth } from '@clerk/react'
import { Navigate } from 'react-router-dom'

export function RequireAuth({ children }: { children: React.ReactNode }) {
  const { isLoaded, isSignedIn } = useAuth()
  if (!isLoaded) return null
  if (!isSignedIn) return <Navigate to="/sign-in" replace />
  return <>{children}</>
}
```

Use hooks when you need to run code conditionally (not just render), use `<Show>` when you only need to render conditionally.

### Where the publishable key lives in your bundle

The publishable key ships to the client, and that is deliberate. It identifies your Clerk Frontend API to the browser and cannot be used to call the Backend API or modify user records (see [How Clerk works](https://clerk.com/docs/guides/how-clerk-works/overview.md) for the underlying model). The authority surface of a publishable key is: tell Clerk "I am this instance, here is a session, please issue a JWT." That is the entire capability.

The secret key never ships to the client. In a pure SPA there is no place to use it safely — it belongs on a server. `clerk env pull` writes it to `.env.local` because your future backend will need it and keeping both keys in one file avoids drift. Until that backend exists, the secret key sits there unused.

### OAuth redirect URLs on production deploys

In development, OAuth flows (Google, GitHub, etc.) use Clerk's shared OAuth credentials, which are pre-allowlisted for `localhost`. In production, each origin you deploy to — `https://app.example.com`, `https://staging.example.com` — must be allowlisted in your Clerk instance before first sign-in will complete. Missing this step is the most common "it worked locally, broken in prod" OAuth failure.

Allowlist production origins via the Clerk Dashboard ([production deploy checklist](https://clerk.com/docs/guides/development/deployment/production.md), [customize redirect URLs](https://clerk.com/docs/guides/development/customize-redirect-urls.md)) or via the Backend API. If you enable your own OAuth providers (not Clerk's shared credentials), each provider also has its own authorized-redirect list inside that provider's console — Google, GitHub, and Apple all require the provider-side allowlist independently of Clerk's side. Plan both sides of that allowlist change into the same deploy.

### When React SPA + Clerk is not enough

A pure client-rendered React app using Clerk is the right shape when:

- Your app is fully client-rendered and talks to APIs you already secure elsewhere.
- The sensitive data your app shows is gated by its own API's authorization, not by what the React tree renders.
- You do not need server-side session reads or middleware-based route protection.

If any of those do not hold — if you need server-side rendering, protected API routes on the same origin, or server-only secrets in request handlers — reach for Next.js or TanStack Start. Both ship dedicated Clerk SDKs (`@clerk/nextjs` v6, `@clerk/tanstack-react-start` v1) with server-side auth helpers, middleware, and SSR-aware providers. Switching framework later is annoying but possible; getting the shape right up front is cheaper.

If you have a backend (Express, Hono, FastAPI, Go, whatever), Clerk issues a short-lived [JWT](https://clerk.com/glossary.md#json-web-token) for each signed-in user that your SPA attaches to outbound requests as a `Bearer` token via `useAuth().getToken()`. Your backend must verify that token — signature, issuer, expiry, and any required claims — on every protected request using Clerk's server SDK or a JWKS lookup. The "How do I verify Clerk tokens on my backend?" question in the FAQ points at the dedicated guide; that workflow is out of scope for this article.

## Configure Clerk as code: `clerk config`

The click-through dashboard is still there when you want it, but the CLI now exposes instance configuration as pull-able, patch-able JSON. Combined with git, that is the "config as code" workflow dashboards never supported: every sign-in option, passkey toggle, session policy, and attack-protection rule is a diff on a branch, reviewed and merged like any other change.

The core loop is four commands:

```bash
clerk config schema                          # what fields exist
clerk config pull --output config.before.json  # snapshot the current state
clerk config patch --dry-run --json '{...}'  # preview a change
clerk config patch --json '{...}' --yes      # apply it
```

Let's run four patches against the React instance. Start by snapshotting the baseline:

```bash
clerk config pull --output config.before.json
```

### Patch 1 — enable passkeys

Passkeys are [WebAuthn](https://clerk.com/glossary.md#webauthn)-backed credentials (biometric or device-bound) that replace passwords. The correct schema field is `used_for_sign_in`:

```bash
clerk config patch --dry-run --json '{"auth_passkey":{"used_for_sign_in":true}}'
clerk config patch --json '{"auth_passkey":{"used_for_sign_in":true}}' --yes
```

> `auth_passkey.enabled` is not a real schema field. Patches that include it are accepted by the API but silently stripped on apply, and the diff looks like a no-op even though the CLI reports success. Always snapshot with `clerk config pull` and diff against the result to confirm a patch actually persisted. `used_for_sign_in` is the field that toggles passkey sign-in on.

> Passkeys are a paid-plan feature. On the free plan, `config patch` returns a 403 when you try to enable them. Plan and pricing details live on the [Clerk Pricing page](https://clerk.com/pricing).

### Patch 2 — add username as a sign-in method

Enable username-based sign-in and sign-up:

```bash
clerk config patch --dry-run --json '{"auth_username":{"used_for_sign_in":true,"used_for_sign_up":true}}'
clerk config patch --json '{"auth_username":{"used_for_sign_in":true,"used_for_sign_up":true}}' --yes
```

Each identifier (email, phone, username, first name, last name) has the same two flags — `used_for_sign_in` and `used_for_sign_up` — which is how you mix and match the shape of your sign-in form as code.

### Patch 3 — set session lifetime

Session policy lives under `session`. The correct schema field is `lifetime` (seconds), not `inactivity_timeout_in_ms`:

```bash
clerk config patch --dry-run --json '{"session":{"lifetime":7200}}'
clerk config patch --json '{"session":{"lifetime":7200}}' --yes
```

`lifetime: 7200` sets a two-hour session token lifetime. The Clerk frontend auto-refreshes tokens before expiry, so `lifetime` sets the outer bound, not the frequency of a full sign-in.

> Custom session configuration is a paid-plan feature. On the free plan, the patch returns 403. Pricing on the [Clerk Pricing page](https://clerk.com/pricing).

### Patch 4 — harden attack protection

Block disposable-email providers and tighten the brute-force lockout threshold in one patch:

```bash
clerk config patch --dry-run --json '{"auth_access_control":{"block_disposable_email_domains":true},"auth_attack_protection":{"user_lockout":{"max_attempts":10}}}'
clerk config patch --json '{"auth_access_control":{"block_disposable_email_domains":true},"auth_attack_protection":{"user_lockout":{"max_attempts":10}}}' --yes
```

One JSON payload can touch unrelated subsystems; `config patch` merges into the existing configuration and only persists fields the schema recognizes.

### Confirm the diff

Snapshot again and diff:

```bash
clerk config pull --output config.after.json
diff config.before.json config.after.json
```

Expected output (abbreviated):

```
auth_access_control.block_disposable_email_domains: false → true
auth_attack_protection.user_lockout.max_attempts:   100 → 10
auth_passkey.used_for_sign_in:                       false → true
auth_username.used_for_sign_in:                      false → true
auth_username.used_for_sign_up:                      false → true
session:                                             null → { allowed_clock_skew: 5, claims: {}, lifetime: 7200 }
```

If your diff has a field listed in one of the patches but missing from the after snapshot, that field name is not in the current schema. Re-check `clerk config schema` and adjust.

> `clerk config put` replaces the entire configuration with the supplied JSON. Fields you omit are reset to defaults — which, for a live application, can silently log every user out or strip OAuth providers. Use `patch` for incremental changes and reserve `put` for scripted instance re-creation with a reviewed-to-completion payload.

Commit `config.before.json` and `config.after.json` (or their delta) to your repo. When the next engineer asks "when did we turn on passkeys?" the answer is in `git log`, not a dashboard's audit tab.

## Verify the config changes worked

Reload the dev server (`Ctrl-C`, `pnpm dev`). Visit `/` signed in, open the `<UserButton />` menu, and pick **Manage account**. The `<UserProfile />` component now exposes a **Passkeys** section with a **Register a passkey** button — that is patch 1 taking effect. The sign-in page now accepts a username field for new sign-ups (patch 2). Your session tokens now expire after two hours (patch 3 — open devtools and inspect the `__session` cookie's `exp` claim to confirm).

If any of these do not appear, run `clerk doctor` and re-diff the before/after snapshots. Silent schema strips are the most common reason a patch looks like it applied but did not. See the warning in Patch 1 for the canonical example.

## Inspect your instance with `clerk api`

`clerk api` is an HTTP client pre-authenticated to your linked instance. It covers both the Backend API (users, organizations, sessions, etc.) and the Platform API (accounts, applications, instances — for managing Clerk itself).

List available endpoints:

```bash
clerk api ls users
```

Output: the endpoints under `/users`, with HTTP method and path. The same `ls` works against any resource:

```bash
clerk api ls organizations
clerk api ls sessions
clerk api ls sign-in-tokens
```

Fetch an actual resource:

```bash
clerk api /users
clerk api /users/<user_id>
```

The first returns the user list as JSON (empty array until you sign up). The second returns one user record.

For the Platform API — the surface that manages your Clerk account itself rather than the users inside one instance — pass `--platform`:

```bash
clerk api --platform ls
clerk api --platform /platform/applications
```

> Platform API paths are prefixed with `/platform/...`, not `/accounts`. Calls like `clerk api --platform /accounts` return `clerk_key_invalid` because the routing table does not have an `/accounts` entry at the Platform level. Use `clerk api --platform ls` to browse the correct paths — `/platform/applications`, `/platform/instances`, and similar are the real endpoints.

Every `clerk api` call is a real API call against your real instance. Use `--instance dev` / `--instance prod` to target a specific instance if the app has both. Pipe the output through `jq` for quick filtering:

```bash
clerk api /users | jq '.[].email_addresses[0].email_address'
```

## CLI reference (quick skim)

The commands this article used, plus the handful you will reach for next:

| Command                                            | What it does                                                                     |
| -------------------------------------------------- | -------------------------------------------------------------------------------- |
| `clerk --version`                                  | Print CLI version.                                                               |
| `clerk update --yes`                               | Update to latest stable. `--channel canary` for pre-release.                     |
| `clerk auth login`                                 | Browser-based OAuth to Clerk. Stores tokens in OS keychain.                      |
| `clerk whoami`                                     | Print the authenticated user and currently-selected app.                         |
| `clerk apps list`                                  | List Clerk applications you have access to.                                      |
| `clerk apps create`                                | Create a new Clerk application without the dashboard.                            |
| `clerk init --starter --framework react --pm pnpm` | Scaffold a Vite + React + Clerk project.                                         |
| `clerk init`                                       | Same, but runs in an existing project directory (auto-detects framework).        |
| `clerk link --app app_xxx`                         | Associate the current project with a Clerk app.                                  |
| `clerk unlink`                                     | Clear the current project's application link.                                    |
| `clerk env pull`                                   | Write `.env.local` with the linked app's keys. `--instance prod` for production. |
| `clerk doctor`                                     | Pre-flight the local setup. `--spotlight` for bug-report formatting.             |
| `clerk config schema`                              | Print the configuration JSON schema.                                             |
| `clerk config pull --output <file>`                | Snapshot the instance configuration.                                             |
| `clerk config patch --dry-run --json '{...}'`      | Preview a config change.                                                         |
| `clerk config patch --json '{...}' --yes`          | Apply a config change.                                                           |
| `clerk config put`                                 | Replace the entire configuration (destructive — use sparingly).                  |
| `clerk api ls <resource>`                          | List endpoints under a resource.                                                 |
| `clerk api /<path>`                                | Call a Backend API endpoint.                                                     |
| `clerk api --platform /<path>`                     | Call a Platform API endpoint.                                                    |
| `clerk skill install -y`                           | Install Clerk-maintained agent skills globally.                                  |
| `clerk completion <shell>`                         | Generate shell completion for bash/zsh/fish.                                     |
| `clerk open`                                       | Open the Clerk Dashboard for the current app in your browser.                    |

Anything not listed here: run `clerk <subcommand> --help` or `clerk --help` for the full surface.

## FAQ

## FAQ

### Should I use Clerk in a React SPA, or do I need Next.js?

Both work. A client-rendered React SPA (the shape `clerk init --starter --framework react` scaffolds) is fine when your app is fully client-rendered and talks to APIs you already secure elsewhere. Reach for Next.js when you need server-side rendering, server-only secrets in route handlers, middleware-based route protection, or React Server Components. Clerk ships a first-party `@clerk/react` SDK for SPAs and `@clerk/nextjs` for Next.js — pick the shape that matches your product, not the one that sounds more capable.

### What does clerk init --starter --framework react actually scaffold?

A Vite 8 + React 19 project using `@clerk/react` v6 (Core 3), with `<ClerkProvider>` wired into `main.tsx`, `<Show when="signed-in">` / `<Show when="signed-out">` gating the header, and the dev server running on port 5173. The starter ships no router and no protected route — adding those is your call. Record the exact versions from your own `package.json` because the starter tracks upstream.

### Can I add Clerk to an existing pnpm create vite React app without --starter?

Yes. Run `clerk init` inside the existing project directory. The CLI auto-detects Vite + React, installs `@clerk/react`, wires `<ClerkProvider>` into `main.tsx`, and (if you're logged in) links the project to a Clerk application and pulls env vars. Skip the `--starter` flag — that one is specifically for bootstrapping a brand-new project from nothing.

### Does CLERK\_SECRET\_KEY leak into my Vite bundle?

No — not unless you rename it. Vite only inlines variables prefixed with `VITE_` into the client bundle. `clerk env pull` writes both `VITE_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` to `.env.local`, but the secret key is ignored by Vite's define step because it lacks the prefix. Never add `VITE_` to the secret key, never reference it from client code via `import.meta.env`, and consider a CI step that scans built JS for credential patterns if you're shipping to production.

### Why does @clerk/react export <Show when="signed-in"> instead of <SignedIn>?

Clerk Core 3 (released March 2026) collapsed every conditional-render gate — auth state, roles, permissions, features, plans — into one component. `<SignedIn>` and `<SignedOut>` are removed from `@clerk/react` v6. Use `<Show when="signed-in">` and `<Show when="signed-out">`, or pass an authorization object to `when` for role/permission checks. If you find legacy imports in a tutorial or old skill, they predate Core 3 and will not compile against the current types.

### Can I protect routes without <Show> — e.g. with a route guard?

Yes. `useAuth()` from `@clerk/react` returns `{ isLoaded, isSignedIn, userId, getToken }`. Wrap your router's guard component in a conditional that waits for `isLoaded` and then branches on `isSignedIn`. For navigation-triggered redirects, pair `useAuth()` with your router's `<Navigate>` (React Router) or equivalent. Hooks are the right choice when you need to run code conditionally; `<Show>` is for pure render gating.

### Do passkeys work in a React SPA?

Yes. Enable them with `clerk config patch --json '{"auth_passkey":{"used_for_sign_in":true}}'` (the `enabled` key is silently stripped — use `used_for_sign_in`), and once on, `<UserProfile />` exposes the passkey registration flow automatically. Passkeys require a paid Clerk plan; the patch returns a 403 on the free plan. Check the [Clerk Pricing page](https://clerk.com/pricing) for current tiers.

### How do I verify Clerk tokens on my backend?

Call `useAuth().getToken()` on the client to get a short-lived JWT, send it as a `Bearer` header, and verify it on the backend using Clerk's server SDK or a JWKS lookup against Clerk's public keys. The default token lifetime is 60 seconds with auto-refresh, so the backend always sees a fresh-ish signature. Do not treat `useAuth()` on the client as an authorization decision — it's a rendering hint, and the backend verification is what actually protects data.

### How do I handle OAuth redirect URLs when deploying to production?

Each production origin (e.g. `https://app.example.com`) must be allowlisted on your Clerk instance before first OAuth sign-in will complete — without it the provider returns to Clerk but no session is created. Allowlist via the Clerk Dashboard (or the Backend API) under your production instance's redirect-URL settings. If you use your own OAuth credentials instead of Clerk's shared dev ones, each provider (Google, GitHub, Apple, etc.) also has its own authorized-redirect list inside that provider's console. Plan both allowlist updates into the same deploy window.

### Does clerk doctor check anything React-specific?

Not really. `clerk doctor` confirms `VITE_CLERK_PUBLISHABLE_KEY` and `CLERK_SECRET_KEY` are present in `.env.local`, that you're authenticated, and that an application is selected. It does not audit `<ClerkProvider>` placement in `main.tsx`, Vite config correctness, or whether you accidentally prefixed the secret key. Treat it as a fast pre-flight, not a full lint.

### How do I switch the project to production keys?

Run `clerk env pull --instance prod` from the project directory. The CLI rewrites `.env.local` with the production publishable and secret keys. Restart the dev server to pick up the change. Every other command (`clerk config patch`, `clerk api`, `clerk doctor`) also supports `--instance prod` to target the production instance explicitly.

### Is the Clerk CLI open source, and where do bugs go?

Yes. Source lives at [github.com/clerk/cli](https://github.com/clerk/cli), released 2026-04-22. File bugs and feature requests on that repo's issue tracker. If you want to track pre-release fixes between stable releases, `clerk update --channel canary` opts you into the canary channel; run `clerk update --channel stable` to switch back.
