Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/grow-1285-link-claim-and-advice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"clerk": minor
---

Make `clerk link` claim the application a signed-out `clerk init` created and link the folder to it, and make `clerk deploy` and `clerk deploy status` say how to link a folder that runs on an unclaimed application. If the claim fails with an error that keeps the claim token, or the workspace is managed by an integration, `clerk link` now stops with the error and says whether to rerun it or link another app with `--app`, instead of offering apps that can't include the claimed one. If the folder's publishable key belongs to a different application, `clerk link` and `clerk auth login` claim the application but leave the folder unlinked and its env files unchanged, and say which file holds the other key.

- `clerk auth login`'s claim-failure messages now say why the claim failed and what to do, and `clerk init`, `clerk migrate` and `clerk doctor` print them too when they sign you in.
15 changes: 9 additions & 6 deletions packages/cli-core/src/commands/auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,19 @@ Before signing in, the command prints a banner: the Clerk logo beside a link to
6. Exchanges the code for an access token
7. Stores the token and user info in local config
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
9. **Autoclaim**: if `.clerk/keyless.json` exists in the current directory, claims the temporary application, links it to the project, and pulls environment variables. `clerk link`'s claim step signs in through the same `login()` with claiming turned off (`claim: false`) and then claims the application itself, so it can act on the result. A signed-out `clerk link --app` or relink signs in with claiming on, so its sign-in claims here

#### Accountless autoclaim breadcrumb lifecycle

When `clerk init` runs in accountless 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` (or `clerk link`, which runs the same claim):

- **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.
- **Any other error** — treated as transient; breadcrumb is preserved so the next login retries.
- **Success** — application is claimed and linked, `.env` is updated via `clerk env pull`, breadcrumb is deleted. The command prints the Dashboard URL of the claimed app's development instance, since this is the first time the app has a home in an account.
- **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.
- **Any other error** (a Platform API error or a network failure): breadcrumb is preserved, so the next `clerk link` or `clerk auth login` claims again. A 5xx, a 429 or a network failure may pass on that retry, so the warning says to rerun `clerk link`. A 401, or a saved session that can no longer be refreshed, means Clerk rejected the saved sign-in, so the warning says to run `clerk auth login` to sign in again and then rerun `clerk link`. With `CLERK_PLATFORM_API_KEY` set, the CLI uses that key before any saved sign-in, so a 401 means the key was rejected and the warning says to fix or unset it. Any other 4xx will fail the same way on every retry, so the warning says Clerk rejected the claim, with the status and reason, and to run `clerk link --app <app_id>` with the application the folder should use. `clerk auth login` prints that advice as a warning and exits 0; `clerk link` stops with the original error and the same advice, and also stops for a managed workspace, because its app list can't show the application the token would claim.
- **Publishable key for another application**: the claim succeeds, but a publishable key for a different application is in the framework's publishable key variable (for example `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`), either exported or in a development env file (`.env.development.local`, `.env.local`, `.env.development`, `.env`). `clerk env pull` writes `.env.development.local`, `.env.local` or `.env`; `.env.development` is checked because it outranks `.env` in development, so a different key there would override the pulled one. Each file is read on its own, and values are compared as a framework reads them: trimmed, unquoted, and without an inline comment. The folder is not linked and its env files are not changed, because pulling would pair that key with the claimed app's secret key. Only the `clerk auth login` command (and its `clerk login` alias) and `clerk link`'s claim step check keys; `clerk init`, `clerk migrate`, the `clerk doctor` fix, and signed-out `clerk link --app` and relink flows call `login()` without the check and link whatever they claim. The breadcrumb is deleted, since the token is spent. `clerk auth login` prints a warning naming the claimed app and the file, and exits 0; its next step is `clerk link --app <app_id>`, with no `clerk env pull`. `clerk link` prints the same text as a `key_pair_mismatch` error and exits 1. To finish, run `clerk link --app <app_id>` with the application the folder should use.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
- **Unreadable env file**: the claim succeeds. A different application's key in the exported variable or any readable file still stops it as above. Otherwise the folder is linked, but no keys are written, because the unread file could hold another application's key. A warning names the file and the reason and says to run `clerk env pull` once the file is fixed.
- **Claimed but not linked**: saving the link failed, or the application has no development instance. The command prints "Claimed application: ..." and a warning with the reason, and says to run `clerk link` to finish setup.
- **Success**: application is claimed and linked, `.env` is updated via `clerk env pull`, breadcrumb is deleted. The command prints the Dashboard URL of the claimed app's development instance, since this is the first time the app has a home in an account.

#### API Endpoints

Expand Down
32 changes: 32 additions & 0 deletions packages/cli-core/src/commands/auth/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { afterEach, describe, expect, mock, test } from "bun:test";
import { Command } from "@commander-js/extra-typings";
import "../../lib/help.ts";

const mockLogin = mock();
mock.module("./login.ts", () => ({
login: (...args: unknown[]) => mockLogin(...args),
}));
mock.module("./logout.ts", () => ({ logout: async () => {} }));
mock.module("../../lib/banner.ts", () => ({ printBanner: () => {} }));

const { registerAuth } = await import("./index.ts");

describe("registerAuth", () => {
afterEach(() => {
mockLogin.mockReset();
});

// Only these commands check the folder's keys after a claim; `clerk init`, `clerk migrate`
// and `clerk doctor` call `login()` without the option.
test.each([[["auth", "login"]], [["login"]]])(
"`clerk %p` signs in with the claim key check on",
async (args) => {
const program = new Command();
registerAuth(program as never);

await program.parseAsync(args, { from: "user" });

expect(mockLogin).toHaveBeenCalledWith(expect.objectContaining({ checkClaimKeys: true }));
},
);
});
4 changes: 2 additions & 2 deletions packages/cli-core/src/commands/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function registerAuth(program: Program): void {
])
.action(async (opts) => {
printBanner();
await login(opts);
await login({ ...opts, checkClaimKeys: true });
});

auth
Expand All @@ -42,7 +42,7 @@ export function registerAuth(program: Program): void {
.option("-y, --yes", "Proceed with OAuth without prompting when already logged in")
.action(async (opts) => {
printBanner();
await login(opts);
await login({ ...opts, checkClaimKeys: true });
});

program
Expand Down
Loading
Loading