Skip to content

Make clerk link claim the app a signed-out clerk init created - #600

Open
shane-kercheval wants to merge 15 commits into
mainfrom
shane/grow-1285-link-claim-and-advice
Open

shane-kercheval wants to merge 15 commits into
mainfrom
shane/grow-1285-link-claim-and-advice

Conversation

@shane-kercheval

@shane-kercheval shane-kercheval commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does and why

Before this PR. clerk init run without signing in creates a Clerk app that no account owns and writes its keys into the project. That part is unchanged by this PR. The bug: clerk deploy in that project fails with "No Clerk project linked to this directory. Run clerk link", but clerk link can't help a person who is already signed in. It shows a picker of the apps their account owns, and this app has no owner, so it isn't in the list. An agent gets "Cannot select an application in agent mode. Pass --app ". A signed-out person is the exception: their sign-in claims the app before the picker opens. Either way the project ends up linked to the wrong app or not linked at all.

After this PR:

  • clerk link in that project claims the app into your account, links the project to it, and writes the app's keys. If an env file can't be read, it links but writes no keys, and says "Run clerk env pull once the file is fixed." No picker, no prompt. It prints "Claimed and linked application: <name>" and the app's Dashboard URL.
  • clerk deploy and clerk deploy status in that project still fail with error code not_linked, but the message now says: "This folder holds a claim token a signed-out clerk init wrote to .clerk/keyless.json. If no account owns that application yet, clerk link claims it into your account. Run clerk link to link this folder, then rerun clerk deploy." For clerk deploy status, the message ends "then rerun clerk deploy status."
  • A project whose app was created by a Clerk SDK, not clerk init (the SDK writes a claim link to .clerk/.tmp/keyless.json), gets a different message: a person must claim the app with clerk open first, then run clerk link. The CLI can't claim those itself.
  • If the project's env file holds a publishable key from a different app, clerk link claims the app but refuses to link the project or touch the env file, and says to run clerk link --app <app_id>. clerk auth login, which already claimed apps this way, now refuses the same way. Before, it wrote the claimed app's keys over the other app's.
  • If the claim fails and the claim token is still usable (a server error, a rejected sign-in, a workspace that an integration like Vercel owns), clerk link exits with an error that says what to do. Before, it showed the picker, which couldn't contain the right app.
  • clerk open in a project whose app a Clerk SDK created passes the claim link from .clerk/.tmp/keyless.json to the browser in its parsed form, which turns a " or a space in the link into %22 or %20. Before, it passed the file's raw text, and on Windows a " in that text could end the launcher's command line. The https-only check is unchanged.

clerk init, clerk migrate, and clerk doctor pick apps and write keys exactly as before. When their sign-in claims an app, they print clerk auth login's claim messages, which now say why a claim failed and what to do. Before, every failure said "Auto-claim failed due to a temporary error. It will be retried on your next clerk auth login."

Decisions

  • clerk link claims without asking.
    • The project already runs on that app's keys. Anyone who wants a different app passes --app. The ticket asks for a yes/no prompt before claiming; left out to keep this PR small.
  • When the claim fails and the claim token is still usable, clerk link stops instead of showing the picker. A token Clerk reports as no longer valid is deleted, and clerk link continues as on main.
    • The picker can't contain the right app, so showing it is how the wrong app gets linked. The first version showed the picker; review changed it.
  • The key check reads .env.development.local, .env.local, .env.development, .env, and the shell variable, where the CLI's existing key reader reads only .env and .env.local. It also reads lines the CLI's env parser skips (Windows line endings, indented lines, spaces around =).
    • clerk env pull writes .env.development.local first when that file exists, so a check that skipped it would overwrite keys there. The first version used the existing reader; review found both gaps. The env parser itself is unchanged.
  • The check compares publishable keys only.
    • A reviewer proposed comparing secret keys too. An instance can have several active secret keys, so a different secret key proves nothing.
  • The check runs in clerk link and clerk auth login only.
    • clerk init, clerk migrate, and clerk doctor choose the app and write keys themselves. A refusal in the middle of their sign-in would leave them half done.
  • A signed-out clerk link --app <id> still claims the project's clerk init app during sign-in, as on main.
    • Skipping that claim would leave the claim token behind for the next clerk auth login to claim and relink.

Notes for the reviewer

  • The key check (checkLocalKeys in lib/autoclaim.ts) is where a bug hurts most. A missed key pairs one app's secret key with another app's publishable key, the exact thing the check exists to prevent. A false match blocks a correct link, and the only way past it is --app. Known gap: it reads KEY=value lines as clerk init and clerk env pull write them, not every line dotenv accepts. A KEY: value assignment is skipped, and a #comment with no space before it or a backtick-quoted value is compared as part of the key.
  • Not tested by hand: the message for an app a Clerk SDK created. Unit tests only.
  • Not changed: an agent with no saved session that runs clerk link still gets the browser sign-in and its 15-minute wait, as on main. The new deploy message sends agents to clerk link, so this is the next gap.
  • The e2e suite runs clerk init for each framework, clerk link --app, clerk migrate, and API, config and users commands, but never a signed-out clerk init followed by a claim, so CI does not exercise the claim.

How it was tested

  • bun run format:check, bun run lint, bun run typecheck, and bun run test pass at 837f811: 6,560 tests, 0 failures (my run). CI's Lint, Test, and E2E Test jobs pass at 0468dc1, the current head. The new tests were not run against main; they test a claim that main's clerk link never makes, so I expect them to fail there.
  • Ran the built CLI against a personal Clerk account in two scratch folders, every command in agent mode:
    • Folder A: clerk init while signed out, then clerk deploy printed the not_linked message quoted above. After signing in, clerk link printed "Claimed and linked application: handson-app-a" and the Dashboard URL.
    • Folder B: clerk init while signed out, then another app's publishable key written into .env.local, then clerk link printed:
      {"error":{"code":"key_pair_mismatch","message":"Claimed application `handson-app-b` (app_...) into your account, but .env.local holds a publishable key for a different application, so this folder wasn't linked and its keys weren't changed. Run `clerk link --app <app_id>` with the application this folder should use."}}
      
      The project stayed unlinked and .env.local was unchanged.

Drafted by Claude, reviewed by Shane

- `clerk deploy` and `clerk deploy status` now pick their `not_linked` message by what the folder holds: a claim token from a signed-out `clerk init` (`.clerk/keyless.json`) says to run `clerk link` to claim it; an SDK keyless file (`.clerk/.tmp/keyless.json`) says a person claims it with `clerk open` first; anything else keeps today's text. The error code is unchanged.
- Agents hit `not_linked` in these folders and ran `clerk link`, which can't list an application no account owns, so they linked the wrong app or gave up.
- New helper `lib/unlinked-advice.ts` reads the breadcrumb without side effects, so an error message never deletes a malformed file.
- The SDK keyless file path moves into `lib/keyless.ts` so the helper can use it without importing the Backend API client.
- After a successful claim, `attemptAutoclaim` compares the folder's publishable keys (env var, `.env`, `.env.local`) with the claimed app's development key. On any mismatch it links nothing and pulls nothing, and returns `keys_mismatch` with the sources.
- Before, `clerk auth login` linked the claimed app and its env pull wrote the claimed app's secret key next to another app's publishable key.
- `clerk auth login` prints the mismatch as a warning, exits 0, and shows the manual-link next steps. The breadcrumb stays cleared because the claim already happened.
- The claim warning texts move from `login.ts` into `lib/autoclaim.ts` so `clerk link` can print the same ones.
- `clerk link` with no link and no `--app`, in a folder holding a claim token from a signed-out `clerk init`, now signs in if needed, claims that application into the account, links the folder, and writes its keys. It prints "Claimed and linked application: ..." with no prompt in either mode.
- Before, `clerk link` listed only owned applications, so a person got a picker that couldn't show the app and an agent got the usage error.
- When the folder's publishable key belongs to another application, `clerk link` stops with `key_pair_mismatch` and changes nothing. Other claim failures print the same warning as `clerk auth login` and continue with the usual flow.
- Only the `clerk link` command claims: it sets the internal `claimKeylessApp` option on `link()`. `clerk init`, `clerk migrate` and the `clerk doctor` link fix call `link()` without it.
- `login()` takes `claim: false`, which `clerk link` passes so it is the one step that claims and sees the result, including a key mismatch.
- Adds a minor changeset for `clerk link` claiming the `clerk init` application, the per-folder `not_linked` messages in `clerk deploy` and `clerk deploy status`, and the publishable-key check after a claim.
- The `not_linked` message for a folder a Clerk SDK set up now appears only when `.clerk/.tmp/keyless.json` holds a well-formed https claim link. A file with no link, a non-https link or malformed JSON gets the plain message, because `clerk open` would fail there with "no accountless application was found".
- The claim-link reader moves from `commands/open/keyless-claim.ts` into `lib/keyless.ts`, so `clerk open` and the message use the same check. `clerk open` behaves as before; only its debug line's prefix changes.
- The message now says what the CLI found, hedges on whether the app is still unowned, and gives agents runnable steps (`clerk auth login` if needed, `clerk apps list --json`, `clerk link --app <app_id>`).
- A claim that succeeds but doesn't link the folder (a failed config write, or an app with no development instance) no longer prints "Claimed and linked". `clerk link` prints "Claimed application" and links the claimed app directly, as `--app <its id>` would; `clerk auth login` says the folder wasn't linked and to run `clerk link`.
- Both commands print the claim result, including the Dashboard URL, from one `reportClaimed` in `lib/autoclaim.ts`.
- The publishable-key check runs only for the `clerk auth login` command, its `clerk login` alias and `clerk link`'s own claim. `clerk init`, `clerk migrate` and the `clerk doctor` fix sign in through `login()` without it and behave as before. `attemptAutoclaim` now requires callers to choose.
- An env file the check can't read no longer counts as "no keys": the folder is linked but no keys are written, and a warning names the file and says to run `clerk env pull` once it's fixed.
- A temporary claim failure or a managed workspace now stops `clerk link` with the original error and says to rerun `clerk link` or use `--app`, instead of offering a list that can't contain the claimed app.
- Tests clear and restore the shell variables they depend on and stub the credential store in the autoclaim tests.
- `clerk link` still stops when a claim fails with the token kept, but the advice now depends on the cause: a 5xx, 429 or network failure says it is temporary and to rerun `clerk link`; any other 4xx says Clerk rejected the claim and to run `clerk link --app <app_id>`, because it would repeat on every run.
- For Platform API failures the advice is the error's message prefix, so agent JSON carries it next to the API's own code, printed once. Network failures are reported as before, after a warning with the advice.
- An unreadable env file no longer hides an exported publishable key for another app: the claim stops with `key_pair_mismatch` as it does when the files are readable.
- `clerk link` tests set their mock defaults before each test, so each passes when run alone.
- Removes an unused import from `commands/open/keyless-claim.ts`.
- `clerk auth login` warned "It will be retried on your next `clerk auth login`" for every claim failure that kept the token, and its next steps said "Run `clerk link` to connect your application manually", but a bare `clerk link` now retries the claim and stops if it fails again.
- The 5xx/429/network test and both advice texts move into `lib/autoclaim.ts`, so login and link word a failure the same way: a temporary failure says to rerun `clerk link`; any other 4xx says Clerk rejected the claim, with the status and reason, and to run `clerk link --app <app_id>`.
- Login's next steps offer "Run `clerk auth login` again to retry auto-claim" only after a temporary failure; both cases show "Run `clerk link --app <app_id>` to link a different application".
- Adds an agent-mode `clerk link` test for a spent token (404) that continues into autolink.
- Updates the auth README and the changeset to match.
- The `not_linked` message for a folder holding a signed-out `clerk init`'s claim token said the application was one "no account owns yet", but a Dashboard claim can leave the token file behind, so that wasn't known.
- It now says what the CLI found and hedges the same way the SDK message does: "If no account owns that application yet, `clerk link` claims it into your account. Run `clerk link` to link this folder, then rerun `clerk deploy`." Running `clerk link` is the right step either way.
- The key check read only `.env` and `.env.local`, but the claim's env pull writes `.env.development.local` first when it exists, so a claim could overwrite another app's keys there. The check now reads the exported variable and `.env.development.local`, `.env.local`, `.env.development` and `.env`, each on its own.
- An unreadable file no longer hides a mismatch found in another file: any mismatch refuses with `key_pair_mismatch`, and an unreadable file only leads to "linked, no keys written" when nothing mismatches.
- The claim's env pull now writes the folder the check read (`pull({ cwd })`), not the process's working directory. CLI callers pass the current directory, so their behavior is the same.
- A 401 from the claim means the saved sign-in was rejected: both commands say to run `clerk auth login` and rerun `clerk link`, with no `--app` advice, which would fail the same way.
- Adds tests with the real link and env pull, a 401 in each command, the registered `clerk link` command with no or a malformed token file, no-organization continuing, and a failed key write.
- An expired saved session during the claim was classed as a temporary error, so `clerk link` printed "Rerun `clerk link` to retry" next to the session error saying to sign in. It now counts as a rejected sign-in: `clerk link` throws the session error alone, and `clerk auth login` gives the sign-in advice.
- With `CLERK_PLATFORM_API_KEY` set, the Platform API client uses that key before any saved sign-in, so a 401 means the key was rejected. Both commands now say to fix or unset it instead of signing in again, and login's next steps match.
- Any other error the CLI raises itself (e.g. `invalid_key_format`) is no longer called temporary. `clerk link` rethrows it unchanged; `clerk auth login` warns with its message and shows the fix-the-key step only when `CLERK_PLATFORM_API_KEY` is set, and no next steps otherwise. "Temporary" now covers only network failures, 5xx and 429.
- The key check matched key lines with the shared env parser, which reads a CRLF line, an indented line or spaces around `=` as a comment, so another app's key on such a line was never seen. It now converts line endings, matches key lines with its own pattern, and compares values trimmed, unquoted and without an inline comment. The shared parser is unchanged.
- The reader's comment and the READMEs say why `.env.development` is checked: `clerk env pull` never writes it, but it outranks `.env` in development.
- `login.test.ts` clears and restores `CLERK_PLATFORM_API_KEY` around each test.
@changeset-bot

changeset-bot Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0468dc1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
clerk Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • Resume reviews
  • Trigger review
Walkthrough

The CLI adds keyless application claiming to clerk link and configurable claim and key-check behavior to login. It checks local publishable keys before linking, reports claim and link outcomes, and selects recovery guidance by claim error. Deploy and deploy status now provide unlinked-folder messages based on CLI breadcrumbs or SDK claim files.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related issues

  • clerk/cli#580 — The PR retains SDK claim guidance that directs users to clerk open. The issue’s question about agent-mode output remains unresolved.
  • clerk/cli#576 — The PR adds runtime advice with bare clerk command names. The issue’s command-naming question remains unresolved.
  • clerk/cli#555 — The PR checks publishable keys in environment sources, but the issue’s broader environment-line parsing gap remains open.

Possibly related PRs

  • clerk/cli#577 — It changes shared claim, link, key-matching, and unlinked-folder flows that this PR extends.

Suggested reviewers: wyattjoh, rafa-thayto

Merge Risk: Low · up to 0468d

Update both prompt references so users receive accurate instructions for signed-out application claiming.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 44.83% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documenta… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: clerk link claims applications created by signed-out clerk init.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The pull request description directly explains the changes to clerk link, claim handling, key mismatch checks, deploy guidance, SDK claim links, testing, and known limitations.
Full details: Docstring Coverage

Explanation

Docstring coverage is 44.83% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 29 functions across 21 files. (1 skipped: 1 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Linked repositories: Some configured repositories could not be included in this review. Cross-repository analysis may be incomplete. Please check linked-repository access and settings.

Tip

🎯 Customize your risk assessments

Guide how CodeRabbit assesses merge risk and security architecture risk. Highlight sensitive changes and explain your security boundaries using Merge Risk Instructions and Security Risk Instructions in your review settings. Security architecture risk assessments require the Advanced plan or above.

✅ Automatic approvals, with your rules

Let CodeRabbit approve pull requests once required threads are resolved, pre-merge checks pass, and your risk limits are met. Enable Automatic approvals in review settings, with overrides for teams or file paths.

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.
Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Team
  • Run ID: 0de7dd4d-1429-4125-b08e-58e065e29057
Commits

Reviewing files that changed from the base of the PR and between ae38e8f and 0468dc1.

Files selected for processing: 1
  • packages/cli-core/src/commands/auth/README.md
Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

Included review availability: This review used your included allowance. 7 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 10 reviews per hour.

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Fix comments: Fix CodeRabbit comments on this PR

Comment thread packages/cli-core/src/lib/keyless.ts Resolved
- `readSdkClaimUrl` checked that the SDK's claim link parsed as https but returned the original string. On Windows, `clerk open` launches the browser with `cmd /c start "" "<url>"`, so an https link containing a double quote, such as `https://a.com/" & calc & "`, passed the check and its quote ended the argument.
- It now returns the parsed URL's `href`, which percent-encodes quotes and spaces, so no raw quote reaches `cmd.exe`. A normal claim link such as `https://dashboard.clerk.com/apps/claim?token=tok_sdk` is unchanged.
- Adds a test that an https claim link with an embedded quote and spaces comes back percent-encoded.

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Fix comments: Fix CodeRabbit comments on this PR

Comment thread packages/cli-core/src/lib/keyless.ts Resolved
- `clerk link` and `clerk auth login` no longer show a raw TypeError when a claim response has no `instances` array. `link()` reads the instances defensively, so `clerk link` ends in "Application has no development instance" (`instance_not_found`). The claim step checks for a development instance before calling `linkApp`, so `clerk auth login` says "This folder wasn't linked: the application has no development instance". The claim response is untrusted JSON, so a missing array must not fail a claim that already succeeded server-side.
- After a key mismatch, `clerk auth login`'s only next step is "Run `clerk link --app <app_id>` with the application this folder should use", matching its warning. It used to suggest a bare `clerk link` and then `clerk env pull`. The folder isn't linked yet, so there is nothing to pull until `--app` picks the application, and a bare `clerk link` that picks the claimed app, followed by a pull, would write its keys over the other app's.
- Corrects two comments that said `clerk link` can't link an unclaimed app. It claims an app a signed-out `clerk init` created; an SDK-created app still needs `clerk open`, which is why `whoami` keeps its keyless next steps.
- Defines the SDK keyless file path once, in `lib/keyless.ts`. `lib/keyless-target.ts` imports it, so `clerk open`, the deploy advice, `env pull`, `whoami` and `doctor` can't drift apart if the SDK moves the file.
- `ensureAuth` passes `claim` straight to `login()`. `runLogin` treats an undefined `claim` as `true`, so the ternary that dropped the key changed nothing.
- Shortens the claim code's doc comments to the constraint each states. The reasoning moved here:
  - Why a claim failed decides its advice. An expired saved session or a 401 means the credentials were rejected. Any other `CliError` (e.g. `invalid_key_format`) is the CLI's own check, repeats on every run, and its message already says what to fix. Any other 4xx also repeats on every run.
  - The key check covers the exported variable and the development env files. `clerk env pull` writes `.env.development.local`, `.env.local` or `.env`. Each file is read on its own.
  - `clerk init`, `clerk migrate` and the `clerk doctor` fix sign in through `login()` without the key check, so they keep linking whatever they claim. They call `link()` without `claimKeylessApp`. A signed-out `clerk link --app` or relink also claims during sign-in, without the check.
  - For an integration-managed workspace, the user switches to a workspace they own and the next `clerk auth login` or `clerk link` retries.
- `login.test.ts` sets the browser opener's default before each test as well as after, so the first test no longer depends on an earlier one.
- The auth README said `clerk link` always signs in with claiming turned off. Only its claim step does; a signed-out `clerk link --app` or relink signs in with claiming on, so that sign-in claims, without the key check. Its list of which commands check keys now includes that case.
- The deploy README said the `not_linked` message names the kind of application because only one kind is listed by `clerk link`. The picker lists neither; the reason is that `clerk link` can claim only the kind `clerk init` created.
- The `key_pair_mismatch` comment named only `clerk env pull`'s use. `clerk link` now also returns the code when the folder's publishable key belongs to an app other than the one it just claimed, so an agent branching on the code meets both situations.
- The changeset adds that `clerk auth login`'s claim-failure messages now say why the claim failed and what to do, and that `clerk init`, `clerk migrate` and `clerk doctor` print them too when they sign in.

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Fix comments: Fix CodeRabbit comments on this PR

- The sentence listing who signs in without the key check said "a signed-out `clerk link --app` or relink sign in", which read as if "relink" were a verb; it now says "signed-out `clerk link --app` and relink flows call `login()`".

@coderabbitai coderabbitai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Fix comments: Fix CodeRabbit comments on this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant