Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-lions-claim.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk init --accountless` as the canonical flag and keep `--keyless` as a deprecated compatibility alias. `open` and `whoami` agent JSON now emit the canonical `accountless` key alongside the deprecated `keyless` alias.
6 changes: 3 additions & 3 deletions packages/cli-core/src/commands/api/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Make authenticated HTTP requests to Clerk APIs directly from the command line.
By default, targets the Clerk Backend API (`https://api.clerk.dev/v1/`) using
the instance secret key. Use `--platform` to target the Platform API instead.

Works with no login and no linked project on an **unclaimed keyless
Works with no login and no linked project on an **unclaimed accountless
application** — the one an SDK creates for itself the first time you run
`next dev` (or similar) with no keys configured — by reading the secret key it
already left on disk. See [Authentication](#authentication) below.
Expand DownExpand Up@@ -87,13 +87,13 @@ Secret key resolution order (Backend API, the default):

1. `--secret-key` flag (explicit)
2. Auto-resolve from `--app <id>` via the Platform API (see below)
3. This project's own keyless secret key — from `CLERK_SECRET_KEY`, `.env.local`
3. This project's own accountless secret key — from `CLERK_SECRET_KEY`, `.env.local`
(or the framework's detected env var name), or the SDK's own `.clerk/.tmp/keyless.json`
4. Auto-resolve from linked project profile via the Platform API (see below)

Step 2 exchanges a Platform API token for the target instance's secret key and
works from any directory (no `clerk link` required). Step 3 is what makes
`clerk api` work out of the box against an **unclaimed keyless application** —
`clerk api` work out of the box against an **unclaimed accountless application** —
the one an SDK creates for itself on first `next dev` (or similar) with no keys
configured — with no login and no Platform API auth at all; it only applies when
the directory isn't linked and `--app` wasn't passed, since either of those names
Expand Down
8 changes: 4 additions & 4 deletions packages/cli-core/src/commands/auth/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ Manage authentication with Clerk.

### `clerk auth login` (aliases: `signup`, `signin`, `sign-in`)

Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any keyless application previously created by `clerk init`.
Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any accountless application previously created by `clerk init`.

1. Checks for an existing valid token — if found, prompts to re-authenticate (in agent mode, skips and runs autoclaim immediately)
2. Generates PKCE parameters (code verifier, challenge, state)
Expand All@@ -18,9 +18,9 @@ Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or
8. If this was a re-authentication over an existing session, revokes the previous grant. The outgoing session is read once the authorization code arrives and just before the token exchange replaces it, and revoked only after the replacement is stored — so an abandoned browser flow leaves the original session intact, and a concurrent refresh has the smallest possible window to rotate the token out from under the revocation. A failure here warns rather than failing the login
9. **Autoclaim**: if `.clerk/keyless.json` exists in the current directory, claims the temporary application, links it to the project, and pulls environment variables

#### Keyless autoclaim breadcrumb lifecycle
#### Accountless autoclaim breadcrumb lifecycle

When `clerk init` runs in keyless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:
When `clerk init` runs in accountless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:

- **404** — claim token expired or application already deleted; breadcrumb is cleared and a warning is shown.
- **403** — authenticated account has no active organization; breadcrumb is cleared and a warning is shown.
Expand All@@ -37,7 +37,7 @@ OAuth requests are made against the Clerk OAuth system instance (default `https:
| Token exchange | `POST` | `/oauth/token` | Exchanges authorization code + `code_verifier` for an access token |
| User info | `GET` | `/oauth/userinfo` | Fetches `sub` (user ID) and `email` using the access token |
| Revoke | `POST` | `/oauth/token/revoke` | Revokes the superseded refresh token on re-authentication (RFC 7009) |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims a keyless application by token; returns the full `Application` object |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims an accountless application by token; returns the full `Application` object |

### `clerk auth logout` (aliases: `signout`, `sign-out`)

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/billing/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,9 @@ The handlers are wired to top-level `clerk enable billing` and `clerk disable
billing` commands.

**Requires a claimed application.** Unlike `clerk enable/disable orgs`, these
commands cannot run against an unclaimed keyless application: billing settings
commands cannot run against an unclaimed accountless application: billing settings
exist only in the account-level config document, and Clerk's Backend API exposes
no billing resource an instance secret key could reach. In a keyless project both
no billing resource an instance secret key could reach. In an accountless project both
commands exit with an `auth_required` error pointing at `clerk auth login`.

For arbitrary billing config edits (plans, trials, payment-method requirements)
Expand Down
28 changes: 14 additions & 14 deletions packages/cli-core/src/commands/config/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Manage Clerk instance configuration.
Two modes exist, picked automatically:

- **Account mode** (default) — full instance config document via the Platform API. Used whenever the project is linked or `--app` is passed; requires an account (`clerk auth login` or `CLERK_PLATFORM_API_KEY`).
- **Keyless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Keyless mode](#keyless-mode).
- **Accountless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Accountless mode](#accountless-mode).

## Commands

Expand DownExpand Up@@ -36,7 +36,7 @@ clerk config pull --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -75,7 +75,7 @@ clerk config schema --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand DownExpand Up@@ -116,7 +116,7 @@ clerk config patch --file partial-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -157,7 +157,7 @@ clerk config put --file full-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand All@@ -167,9 +167,9 @@ clerk config put --file full-config.json --dry-run

---

## Keyless mode
## Accountless mode

A keyless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.
An accountless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.

### When it engages

Expand All@@ -178,11 +178,11 @@ Both must hold, otherwise the account-authenticated path runs unchanged:
1. No `--app` was passed.
2. No linked project in the current directory.

**Account credentials are not part of this decision.** Keyless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.
**Account credentials are not part of this decision.** Accountless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.

When credentials _are_ present and the directory simply isn't linked, the command prints a warning that it's using the reduced key-based view and points at `clerk link`, so the narrower output is never a silent surprise.

The secret key is resolved the way the app itself would resolve one, and this order is shared by every keyless-capable command (`lib/keyless-target.ts`):
The secret key is resolved the way the app itself would resolve one, and this order is shared by every accountless-capable command (`lib/keyless-target.ts`):

1. `CLERK_SECRET_KEY`, or the framework's secret key variable (e.g. `NUXT_CLERK_SECRET_KEY`), in the environment
2. `.env`, then `.env.local` — the later file wins
Expand All@@ -192,7 +192,7 @@ A key that doesn't start with `sk_` is rejected. The SDK file comes last because

### Payload shape

The Backend API has no single config document — it exposes independent resources — so keyless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:
The Backend API has no single config document — it exposes independent resources — so accountless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:

```sh
clerk config patch --json '{
Expand DownExpand Up@@ -221,19 +221,19 @@ Any other top-level key exits with a usage error naming the supported ones. Most

### Round-trip verification

A 200 or 204 from a keyless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.
A 200 or 204 from an accountless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.

After a write, the CLI checks every field it sent against the PATCH response body, and against nothing else. Fields whose value round-trips are reported as applied; fields the response doesn't reflect are named explicitly instead of folded into an unconditional success line.

A follow-up GET looks like stronger evidence and is in fact weaker. BAPI omits writable-but-not-readable fields from its reads — `instance.support_email` is accepted and never echoed — and reads are eventually consistent, so a GET issued straight after a write routinely returns the pre-write value. Verifying against one reports perfectly good writes as dropped. This applies equally to the six groups that do have a GET route: the response body is the only read that is guaranteed to be about _this_ write.

That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.
That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and the legacy-named `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.

`restrictions` and `instance_settings` have no GET route, but both echo their new state in the PATCH response, so their writes verify normally.

### Differences from account mode

| Behavior | Account mode | Keyless mode |
| Behavior | Account mode | Accountless mode |
| ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Coverage | Full config document | Seven Backend API resources |
| `--instance` | Selects dev/prod | Usage error — the secret key already targets exactly one instance |
Expand All@@ -244,7 +244,7 @@ That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothi

Run `clerk auth login` to claim the application; auto-claim links it, and every config command then uses account mode with full coverage.

### API Endpoints (keyless mode)
### API Endpoints (accountless mode)

All requests go to the Clerk Backend API (default `https://api.clerk.dev`, overridable via `CLERK_BACKEND_API_URL`), authenticated with a `Bearer sk_…` instance secret key.

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/config/keyless.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,7 +285,7 @@ describe("keyless config", () => {
const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts");

await expect(resolveKeylessTarget({ instance: "prod", cwd: projectDir })).rejects.toThrow(
/--instance is not supported for an unclaimed keyless application/,
/--instance is not supported for an unclaimed accountless application/,
);
});

Expand DownExpand Up@@ -761,7 +761,7 @@ describe("keyless config", () => {

const written = await Bun.file(join(projectDir, ".env.written")).text();
expect(written).toContain(`CLERK_SECRET_KEY=${SECRET_KEY}`);
expect(captured.err).toContain("Keyless application keys");
expect(captured.err).toContain("Accountless application keys");
});

test("enable billing explains that billing needs a claimed application", async () => {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-lions-claim.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk init --accountless` as the canonical flag and keep `--keyless` as a deprecated compatibility alias. `open` and `whoami` agent JSON now emit the canonical `accountless` key alongside the deprecated `keyless` alias.
6 changes: 3 additions & 3 deletions packages/cli-core/src/commands/api/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Make authenticated HTTP requests to Clerk APIs directly from the command line.
By default, targets the Clerk Backend API (`https://api.clerk.dev/v1/`) using
the instance secret key. Use `--platform` to target the Platform API instead.

Works with no login and no linked project on an **unclaimed keyless
Works with no login and no linked project on an **unclaimed accountless
application** — the one an SDK creates for itself the first time you run
`next dev` (or similar) with no keys configured — by reading the secret key it
already left on disk. See [Authentication](#authentication) below.
Expand DownExpand Up@@ -87,13 +87,13 @@ Secret key resolution order (Backend API, the default):

1. `--secret-key` flag (explicit)
2. Auto-resolve from `--app <id>` via the Platform API (see below)
3. This project's own keyless secret key — from `CLERK_SECRET_KEY`, `.env.local`
3. This project's own accountless secret key — from `CLERK_SECRET_KEY`, `.env.local`
(or the framework's detected env var name), or the SDK's own `.clerk/.tmp/keyless.json`
4. Auto-resolve from linked project profile via the Platform API (see below)

Step 2 exchanges a Platform API token for the target instance's secret key and
works from any directory (no `clerk link` required). Step 3 is what makes
`clerk api` work out of the box against an **unclaimed keyless application** —
`clerk api` work out of the box against an **unclaimed accountless application** —
the one an SDK creates for itself on first `next dev` (or similar) with no keys
configured — with no login and no Platform API auth at all; it only applies when
the directory isn't linked and `--app` wasn't passed, since either of those names
Expand Down
8 changes: 4 additions & 4 deletions packages/cli-core/src/commands/auth/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ Manage authentication with Clerk.

### `clerk auth login` (aliases: `signup`, `signin`, `sign-in`)

Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any keyless application previously created by `clerk init`.
Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any accountless application previously created by `clerk init`.

1. Checks for an existing valid token — if found, prompts to re-authenticate (in agent mode, skips and runs autoclaim immediately)
2. Generates PKCE parameters (code verifier, challenge, state)
Expand All@@ -18,9 +18,9 @@ Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or
8. If this was a re-authentication over an existing session, revokes the previous grant. The outgoing session is read once the authorization code arrives and just before the token exchange replaces it, and revoked only after the replacement is stored — so an abandoned browser flow leaves the original session intact, and a concurrent refresh has the smallest possible window to rotate the token out from under the revocation. A failure here warns rather than failing the login
9. **Autoclaim**: if `.clerk/keyless.json` exists in the current directory, claims the temporary application, links it to the project, and pulls environment variables

#### Keyless autoclaim breadcrumb lifecycle
#### Accountless autoclaim breadcrumb lifecycle

When `clerk init` runs in keyless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:
When `clerk init` runs in accountless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:

- **404** — claim token expired or application already deleted; breadcrumb is cleared and a warning is shown.
- **403** — authenticated account has no active organization; breadcrumb is cleared and a warning is shown.
Expand All@@ -37,7 +37,7 @@ OAuth requests are made against the Clerk OAuth system instance (default `https:
| Token exchange | `POST` | `/oauth/token` | Exchanges authorization code + `code_verifier` for an access token |
| User info | `GET` | `/oauth/userinfo` | Fetches `sub` (user ID) and `email` using the access token |
| Revoke | `POST` | `/oauth/token/revoke` | Revokes the superseded refresh token on re-authentication (RFC 7009) |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims a keyless application by token; returns the full `Application` object |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims an accountless application by token; returns the full `Application` object |

### `clerk auth logout` (aliases: `signout`, `sign-out`)

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/billing/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,9 @@ The handlers are wired to top-level `clerk enable billing` and `clerk disable
billing` commands.

**Requires a claimed application.** Unlike `clerk enable/disable orgs`, these
commands cannot run against an unclaimed keyless application: billing settings
commands cannot run against an unclaimed accountless application: billing settings
exist only in the account-level config document, and Clerk's Backend API exposes
no billing resource an instance secret key could reach. In a keyless project both
no billing resource an instance secret key could reach. In an accountless project both
commands exit with an `auth_required` error pointing at `clerk auth login`.

For arbitrary billing config edits (plans, trials, payment-method requirements)
Expand Down
28 changes: 14 additions & 14 deletions packages/cli-core/src/commands/config/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Manage Clerk instance configuration.
Two modes exist, picked automatically:

- **Account mode** (default) — full instance config document via the Platform API. Used whenever the project is linked or `--app` is passed; requires an account (`clerk auth login` or `CLERK_PLATFORM_API_KEY`).
- **Keyless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Keyless mode](#keyless-mode).
- **Accountless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Accountless mode](#accountless-mode).

## Commands

Expand DownExpand Up@@ -36,7 +36,7 @@ clerk config pull --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -75,7 +75,7 @@ clerk config schema --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand DownExpand Up@@ -116,7 +116,7 @@ clerk config patch --file partial-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -157,7 +157,7 @@ clerk config put --file full-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand All@@ -167,9 +167,9 @@ clerk config put --file full-config.json --dry-run

---

## Keyless mode
## Accountless mode

A keyless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.
An accountless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.

### When it engages

Expand All@@ -178,11 +178,11 @@ Both must hold, otherwise the account-authenticated path runs unchanged:
1. No `--app` was passed.
2. No linked project in the current directory.

**Account credentials are not part of this decision.** Keyless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.
**Account credentials are not part of this decision.** Accountless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.

When credentials _are_ present and the directory simply isn't linked, the command prints a warning that it's using the reduced key-based view and points at `clerk link`, so the narrower output is never a silent surprise.

The secret key is resolved the way the app itself would resolve one, and this order is shared by every keyless-capable command (`lib/keyless-target.ts`):
The secret key is resolved the way the app itself would resolve one, and this order is shared by every accountless-capable command (`lib/keyless-target.ts`):

1. `CLERK_SECRET_KEY`, or the framework's secret key variable (e.g. `NUXT_CLERK_SECRET_KEY`), in the environment
2. `.env`, then `.env.local` — the later file wins
Expand All@@ -192,7 +192,7 @@ A key that doesn't start with `sk_` is rejected. The SDK file comes last because

### Payload shape

The Backend API has no single config document — it exposes independent resources — so keyless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:
The Backend API has no single config document — it exposes independent resources — so accountless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:

```sh
clerk config patch --json '{
Expand DownExpand Up@@ -221,19 +221,19 @@ Any other top-level key exits with a usage error naming the supported ones. Most

### Round-trip verification

A 200 or 204 from a keyless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.
A 200 or 204 from an accountless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.

After a write, the CLI checks every field it sent against the PATCH response body, and against nothing else. Fields whose value round-trips are reported as applied; fields the response doesn't reflect are named explicitly instead of folded into an unconditional success line.

A follow-up GET looks like stronger evidence and is in fact weaker. BAPI omits writable-but-not-readable fields from its reads — `instance.support_email` is accepted and never echoed — and reads are eventually consistent, so a GET issued straight after a write routinely returns the pre-write value. Verifying against one reports perfectly good writes as dropped. This applies equally to the six groups that do have a GET route: the response body is the only read that is guaranteed to be about _this_ write.

That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.
That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and the legacy-named `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.

`restrictions` and `instance_settings` have no GET route, but both echo their new state in the PATCH response, so their writes verify normally.

### Differences from account mode

| Behavior | Account mode | Keyless mode |
| Behavior | Account mode | Accountless mode |
| ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Coverage | Full config document | Seven Backend API resources |
| `--instance` | Selects dev/prod | Usage error — the secret key already targets exactly one instance |
Expand All@@ -244,7 +244,7 @@ That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothi

Run `clerk auth login` to claim the application; auto-claim links it, and every config command then uses account mode with full coverage.

### API Endpoints (keyless mode)
### API Endpoints (accountless mode)

All requests go to the Clerk Backend API (default `https://api.clerk.dev`, overridable via `CLERK_BACKEND_API_URL`), authenticated with a `Bearer sk_…` instance secret key.

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/config/keyless.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,7 +285,7 @@ describe("keyless config", () => {
const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts");

await expect(resolveKeylessTarget({ instance: "prod", cwd: projectDir })).rejects.toThrow(
/--instance is not supported for an unclaimed keyless application/,
/--instance is not supported for an unclaimed accountless application/,
);
});

Expand DownExpand Up@@ -761,7 +761,7 @@ describe("keyless config", () => {

const written = await Bun.file(join(projectDir, ".env.written")).text();
expect(written).toContain(`CLERK_SECRET_KEY=${SECRET_KEY}`);
expect(captured.err).toContain("Keyless application keys");
expect(captured.err).toContain("Accountless application keys");
});

test("enable billing explains that billing needs a claimed application", async () => {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-lions-claim.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk init --accountless` as the canonical flag and keep `--keyless` as a deprecated compatibility alias. `open` and `whoami` agent JSON now emit the canonical `accountless` key alongside the deprecated `keyless` alias.
6 changes: 3 additions & 3 deletions packages/cli-core/src/commands/api/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Make authenticated HTTP requests to Clerk APIs directly from the command line.
By default, targets the Clerk Backend API (`https://api.clerk.dev/v1/`) using
the instance secret key. Use `--platform` to target the Platform API instead.

Works with no login and no linked project on an **unclaimed keyless
Works with no login and no linked project on an **unclaimed accountless
application** — the one an SDK creates for itself the first time you run
`next dev` (or similar) with no keys configured — by reading the secret key it
already left on disk. See [Authentication](#authentication) below.
Expand DownExpand Up@@ -87,13 +87,13 @@ Secret key resolution order (Backend API, the default):

1. `--secret-key` flag (explicit)
2. Auto-resolve from `--app <id>` via the Platform API (see below)
3. This project's own keyless secret key — from `CLERK_SECRET_KEY`, `.env.local`
3. This project's own accountless secret key — from `CLERK_SECRET_KEY`, `.env.local`
(or the framework's detected env var name), or the SDK's own `.clerk/.tmp/keyless.json`
4. Auto-resolve from linked project profile via the Platform API (see below)

Step 2 exchanges a Platform API token for the target instance's secret key and
works from any directory (no `clerk link` required). Step 3 is what makes
`clerk api` work out of the box against an **unclaimed keyless application** —
`clerk api` work out of the box against an **unclaimed accountless application** —
the one an SDK creates for itself on first `next dev` (or similar) with no keys
configured — with no login and no Platform API auth at all; it only applies when
the directory isn't linked and `--app` wasn't passed, since either of those names
Expand Down
8 changes: 4 additions & 4 deletions packages/cli-core/src/commands/auth/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ Manage authentication with Clerk.

### `clerk auth login` (aliases: `signup`, `signin`, `sign-in`)

Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any keyless application previously created by `clerk init`.
Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any accountless application previously created by `clerk init`.

1. Checks for an existing valid token — if found, prompts to re-authenticate (in agent mode, skips and runs autoclaim immediately)
2. Generates PKCE parameters (code verifier, challenge, state)
Expand All@@ -18,9 +18,9 @@ Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or
8. If this was a re-authentication over an existing session, revokes the previous grant. The outgoing session is read once the authorization code arrives and just before the token exchange replaces it, and revoked only after the replacement is stored — so an abandoned browser flow leaves the original session intact, and a concurrent refresh has the smallest possible window to rotate the token out from under the revocation. A failure here warns rather than failing the login
9. **Autoclaim**: if `.clerk/keyless.json` exists in the current directory, claims the temporary application, links it to the project, and pulls environment variables

#### Keyless autoclaim breadcrumb lifecycle
#### Accountless autoclaim breadcrumb lifecycle

When `clerk init` runs in keyless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:
When `clerk init` runs in accountless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:

- **404** — claim token expired or application already deleted; breadcrumb is cleared and a warning is shown.
- **403** — authenticated account has no active organization; breadcrumb is cleared and a warning is shown.
Expand All@@ -37,7 +37,7 @@ OAuth requests are made against the Clerk OAuth system instance (default `https:
| Token exchange | `POST` | `/oauth/token` | Exchanges authorization code + `code_verifier` for an access token |
| User info | `GET` | `/oauth/userinfo` | Fetches `sub` (user ID) and `email` using the access token |
| Revoke | `POST` | `/oauth/token/revoke` | Revokes the superseded refresh token on re-authentication (RFC 7009) |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims a keyless application by token; returns the full `Application` object |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims an accountless application by token; returns the full `Application` object |

### `clerk auth logout` (aliases: `signout`, `sign-out`)

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/billing/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,9 @@ The handlers are wired to top-level `clerk enable billing` and `clerk disable
billing` commands.

**Requires a claimed application.** Unlike `clerk enable/disable orgs`, these
commands cannot run against an unclaimed keyless application: billing settings
commands cannot run against an unclaimed accountless application: billing settings
exist only in the account-level config document, and Clerk's Backend API exposes
no billing resource an instance secret key could reach. In a keyless project both
no billing resource an instance secret key could reach. In an accountless project both
commands exit with an `auth_required` error pointing at `clerk auth login`.

For arbitrary billing config edits (plans, trials, payment-method requirements)
Expand Down
28 changes: 14 additions & 14 deletions packages/cli-core/src/commands/config/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Manage Clerk instance configuration.
Two modes exist, picked automatically:

- **Account mode** (default) — full instance config document via the Platform API. Used whenever the project is linked or `--app` is passed; requires an account (`clerk auth login` or `CLERK_PLATFORM_API_KEY`).
- **Keyless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Keyless mode](#keyless-mode).
- **Accountless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Accountless mode](#accountless-mode).

## Commands

Expand DownExpand Up@@ -36,7 +36,7 @@ clerk config pull --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -75,7 +75,7 @@ clerk config schema --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand DownExpand Up@@ -116,7 +116,7 @@ clerk config patch --file partial-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -157,7 +157,7 @@ clerk config put --file full-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand All@@ -167,9 +167,9 @@ clerk config put --file full-config.json --dry-run

---

## Keyless mode
## Accountless mode

A keyless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.
An accountless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.

### When it engages

Expand All@@ -178,11 +178,11 @@ Both must hold, otherwise the account-authenticated path runs unchanged:
1. No `--app` was passed.
2. No linked project in the current directory.

**Account credentials are not part of this decision.** Keyless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.
**Account credentials are not part of this decision.** Accountless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.

When credentials _are_ present and the directory simply isn't linked, the command prints a warning that it's using the reduced key-based view and points at `clerk link`, so the narrower output is never a silent surprise.

The secret key is resolved the way the app itself would resolve one, and this order is shared by every keyless-capable command (`lib/keyless-target.ts`):
The secret key is resolved the way the app itself would resolve one, and this order is shared by every accountless-capable command (`lib/keyless-target.ts`):

1. `CLERK_SECRET_KEY`, or the framework's secret key variable (e.g. `NUXT_CLERK_SECRET_KEY`), in the environment
2. `.env`, then `.env.local` — the later file wins
Expand All@@ -192,7 +192,7 @@ A key that doesn't start with `sk_` is rejected. The SDK file comes last because

### Payload shape

The Backend API has no single config document — it exposes independent resources — so keyless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:
The Backend API has no single config document — it exposes independent resources — so accountless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:

```sh
clerk config patch --json '{
Expand DownExpand Up@@ -221,19 +221,19 @@ Any other top-level key exits with a usage error naming the supported ones. Most

### Round-trip verification

A 200 or 204 from a keyless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.
A 200 or 204 from an accountless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.

After a write, the CLI checks every field it sent against the PATCH response body, and against nothing else. Fields whose value round-trips are reported as applied; fields the response doesn't reflect are named explicitly instead of folded into an unconditional success line.

A follow-up GET looks like stronger evidence and is in fact weaker. BAPI omits writable-but-not-readable fields from its reads — `instance.support_email` is accepted and never echoed — and reads are eventually consistent, so a GET issued straight after a write routinely returns the pre-write value. Verifying against one reports perfectly good writes as dropped. This applies equally to the six groups that do have a GET route: the response body is the only read that is guaranteed to be about _this_ write.

That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.
That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and the legacy-named `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.

`restrictions` and `instance_settings` have no GET route, but both echo their new state in the PATCH response, so their writes verify normally.

### Differences from account mode

| Behavior | Account mode | Keyless mode |
| Behavior | Account mode | Accountless mode |
| ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Coverage | Full config document | Seven Backend API resources |
| `--instance` | Selects dev/prod | Usage error — the secret key already targets exactly one instance |
Expand All@@ -244,7 +244,7 @@ That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothi

Run `clerk auth login` to claim the application; auto-claim links it, and every config command then uses account mode with full coverage.

### API Endpoints (keyless mode)
### API Endpoints (accountless mode)

All requests go to the Clerk Backend API (default `https://api.clerk.dev`, overridable via `CLERK_BACKEND_API_URL`), authenticated with a `Bearer sk_…` instance secret key.

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/config/keyless.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,7 +285,7 @@ describe("keyless config", () => {
const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts");

await expect(resolveKeylessTarget({ instance: "prod", cwd: projectDir })).rejects.toThrow(
/--instance is not supported for an unclaimed keyless application/,
/--instance is not supported for an unclaimed accountless application/,
);
});

Expand DownExpand Up@@ -761,7 +761,7 @@ describe("keyless config", () => {

const written = await Bun.file(join(projectDir, ".env.written")).text();
expect(written).toContain(`CLERK_SECRET_KEY=${SECRET_KEY}`);
expect(captured.err).toContain("Keyless application keys");
expect(captured.err).toContain("Accountless application keys");
});

test("enable billing explains that billing needs a claimed application", async () => {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', '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('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-lions-claim.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk init --accountless` as the canonical flag and keep `--keyless` as a deprecated compatibility alias. `open` and `whoami` agent JSON now emit the canonical `accountless` key alongside the deprecated `keyless` alias.
6 changes: 3 additions & 3 deletions packages/cli-core/src/commands/api/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Make authenticated HTTP requests to Clerk APIs directly from the command line.
By default, targets the Clerk Backend API (`https://api.clerk.dev/v1/`) using
the instance secret key. Use `--platform` to target the Platform API instead.

Works with no login and no linked project on an **unclaimed keyless
Works with no login and no linked project on an **unclaimed accountless
application** — the one an SDK creates for itself the first time you run
`next dev` (or similar) with no keys configured — by reading the secret key it
already left on disk. See [Authentication](#authentication) below.
Expand DownExpand Up@@ -87,13 +87,13 @@ Secret key resolution order (Backend API, the default):

1. `--secret-key` flag (explicit)
2. Auto-resolve from `--app <id>` via the Platform API (see below)
3. This project's own keyless secret key — from `CLERK_SECRET_KEY`, `.env.local`
3. This project's own accountless secret key — from `CLERK_SECRET_KEY`, `.env.local`
(or the framework's detected env var name), or the SDK's own `.clerk/.tmp/keyless.json`
4. Auto-resolve from linked project profile via the Platform API (see below)

Step 2 exchanges a Platform API token for the target instance's secret key and
works from any directory (no `clerk link` required). Step 3 is what makes
`clerk api` work out of the box against an **unclaimed keyless application** —
`clerk api` work out of the box against an **unclaimed accountless application** —
the one an SDK creates for itself on first `next dev` (or similar) with no keys
configured — with no login and no Platform API auth at all; it only applies when
the directory isn't linked and `--app` wasn't passed, since either of those names
Expand Down
8 changes: 4 additions & 4 deletions packages/cli-core/src/commands/auth/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ Manage authentication with Clerk.

### `clerk auth login` (aliases: `signup`, `signin`, `sign-in`)

Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any keyless application previously created by `clerk init`.
Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any accountless application previously created by `clerk init`.

1. Checks for an existing valid token — if found, prompts to re-authenticate (in agent mode, skips and runs autoclaim immediately)
2. Generates PKCE parameters (code verifier, challenge, state)
Expand All@@ -18,9 +18,9 @@ Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or
8. If this was a re-authentication over an existing session, revokes the previous grant. The outgoing session is read once the authorization code arrives and just before the token exchange replaces it, and revoked only after the replacement is stored — so an abandoned browser flow leaves the original session intact, and a concurrent refresh has the smallest possible window to rotate the token out from under the revocation. A failure here warns rather than failing the login
9. **Autoclaim**: if `.clerk/keyless.json` exists in the current directory, claims the temporary application, links it to the project, and pulls environment variables

#### Keyless autoclaim breadcrumb lifecycle
#### Accountless autoclaim breadcrumb lifecycle

When `clerk init` runs in keyless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:
When `clerk init` runs in accountless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:

- **404** — claim token expired or application already deleted; breadcrumb is cleared and a warning is shown.
- **403** — authenticated account has no active organization; breadcrumb is cleared and a warning is shown.
Expand All@@ -37,7 +37,7 @@ OAuth requests are made against the Clerk OAuth system instance (default `https:
| Token exchange | `POST` | `/oauth/token` | Exchanges authorization code + `code_verifier` for an access token |
| User info | `GET` | `/oauth/userinfo` | Fetches `sub` (user ID) and `email` using the access token |
| Revoke | `POST` | `/oauth/token/revoke` | Revokes the superseded refresh token on re-authentication (RFC 7009) |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims a keyless application by token; returns the full `Application` object |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims an accountless application by token; returns the full `Application` object |

### `clerk auth logout` (aliases: `signout`, `sign-out`)

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/billing/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,9 @@ The handlers are wired to top-level `clerk enable billing` and `clerk disable
billing` commands.

**Requires a claimed application.** Unlike `clerk enable/disable orgs`, these
commands cannot run against an unclaimed keyless application: billing settings
commands cannot run against an unclaimed accountless application: billing settings
exist only in the account-level config document, and Clerk's Backend API exposes
no billing resource an instance secret key could reach. In a keyless project both
no billing resource an instance secret key could reach. In an accountless project both
commands exit with an `auth_required` error pointing at `clerk auth login`.

For arbitrary billing config edits (plans, trials, payment-method requirements)
Expand Down
28 changes: 14 additions & 14 deletions packages/cli-core/src/commands/config/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Manage Clerk instance configuration.
Two modes exist, picked automatically:

- **Account mode** (default) — full instance config document via the Platform API. Used whenever the project is linked or `--app` is passed; requires an account (`clerk auth login` or `CLERK_PLATFORM_API_KEY`).
- **Keyless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Keyless mode](#keyless-mode).
- **Accountless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Accountless mode](#accountless-mode).

## Commands

Expand DownExpand Up@@ -36,7 +36,7 @@ clerk config pull --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -75,7 +75,7 @@ clerk config schema --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand DownExpand Up@@ -116,7 +116,7 @@ clerk config patch --file partial-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -157,7 +157,7 @@ clerk config put --file full-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand All@@ -167,9 +167,9 @@ clerk config put --file full-config.json --dry-run

---

## Keyless mode
## Accountless mode

A keyless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.
An accountless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.

### When it engages

Expand All@@ -178,11 +178,11 @@ Both must hold, otherwise the account-authenticated path runs unchanged:
1. No `--app` was passed.
2. No linked project in the current directory.

**Account credentials are not part of this decision.** Keyless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.
**Account credentials are not part of this decision.** Accountless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.

When credentials _are_ present and the directory simply isn't linked, the command prints a warning that it's using the reduced key-based view and points at `clerk link`, so the narrower output is never a silent surprise.

The secret key is resolved the way the app itself would resolve one, and this order is shared by every keyless-capable command (`lib/keyless-target.ts`):
The secret key is resolved the way the app itself would resolve one, and this order is shared by every accountless-capable command (`lib/keyless-target.ts`):

1. `CLERK_SECRET_KEY`, or the framework's secret key variable (e.g. `NUXT_CLERK_SECRET_KEY`), in the environment
2. `.env`, then `.env.local` — the later file wins
Expand All@@ -192,7 +192,7 @@ A key that doesn't start with `sk_` is rejected. The SDK file comes last because

### Payload shape

The Backend API has no single config document — it exposes independent resources — so keyless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:
The Backend API has no single config document — it exposes independent resources — so accountless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:

```sh
clerk config patch --json '{
Expand DownExpand Up@@ -221,19 +221,19 @@ Any other top-level key exits with a usage error naming the supported ones. Most

### Round-trip verification

A 200 or 204 from a keyless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.
A 200 or 204 from an accountless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.

After a write, the CLI checks every field it sent against the PATCH response body, and against nothing else. Fields whose value round-trips are reported as applied; fields the response doesn't reflect are named explicitly instead of folded into an unconditional success line.

A follow-up GET looks like stronger evidence and is in fact weaker. BAPI omits writable-but-not-readable fields from its reads — `instance.support_email` is accepted and never echoed — and reads are eventually consistent, so a GET issued straight after a write routinely returns the pre-write value. Verifying against one reports perfectly good writes as dropped. This applies equally to the six groups that do have a GET route: the response body is the only read that is guaranteed to be about _this_ write.

That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.
That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and the legacy-named `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.

`restrictions` and `instance_settings` have no GET route, but both echo their new state in the PATCH response, so their writes verify normally.

### Differences from account mode

| Behavior | Account mode | Keyless mode |
| Behavior | Account mode | Accountless mode |
| ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Coverage | Full config document | Seven Backend API resources |
| `--instance` | Selects dev/prod | Usage error — the secret key already targets exactly one instance |
Expand All@@ -244,7 +244,7 @@ That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothi

Run `clerk auth login` to claim the application; auto-claim links it, and every config command then uses account mode with full coverage.

### API Endpoints (keyless mode)
### API Endpoints (accountless mode)

All requests go to the Clerk Backend API (default `https://api.clerk.dev`, overridable via `CLERK_BACKEND_API_URL`), authenticated with a `Bearer sk_…` instance secret key.

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/config/keyless.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,7 +285,7 @@ describe("keyless config", () => {
const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts");

await expect(resolveKeylessTarget({ instance: "prod", cwd: projectDir })).rejects.toThrow(
/--instance is not supported for an unclaimed keyless application/,
/--instance is not supported for an unclaimed accountless application/,
);
});

Expand DownExpand Up@@ -761,7 +761,7 @@ describe("keyless config", () => {

const written = await Bun.file(join(projectDir, ".env.written")).text();
expect(written).toContain(`CLERK_SECRET_KEY=${SECRET_KEY}`);
expect(captured.err).toContain("Keyless application keys");
expect(captured.err).toContain("Accountless application keys");
});

test("enable billing explains that billing needs a claimed application", async () => {
Expand Down
Loading
, '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" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-lions-claim.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk init --accountless` as the canonical flag and keep `--keyless` as a deprecated compatibility alias. `open` and `whoami` agent JSON now emit the canonical `accountless` key alongside the deprecated `keyless` alias.
6 changes: 3 additions & 3 deletions packages/cli-core/src/commands/api/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Make authenticated HTTP requests to Clerk APIs directly from the command line.
By default, targets the Clerk Backend API (`https://api.clerk.dev/v1/`) using
the instance secret key. Use `--platform` to target the Platform API instead.

Works with no login and no linked project on an **unclaimed keyless
Works with no login and no linked project on an **unclaimed accountless
application** — the one an SDK creates for itself the first time you run
`next dev` (or similar) with no keys configured — by reading the secret key it
already left on disk. See [Authentication](#authentication) below.
Expand DownExpand Up@@ -87,13 +87,13 @@ Secret key resolution order (Backend API, the default):

1. `--secret-key` flag (explicit)
2. Auto-resolve from `--app <id>` via the Platform API (see below)
3. This project's own keyless secret key — from `CLERK_SECRET_KEY`, `.env.local`
3. This project's own accountless secret key — from `CLERK_SECRET_KEY`, `.env.local`
(or the framework's detected env var name), or the SDK's own `.clerk/.tmp/keyless.json`
4. Auto-resolve from linked project profile via the Platform API (see below)

Step 2 exchanges a Platform API token for the target instance's secret key and
works from any directory (no `clerk link` required). Step 3 is what makes
`clerk api` work out of the box against an **unclaimed keyless application** —
`clerk api` work out of the box against an **unclaimed accountless application** —
the one an SDK creates for itself on first `next dev` (or similar) with no keys
configured — with no login and no Platform API auth at all; it only applies when
the directory isn't linked and `--app` wasn't passed, since either of those names
Expand Down
8 changes: 4 additions & 4 deletions packages/cli-core/src/commands/auth/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ Manage authentication with Clerk.

### `clerk auth login` (aliases: `signup`, `signin`, `sign-in`)

Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any keyless application previously created by `clerk init`.
Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any accountless application previously created by `clerk init`.

1. Checks for an existing valid token — if found, prompts to re-authenticate (in agent mode, skips and runs autoclaim immediately)
2. Generates PKCE parameters (code verifier, challenge, state)
Expand All@@ -18,9 +18,9 @@ Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or
8. If this was a re-authentication over an existing session, revokes the previous grant. The outgoing session is read once the authorization code arrives and just before the token exchange replaces it, and revoked only after the replacement is stored — so an abandoned browser flow leaves the original session intact, and a concurrent refresh has the smallest possible window to rotate the token out from under the revocation. A failure here warns rather than failing the login
9. **Autoclaim**: if `.clerk/keyless.json` exists in the current directory, claims the temporary application, links it to the project, and pulls environment variables

#### Keyless autoclaim breadcrumb lifecycle
#### Accountless autoclaim breadcrumb lifecycle

When `clerk init` runs in keyless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:
When `clerk init` runs in accountless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:

- **404** — claim token expired or application already deleted; breadcrumb is cleared and a warning is shown.
- **403** — authenticated account has no active organization; breadcrumb is cleared and a warning is shown.
Expand All@@ -37,7 +37,7 @@ OAuth requests are made against the Clerk OAuth system instance (default `https:
| Token exchange | `POST` | `/oauth/token` | Exchanges authorization code + `code_verifier` for an access token |
| User info | `GET` | `/oauth/userinfo` | Fetches `sub` (user ID) and `email` using the access token |
| Revoke | `POST` | `/oauth/token/revoke` | Revokes the superseded refresh token on re-authentication (RFC 7009) |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims a keyless application by token; returns the full `Application` object |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims an accountless application by token; returns the full `Application` object |

### `clerk auth logout` (aliases: `signout`, `sign-out`)

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/billing/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,9 @@ The handlers are wired to top-level `clerk enable billing` and `clerk disable
billing` commands.

**Requires a claimed application.** Unlike `clerk enable/disable orgs`, these
commands cannot run against an unclaimed keyless application: billing settings
commands cannot run against an unclaimed accountless application: billing settings
exist only in the account-level config document, and Clerk's Backend API exposes
no billing resource an instance secret key could reach. In a keyless project both
no billing resource an instance secret key could reach. In an accountless project both
commands exit with an `auth_required` error pointing at `clerk auth login`.

For arbitrary billing config edits (plans, trials, payment-method requirements)
Expand Down
28 changes: 14 additions & 14 deletions packages/cli-core/src/commands/config/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Manage Clerk instance configuration.
Two modes exist, picked automatically:

- **Account mode** (default) — full instance config document via the Platform API. Used whenever the project is linked or `--app` is passed; requires an account (`clerk auth login` or `CLERK_PLATFORM_API_KEY`).
- **Keyless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Keyless mode](#keyless-mode).
- **Accountless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Accountless mode](#accountless-mode).

## Commands

Expand DownExpand Up@@ -36,7 +36,7 @@ clerk config pull --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -75,7 +75,7 @@ clerk config schema --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand DownExpand Up@@ -116,7 +116,7 @@ clerk config patch --file partial-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -157,7 +157,7 @@ clerk config put --file full-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand All@@ -167,9 +167,9 @@ clerk config put --file full-config.json --dry-run

---

## Keyless mode
## Accountless mode

A keyless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.
An accountless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.

### When it engages

Expand All@@ -178,11 +178,11 @@ Both must hold, otherwise the account-authenticated path runs unchanged:
1. No `--app` was passed.
2. No linked project in the current directory.

**Account credentials are not part of this decision.** Keyless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.
**Account credentials are not part of this decision.** Accountless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.

When credentials _are_ present and the directory simply isn't linked, the command prints a warning that it's using the reduced key-based view and points at `clerk link`, so the narrower output is never a silent surprise.

The secret key is resolved the way the app itself would resolve one, and this order is shared by every keyless-capable command (`lib/keyless-target.ts`):
The secret key is resolved the way the app itself would resolve one, and this order is shared by every accountless-capable command (`lib/keyless-target.ts`):

1. `CLERK_SECRET_KEY`, or the framework's secret key variable (e.g. `NUXT_CLERK_SECRET_KEY`), in the environment
2. `.env`, then `.env.local` — the later file wins
Expand All@@ -192,7 +192,7 @@ A key that doesn't start with `sk_` is rejected. The SDK file comes last because

### Payload shape

The Backend API has no single config document — it exposes independent resources — so keyless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:
The Backend API has no single config document — it exposes independent resources — so accountless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:

```sh
clerk config patch --json '{
Expand DownExpand Up@@ -221,19 +221,19 @@ Any other top-level key exits with a usage error naming the supported ones. Most

### Round-trip verification

A 200 or 204 from a keyless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.
A 200 or 204 from an accountless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.

After a write, the CLI checks every field it sent against the PATCH response body, and against nothing else. Fields whose value round-trips are reported as applied; fields the response doesn't reflect are named explicitly instead of folded into an unconditional success line.

A follow-up GET looks like stronger evidence and is in fact weaker. BAPI omits writable-but-not-readable fields from its reads — `instance.support_email` is accepted and never echoed — and reads are eventually consistent, so a GET issued straight after a write routinely returns the pre-write value. Verifying against one reports perfectly good writes as dropped. This applies equally to the six groups that do have a GET route: the response body is the only read that is guaranteed to be about _this_ write.

That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.
That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and the legacy-named `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.

`restrictions` and `instance_settings` have no GET route, but both echo their new state in the PATCH response, so their writes verify normally.

### Differences from account mode

| Behavior | Account mode | Keyless mode |
| Behavior | Account mode | Accountless mode |
| ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Coverage | Full config document | Seven Backend API resources |
| `--instance` | Selects dev/prod | Usage error — the secret key already targets exactly one instance |
Expand All@@ -244,7 +244,7 @@ That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothi

Run `clerk auth login` to claim the application; auto-claim links it, and every config command then uses account mode with full coverage.

### API Endpoints (keyless mode)
### API Endpoints (accountless mode)

All requests go to the Clerk Backend API (default `https://api.clerk.dev`, overridable via `CLERK_BACKEND_API_URL`), authenticated with a `Bearer sk_…` instance secret key.

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/config/keyless.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,7 +285,7 @@ describe("keyless config", () => {
const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts");

await expect(resolveKeylessTarget({ instance: "prod", cwd: projectDir })).rejects.toThrow(
/--instance is not supported for an unclaimed keyless application/,
/--instance is not supported for an unclaimed accountless application/,
);
});

Expand DownExpand Up@@ -761,7 +761,7 @@ describe("keyless config", () => {

const written = await Bun.file(join(projectDir, ".env.written")).text();
expect(written).toContain(`CLERK_SECRET_KEY=${SECRET_KEY}`);
expect(captured.err).toContain("Keyless application keys");
expect(captured.err).toContain("Accountless application keys");
});

test("enable billing explains that billing needs a claimed application", async () => {
Expand Down
Loading
, '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('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-lions-claim.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk init --accountless` as the canonical flag and keep `--keyless` as a deprecated compatibility alias. `open` and `whoami` agent JSON now emit the canonical `accountless` key alongside the deprecated `keyless` alias.
6 changes: 3 additions & 3 deletions packages/cli-core/src/commands/api/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Make authenticated HTTP requests to Clerk APIs directly from the command line.
By default, targets the Clerk Backend API (`https://api.clerk.dev/v1/`) using
the instance secret key. Use `--platform` to target the Platform API instead.

Works with no login and no linked project on an **unclaimed keyless
Works with no login and no linked project on an **unclaimed accountless
application** — the one an SDK creates for itself the first time you run
`next dev` (or similar) with no keys configured — by reading the secret key it
already left on disk. See [Authentication](#authentication) below.
Expand DownExpand Up@@ -87,13 +87,13 @@ Secret key resolution order (Backend API, the default):

1. `--secret-key` flag (explicit)
2. Auto-resolve from `--app <id>` via the Platform API (see below)
3. This project's own keyless secret key — from `CLERK_SECRET_KEY`, `.env.local`
3. This project's own accountless secret key — from `CLERK_SECRET_KEY`, `.env.local`
(or the framework's detected env var name), or the SDK's own `.clerk/.tmp/keyless.json`
4. Auto-resolve from linked project profile via the Platform API (see below)

Step 2 exchanges a Platform API token for the target instance's secret key and
works from any directory (no `clerk link` required). Step 3 is what makes
`clerk api` work out of the box against an **unclaimed keyless application** —
`clerk api` work out of the box against an **unclaimed accountless application** —
the one an SDK creates for itself on first `next dev` (or similar) with no keys
configured — with no login and no Platform API auth at all; it only applies when
the directory isn't linked and `--app` wasn't passed, since either of those names
Expand Down
8 changes: 4 additions & 4 deletions packages/cli-core/src/commands/auth/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ Manage authentication with Clerk.

### `clerk auth login` (aliases: `signup`, `signin`, `sign-in`)

Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any keyless application previously created by `clerk init`.
Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any accountless application previously created by `clerk init`.

1. Checks for an existing valid token — if found, prompts to re-authenticate (in agent mode, skips and runs autoclaim immediately)
2. Generates PKCE parameters (code verifier, challenge, state)
Expand All@@ -18,9 +18,9 @@ Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or
8. If this was a re-authentication over an existing session, revokes the previous grant. The outgoing session is read once the authorization code arrives and just before the token exchange replaces it, and revoked only after the replacement is stored — so an abandoned browser flow leaves the original session intact, and a concurrent refresh has the smallest possible window to rotate the token out from under the revocation. A failure here warns rather than failing the login
9. **Autoclaim**: if `.clerk/keyless.json` exists in the current directory, claims the temporary application, links it to the project, and pulls environment variables

#### Keyless autoclaim breadcrumb lifecycle
#### Accountless autoclaim breadcrumb lifecycle

When `clerk init` runs in keyless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:
When `clerk init` runs in accountless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:

- **404** — claim token expired or application already deleted; breadcrumb is cleared and a warning is shown.
- **403** — authenticated account has no active organization; breadcrumb is cleared and a warning is shown.
Expand All@@ -37,7 +37,7 @@ OAuth requests are made against the Clerk OAuth system instance (default `https:
| Token exchange | `POST` | `/oauth/token` | Exchanges authorization code + `code_verifier` for an access token |
| User info | `GET` | `/oauth/userinfo` | Fetches `sub` (user ID) and `email` using the access token |
| Revoke | `POST` | `/oauth/token/revoke` | Revokes the superseded refresh token on re-authentication (RFC 7009) |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims a keyless application by token; returns the full `Application` object |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims an accountless application by token; returns the full `Application` object |

### `clerk auth logout` (aliases: `signout`, `sign-out`)

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/billing/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,9 @@ The handlers are wired to top-level `clerk enable billing` and `clerk disable
billing` commands.

**Requires a claimed application.** Unlike `clerk enable/disable orgs`, these
commands cannot run against an unclaimed keyless application: billing settings
commands cannot run against an unclaimed accountless application: billing settings
exist only in the account-level config document, and Clerk's Backend API exposes
no billing resource an instance secret key could reach. In a keyless project both
no billing resource an instance secret key could reach. In an accountless project both
commands exit with an `auth_required` error pointing at `clerk auth login`.

For arbitrary billing config edits (plans, trials, payment-method requirements)
Expand Down
28 changes: 14 additions & 14 deletions packages/cli-core/src/commands/config/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Manage Clerk instance configuration.
Two modes exist, picked automatically:

- **Account mode** (default) — full instance config document via the Platform API. Used whenever the project is linked or `--app` is passed; requires an account (`clerk auth login` or `CLERK_PLATFORM_API_KEY`).
- **Keyless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Keyless mode](#keyless-mode).
- **Accountless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Accountless mode](#accountless-mode).

## Commands

Expand DownExpand Up@@ -36,7 +36,7 @@ clerk config pull --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -75,7 +75,7 @@ clerk config schema --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand DownExpand Up@@ -116,7 +116,7 @@ clerk config patch --file partial-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -157,7 +157,7 @@ clerk config put --file full-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand All@@ -167,9 +167,9 @@ clerk config put --file full-config.json --dry-run

---

## Keyless mode
## Accountless mode

A keyless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.
An accountless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.

### When it engages

Expand All@@ -178,11 +178,11 @@ Both must hold, otherwise the account-authenticated path runs unchanged:
1. No `--app` was passed.
2. No linked project in the current directory.

**Account credentials are not part of this decision.** Keyless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.
**Account credentials are not part of this decision.** Accountless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.

When credentials _are_ present and the directory simply isn't linked, the command prints a warning that it's using the reduced key-based view and points at `clerk link`, so the narrower output is never a silent surprise.

The secret key is resolved the way the app itself would resolve one, and this order is shared by every keyless-capable command (`lib/keyless-target.ts`):
The secret key is resolved the way the app itself would resolve one, and this order is shared by every accountless-capable command (`lib/keyless-target.ts`):

1. `CLERK_SECRET_KEY`, or the framework's secret key variable (e.g. `NUXT_CLERK_SECRET_KEY`), in the environment
2. `.env`, then `.env.local` — the later file wins
Expand All@@ -192,7 +192,7 @@ A key that doesn't start with `sk_` is rejected. The SDK file comes last because

### Payload shape

The Backend API has no single config document — it exposes independent resources — so keyless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:
The Backend API has no single config document — it exposes independent resources — so accountless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:

```sh
clerk config patch --json '{
Expand DownExpand Up@@ -221,19 +221,19 @@ Any other top-level key exits with a usage error naming the supported ones. Most

### Round-trip verification

A 200 or 204 from a keyless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.
A 200 or 204 from an accountless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.

After a write, the CLI checks every field it sent against the PATCH response body, and against nothing else. Fields whose value round-trips are reported as applied; fields the response doesn't reflect are named explicitly instead of folded into an unconditional success line.

A follow-up GET looks like stronger evidence and is in fact weaker. BAPI omits writable-but-not-readable fields from its reads — `instance.support_email` is accepted and never echoed — and reads are eventually consistent, so a GET issued straight after a write routinely returns the pre-write value. Verifying against one reports perfectly good writes as dropped. This applies equally to the six groups that do have a GET route: the response body is the only read that is guaranteed to be about _this_ write.

That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.
That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and the legacy-named `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.

`restrictions` and `instance_settings` have no GET route, but both echo their new state in the PATCH response, so their writes verify normally.

### Differences from account mode

| Behavior | Account mode | Keyless mode |
| Behavior | Account mode | Accountless mode |
| ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Coverage | Full config document | Seven Backend API resources |
| `--instance` | Selects dev/prod | Usage error — the secret key already targets exactly one instance |
Expand All@@ -244,7 +244,7 @@ That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothi

Run `clerk auth login` to claim the application; auto-claim links it, and every config command then uses account mode with full coverage.

### API Endpoints (keyless mode)
### API Endpoints (accountless mode)

All requests go to the Clerk Backend API (default `https://api.clerk.dev`, overridable via `CLERK_BACKEND_API_URL`), authenticated with a `Bearer sk_…` instance secret key.

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/config/keyless.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,7 +285,7 @@ describe("keyless config", () => {
const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts");

await expect(resolveKeylessTarget({ instance: "prod", cwd: projectDir })).rejects.toThrow(
/--instance is not supported for an unclaimed keyless application/,
/--instance is not supported for an unclaimed accountless application/,
);
});

Expand DownExpand Up@@ -761,7 +761,7 @@ describe("keyless config", () => {

const written = await Bun.file(join(projectDir, ".env.written")).text();
expect(written).toContain(`CLERK_SECRET_KEY=${SECRET_KEY}`);
expect(captured.err).toContain("Keyless application keys");
expect(captured.err).toContain("Accountless application keys");
});

test("enable billing explains that billing needs a claimed application", async () => {
Expand Down
Loading
, '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); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-lions-claim.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk init --accountless` as the canonical flag and keep `--keyless` as a deprecated compatibility alias. `open` and `whoami` agent JSON now emit the canonical `accountless` key alongside the deprecated `keyless` alias.
6 changes: 3 additions & 3 deletions packages/cli-core/src/commands/api/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Make authenticated HTTP requests to Clerk APIs directly from the command line.
By default, targets the Clerk Backend API (`https://api.clerk.dev/v1/`) using
the instance secret key. Use `--platform` to target the Platform API instead.

Works with no login and no linked project on an **unclaimed keyless
Works with no login and no linked project on an **unclaimed accountless
application** — the one an SDK creates for itself the first time you run
`next dev` (or similar) with no keys configured — by reading the secret key it
already left on disk. See [Authentication](#authentication) below.
Expand DownExpand Up@@ -87,13 +87,13 @@ Secret key resolution order (Backend API, the default):

1. `--secret-key` flag (explicit)
2. Auto-resolve from `--app <id>` via the Platform API (see below)
3. This project's own keyless secret key — from `CLERK_SECRET_KEY`, `.env.local`
3. This project's own accountless secret key — from `CLERK_SECRET_KEY`, `.env.local`
(or the framework's detected env var name), or the SDK's own `.clerk/.tmp/keyless.json`
4. Auto-resolve from linked project profile via the Platform API (see below)

Step 2 exchanges a Platform API token for the target instance's secret key and
works from any directory (no `clerk link` required). Step 3 is what makes
`clerk api` work out of the box against an **unclaimed keyless application** —
`clerk api` work out of the box against an **unclaimed accountless application** —
the one an SDK creates for itself on first `next dev` (or similar) with no keys
configured — with no login and no Platform API auth at all; it only applies when
the directory isn't linked and `--app` wasn't passed, since either of those names
Expand Down
8 changes: 4 additions & 4 deletions packages/cli-core/src/commands/auth/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ Manage authentication with Clerk.

### `clerk auth login` (aliases: `signup`, `signin`, `sign-in`)

Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any keyless application previously created by `clerk init`.
Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any accountless application previously created by `clerk init`.

1. Checks for an existing valid token — if found, prompts to re-authenticate (in agent mode, skips and runs autoclaim immediately)
2. Generates PKCE parameters (code verifier, challenge, state)
Expand All@@ -18,9 +18,9 @@ Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or
8. If this was a re-authentication over an existing session, revokes the previous grant. The outgoing session is read once the authorization code arrives and just before the token exchange replaces it, and revoked only after the replacement is stored — so an abandoned browser flow leaves the original session intact, and a concurrent refresh has the smallest possible window to rotate the token out from under the revocation. A failure here warns rather than failing the login
9. **Autoclaim**: if `.clerk/keyless.json` exists in the current directory, claims the temporary application, links it to the project, and pulls environment variables

#### Keyless autoclaim breadcrumb lifecycle
#### Accountless autoclaim breadcrumb lifecycle

When `clerk init` runs in keyless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:
When `clerk init` runs in accountless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:

- **404** — claim token expired or application already deleted; breadcrumb is cleared and a warning is shown.
- **403** — authenticated account has no active organization; breadcrumb is cleared and a warning is shown.
Expand All@@ -37,7 +37,7 @@ OAuth requests are made against the Clerk OAuth system instance (default `https:
| Token exchange | `POST` | `/oauth/token` | Exchanges authorization code + `code_verifier` for an access token |
| User info | `GET` | `/oauth/userinfo` | Fetches `sub` (user ID) and `email` using the access token |
| Revoke | `POST` | `/oauth/token/revoke` | Revokes the superseded refresh token on re-authentication (RFC 7009) |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims a keyless application by token; returns the full `Application` object |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims an accountless application by token; returns the full `Application` object |

### `clerk auth logout` (aliases: `signout`, `sign-out`)

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/billing/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,9 @@ The handlers are wired to top-level `clerk enable billing` and `clerk disable
billing` commands.

**Requires a claimed application.** Unlike `clerk enable/disable orgs`, these
commands cannot run against an unclaimed keyless application: billing settings
commands cannot run against an unclaimed accountless application: billing settings
exist only in the account-level config document, and Clerk's Backend API exposes
no billing resource an instance secret key could reach. In a keyless project both
no billing resource an instance secret key could reach. In an accountless project both
commands exit with an `auth_required` error pointing at `clerk auth login`.

For arbitrary billing config edits (plans, trials, payment-method requirements)
Expand Down
28 changes: 14 additions & 14 deletions packages/cli-core/src/commands/config/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Manage Clerk instance configuration.
Two modes exist, picked automatically:

- **Account mode** (default) — full instance config document via the Platform API. Used whenever the project is linked or `--app` is passed; requires an account (`clerk auth login` or `CLERK_PLATFORM_API_KEY`).
- **Keyless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Keyless mode](#keyless-mode).
- **Accountless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Accountless mode](#accountless-mode).

## Commands

Expand DownExpand Up@@ -36,7 +36,7 @@ clerk config pull --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -75,7 +75,7 @@ clerk config schema --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand DownExpand Up@@ -116,7 +116,7 @@ clerk config patch --file partial-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -157,7 +157,7 @@ clerk config put --file full-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand All@@ -167,9 +167,9 @@ clerk config put --file full-config.json --dry-run

---

## Keyless mode
## Accountless mode

A keyless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.
An accountless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.

### When it engages

Expand All@@ -178,11 +178,11 @@ Both must hold, otherwise the account-authenticated path runs unchanged:
1. No `--app` was passed.
2. No linked project in the current directory.

**Account credentials are not part of this decision.** Keyless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.
**Account credentials are not part of this decision.** Accountless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.

When credentials _are_ present and the directory simply isn't linked, the command prints a warning that it's using the reduced key-based view and points at `clerk link`, so the narrower output is never a silent surprise.

The secret key is resolved the way the app itself would resolve one, and this order is shared by every keyless-capable command (`lib/keyless-target.ts`):
The secret key is resolved the way the app itself would resolve one, and this order is shared by every accountless-capable command (`lib/keyless-target.ts`):

1. `CLERK_SECRET_KEY`, or the framework's secret key variable (e.g. `NUXT_CLERK_SECRET_KEY`), in the environment
2. `.env`, then `.env.local` — the later file wins
Expand All@@ -192,7 +192,7 @@ A key that doesn't start with `sk_` is rejected. The SDK file comes last because

### Payload shape

The Backend API has no single config document — it exposes independent resources — so keyless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:
The Backend API has no single config document — it exposes independent resources — so accountless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:

```sh
clerk config patch --json '{
Expand DownExpand Up@@ -221,19 +221,19 @@ Any other top-level key exits with a usage error naming the supported ones. Most

### Round-trip verification

A 200 or 204 from a keyless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.
A 200 or 204 from an accountless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.

After a write, the CLI checks every field it sent against the PATCH response body, and against nothing else. Fields whose value round-trips are reported as applied; fields the response doesn't reflect are named explicitly instead of folded into an unconditional success line.

A follow-up GET looks like stronger evidence and is in fact weaker. BAPI omits writable-but-not-readable fields from its reads — `instance.support_email` is accepted and never echoed — and reads are eventually consistent, so a GET issued straight after a write routinely returns the pre-write value. Verifying against one reports perfectly good writes as dropped. This applies equally to the six groups that do have a GET route: the response body is the only read that is guaranteed to be about _this_ write.

That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.
That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and the legacy-named `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.

`restrictions` and `instance_settings` have no GET route, but both echo their new state in the PATCH response, so their writes verify normally.

### Differences from account mode

| Behavior | Account mode | Keyless mode |
| Behavior | Account mode | Accountless mode |
| ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Coverage | Full config document | Seven Backend API resources |
| `--instance` | Selects dev/prod | Usage error — the secret key already targets exactly one instance |
Expand All@@ -244,7 +244,7 @@ That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothi

Run `clerk auth login` to claim the application; auto-claim links it, and every config command then uses account mode with full coverage.

### API Endpoints (keyless mode)
### API Endpoints (accountless mode)

All requests go to the Clerk Backend API (default `https://api.clerk.dev`, overridable via `CLERK_BACKEND_API_URL`), authenticated with a `Bearer sk_…` instance secret key.

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/config/keyless.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,7 +285,7 @@ describe("keyless config", () => {
const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts");

await expect(resolveKeylessTarget({ instance: "prod", cwd: projectDir })).rejects.toThrow(
/--instance is not supported for an unclaimed keyless application/,
/--instance is not supported for an unclaimed accountless application/,
);
});

Expand DownExpand Up@@ -761,7 +761,7 @@ describe("keyless config", () => {

const written = await Bun.file(join(projectDir, ".env.written")).text();
expect(written).toContain(`CLERK_SECRET_KEY=${SECRET_KEY}`);
expect(captured.err).toContain("Keyless application keys");
expect(captured.err).toContain("Accountless application keys");
});

test("enable billing explains that billing needs a claimed application", async () => {
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-lions-claim.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
---
"clerk": minor
---

Add `clerk init --accountless` as the canonical flag and keep `--keyless` as a deprecated compatibility alias. `open` and `whoami` agent JSON now emit the canonical `accountless` key alongside the deprecated `keyless` alias.
6 changes: 3 additions & 3 deletions packages/cli-core/src/commands/api/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Make authenticated HTTP requests to Clerk APIs directly from the command line.
By default, targets the Clerk Backend API (`https://api.clerk.dev/v1/`) using
the instance secret key. Use `--platform` to target the Platform API instead.

Works with no login and no linked project on an **unclaimed keyless
Works with no login and no linked project on an **unclaimed accountless
application** — the one an SDK creates for itself the first time you run
`next dev` (or similar) with no keys configured — by reading the secret key it
already left on disk. See [Authentication](#authentication) below.
Expand DownExpand Up@@ -87,13 +87,13 @@ Secret key resolution order (Backend API, the default):

1. `--secret-key` flag (explicit)
2. Auto-resolve from `--app <id>` via the Platform API (see below)
3. This project's own keyless secret key — from `CLERK_SECRET_KEY`, `.env.local`
3. This project's own accountless secret key — from `CLERK_SECRET_KEY`, `.env.local`
(or the framework's detected env var name), or the SDK's own `.clerk/.tmp/keyless.json`
4. Auto-resolve from linked project profile via the Platform API (see below)

Step 2 exchanges a Platform API token for the target instance's secret key and
works from any directory (no `clerk link` required). Step 3 is what makes
`clerk api` work out of the box against an **unclaimed keyless application** —
`clerk api` work out of the box against an **unclaimed accountless application** —
the one an SDK creates for itself on first `next dev` (or similar) with no keys
configured — with no login and no Platform API auth at all; it only applies when
the directory isn't linked and `--app` wasn't passed, since either of those names
Expand Down
8 changes: 4 additions & 4 deletions packages/cli-core/src/commands/auth/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ Manage authentication with Clerk.

### `clerk auth login` (aliases: `signup`, `signin`, `sign-in`)

Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any keyless application previously created by `clerk init`.
Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any accountless application previously created by `clerk init`.

1. Checks for an existing valid token — if found, prompts to re-authenticate (in agent mode, skips and runs autoclaim immediately)
2. Generates PKCE parameters (code verifier, challenge, state)
Expand All@@ -18,9 +18,9 @@ Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or
8. If this was a re-authentication over an existing session, revokes the previous grant. The outgoing session is read once the authorization code arrives and just before the token exchange replaces it, and revoked only after the replacement is stored — so an abandoned browser flow leaves the original session intact, and a concurrent refresh has the smallest possible window to rotate the token out from under the revocation. A failure here warns rather than failing the login
9. **Autoclaim**: if `.clerk/keyless.json` exists in the current directory, claims the temporary application, links it to the project, and pulls environment variables

#### Keyless autoclaim breadcrumb lifecycle
#### Accountless autoclaim breadcrumb lifecycle

When `clerk init` runs in keyless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:
When `clerk init` runs in accountless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`:

- **404** — claim token expired or application already deleted; breadcrumb is cleared and a warning is shown.
- **403** — authenticated account has no active organization; breadcrumb is cleared and a warning is shown.
Expand All@@ -37,7 +37,7 @@ OAuth requests are made against the Clerk OAuth system instance (default `https:
| Token exchange | `POST` | `/oauth/token` | Exchanges authorization code + `code_verifier` for an access token |
| User info | `GET` | `/oauth/userinfo` | Fetches `sub` (user ID) and `email` using the access token |
| Revoke | `POST` | `/oauth/token/revoke` | Revokes the superseded refresh token on re-authentication (RFC 7009) |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims a keyless application by token; returns the full `Application` object |
| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims an accountless application by token; returns the full `Application` object |

### `clerk auth logout` (aliases: `signout`, `sign-out`)

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/billing/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,9 +5,9 @@ The handlers are wired to top-level `clerk enable billing` and `clerk disable
billing` commands.

**Requires a claimed application.** Unlike `clerk enable/disable orgs`, these
commands cannot run against an unclaimed keyless application: billing settings
commands cannot run against an unclaimed accountless application: billing settings
exist only in the account-level config document, and Clerk's Backend API exposes
no billing resource an instance secret key could reach. In a keyless project both
no billing resource an instance secret key could reach. In an accountless project both
commands exit with an `auth_required` error pointing at `clerk auth login`.

For arbitrary billing config edits (plans, trials, payment-method requirements)
Expand Down
28 changes: 14 additions & 14 deletions packages/cli-core/src/commands/config/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,7 @@ Manage Clerk instance configuration.
Two modes exist, picked automatically:

- **Account mode** (default) — full instance config document via the Platform API. Used whenever the project is linked or `--app` is passed; requires an account (`clerk auth login` or `CLERK_PLATFORM_API_KEY`).
- **Keyless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Keyless mode](#keyless-mode).
- **Accountless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Accountless mode](#accountless-mode).

## Commands

Expand DownExpand Up@@ -36,7 +36,7 @@ clerk config pull --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -75,7 +75,7 @@ clerk config schema --keys auth_email session
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand DownExpand Up@@ -116,7 +116,7 @@ clerk config patch --file partial-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account
- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account

#### API Endpoints

Expand DownExpand Up@@ -157,7 +157,7 @@ clerk config put --file full-config.json --dry-run
- a linked Clerk project in the current directory, or
- `--app <id>` to target an application directly
- Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [keyless mode](#keyless-mode))
- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [accountless mode](#accountless-mode))

#### API Endpoints

Expand All@@ -167,9 +167,9 @@ clerk config put --file full-config.json --dry-run

---

## Keyless mode
## Accountless mode

A keyless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.
An accountless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in.

### When it engages

Expand All@@ -178,11 +178,11 @@ Both must hold, otherwise the account-authenticated path runs unchanged:
1. No `--app` was passed.
2. No linked project in the current directory.

**Account credentials are not part of this decision.** Keyless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.
**Account credentials are not part of this decision.** Accountless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to.

When credentials _are_ present and the directory simply isn't linked, the command prints a warning that it's using the reduced key-based view and points at `clerk link`, so the narrower output is never a silent surprise.

The secret key is resolved the way the app itself would resolve one, and this order is shared by every keyless-capable command (`lib/keyless-target.ts`):
The secret key is resolved the way the app itself would resolve one, and this order is shared by every accountless-capable command (`lib/keyless-target.ts`):

1. `CLERK_SECRET_KEY`, or the framework's secret key variable (e.g. `NUXT_CLERK_SECRET_KEY`), in the environment
2. `.env`, then `.env.local` — the later file wins
Expand All@@ -192,7 +192,7 @@ A key that doesn't start with `sk_` is rejected. The SDK file comes last because

### Payload shape

The Backend API has no single config document — it exposes independent resources — so keyless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:
The Backend API has no single config document — it exposes independent resources — so accountless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint:

```sh
clerk config patch --json '{
Expand DownExpand Up@@ -221,19 +221,19 @@ Any other top-level key exits with a usage error naming the supported ones. Most

### Round-trip verification

A 200 or 204 from a keyless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.
A 200 or 204 from an accountless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both.

After a write, the CLI checks every field it sent against the PATCH response body, and against nothing else. Fields whose value round-trips are reported as applied; fields the response doesn't reflect are named explicitly instead of folded into an unconditional success line.

A follow-up GET looks like stronger evidence and is in fact weaker. BAPI omits writable-but-not-readable fields from its reads — `instance.support_email` is accepted and never echoed — and reads are eventually consistent, so a GET issued straight after a write routinely returns the pre-write value. Verifying against one reports perfectly good writes as dropped. This applies equally to the six groups that do have a GET route: the response body is the only read that is guaranteed to be about _this_ write.

That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.
That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and the legacy-named `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read.

`restrictions` and `instance_settings` have no GET route, but both echo their new state in the PATCH response, so their writes verify normally.

### Differences from account mode

| Behavior | Account mode | Keyless mode |
| Behavior | Account mode | Accountless mode |
| ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Coverage | Full config document | Seven Backend API resources |
| `--instance` | Selects dev/prod | Usage error — the secret key already targets exactly one instance |
Expand All@@ -244,7 +244,7 @@ That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothi

Run `clerk auth login` to claim the application; auto-claim links it, and every config command then uses account mode with full coverage.

### API Endpoints (keyless mode)
### API Endpoints (accountless mode)

All requests go to the Clerk Backend API (default `https://api.clerk.dev`, overridable via `CLERK_BACKEND_API_URL`), authenticated with a `Bearer sk_…` instance secret key.

Expand Down
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/config/keyless.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -285,7 +285,7 @@ describe("keyless config", () => {
const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts");

await expect(resolveKeylessTarget({ instance: "prod", cwd: projectDir })).rejects.toThrow(
/--instance is not supported for an unclaimed keyless application/,
/--instance is not supported for an unclaimed accountless application/,
);
});

Expand DownExpand Up@@ -761,7 +761,7 @@ describe("keyless config", () => {

const written = await Bun.file(join(projectDir, ".env.written")).text();
expect(written).toContain(`CLERK_SECRET_KEY=${SECRET_KEY}`);
expect(captured.err).toContain("Keyless application keys");
expect(captured.err).toContain("Accountless application keys");
});

test("enable billing explains that billing needs a claimed application", async () => {
Expand Down
Loading