Skip to content

feat(keyless): make keyless the default and let agents configure a keyless instance - #395

Open
rafa-thayto wants to merge 8 commits into
mainfrom
rafa-thayto/keyless-mode-default
Open

feat(keyless): make keyless the default and let agents configure a keyless instance#395
rafa-thayto wants to merge 8 commits into
mainfrom
rafa-thayto/keyless-mode-default

Conversation

@rafa-thayto

Copy link
Copy Markdown
Contributor

What

Restores keyless mode as the default for unauthenticated clerk init (reversing #268), then extends keyless so an agent can bootstrap and configure Clerk end to end without a Clerk account — no login, no CLERK_PLATFORM_API_KEY.

No backend changes required. Every route used here was verified live against api.clerk.dev with an unclaimed keyless application's sk_test_ key before being written against.

Init

Flag / case Behavior
unauthenticated + keyless-capable framework keyless again (human bootstrap + all agent runs); existing-project human runs still log in
--keyless forces keyless even when signed in, existing projects included
--login forces the authenticated flow
--template <b2b-saas|b2c-saas|native|waitlist> pre-configures the application at creation

Usage errors: --keyless --login, --keyless --app, --template --login, and agent-mode --login while signed out (agents can't complete browser OAuth).

-y means confirmations only — it neither forces nor bypasses keyless.

Operating a keyless instance

clerk config pull / patch fall back to the Backend API using the instance secret key. The payload names BAPI resources 1:1 rather than translating the Platform API's document shape:

clerk config patch --json '{"instance":{"support_email":"dev@acme.com"},"organization_settings":{"enabled":true}}'

Groups: instance, communication, restrictions, organization_settings, protect, oauth_application_settings, instance_settings. Any other key is a usage error naming the supported ones.

Also now account-free: clerk enable/disable orgs, clerk whoami (reports the instance identity), clerk env pull (writes locally-held keys). Keys are additionally discovered from .clerk/.tmp/keyless.json, the file Clerk SDKs write when they mint their own application — previously invisible to the CLI.

Account credentials are deliberately not part of the keyless decision. Only --app or a linked profile selects the account path, so these work with or without a platform key/login. When credentials do exist and the directory simply isn't linked, the command still runs and warns that linking gives the full configuration.

Known limits (documented, not worked around)

  • Billing and auth strategies / social providers have no secret-key-reachable route — both exit with a clear auth_required explanation. RFC drafted separately.
  • restrictions and instance_settings are write-only (no GET route).
  • GET /v1/instance returns fewer fields than PATCH accepts, so the pre-write diff is best-effort; the write is unaffected.
  • Backend API has no dry run, so --dry-run previews locally and sends nothing.
  • Multi-group writes aren't atomic (no transaction); a mid-sequence failure names the groups already applied.

Test plan

  • bun run format:check, bun run lint, bun run typecheck — clean
  • bun run test1947 pass, 0 fail
  • E2E not run locally (1Password vault unavailable); relies on CI
  • Verified live against a real keyless app: config pull/patch across groups, enable orgs (membership limit change confirmed via Frontend API), whoami, env pull on an SDK-created app with no .env, --template b2b-saas (orgs enabled on creation vs. off without it), and every guard rail
  • Dev servers booted on all five keyless-capable frameworks; the Clerk <SignIn /> widget rendered live from a keyless app

Notes for reviewers

  • All account/keyless dispatch is centralized in commands/config/io.ts over a typed InstanceTarget; no command branches on it directly.
  • init/index.test.ts crossed 1000 lines, so it was split into index.test.ts (plumbing) and strategy.test.ts (strategy selection), with the shared spy harness extracted to src/test/lib/init-harness.ts.
  • Branch is based on an older main; the diff is clean against the merge base but a rebase before merge is reasonable.

@changeset-bot

changeset-bot Bot commented Jul 24, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 21f9572

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

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

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 Jul 24, 2026

Copy link
Copy Markdown

Review 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

Walkthrough

This change adds unified account/keyless target resolution and Backend API configuration support for unclaimed applications. clerk init gains revised keyless defaults, authenticated-flow controls, templates, and rerun protection. Environment, identity, API, organization, billing, doctor, open, and users commands now handle keyless targets with updated validation and guidance. Credential refresh handling, key-pair checks, claim-link handling, and test coverage are also updated.

Estimated code review effort: 5 (Critical) | ~120 minutes

Suggested reviewers: wyattjoh

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: making keyless mode the default and enabling agents to configure keyless instances.
Description check ✅ Passed The description directly explains the keyless initialization, configuration, command support, limitations, and testing changes in the pull request.
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.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@rafa-thayto
rafa-thayto force-pushed the rafa-thayto/keyless-mode-default branch from ee5482e to 92f9de8 Compare July 24, 2026 21:20
@rafa-thayto
rafa-thayto marked this pull request as draft July 24, 2026 21:26

@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.

Actionable comments posted: 2

🧹 Nitpick comments (4)
packages/cli-core/src/commands/orgs/README.md (1)

65-71: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the keyless BAPI endpoint to the reference table.

The endpoints table only lists the two account-mode (PLAPI) endpoints, omitting the PATCH /v1/instance/organization_settings BAPI endpoint that the keyless-mode section above (Line 9) already documents. As per path instructions, flagging because this table is a reference/audit surface where an incomplete endpoint list is a real, if small, correctness gap.

📝 Proposed table addition
 | Method | Endpoint                                                          | Description                                                               |
 | ------ | ------------------------------------------------------------------ | -------------------------------------------------------------------------- |
 | GET    | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Fetch current config for diff and the org-billing dependency check        |
 | PATCH  | `/v1/platform/applications/{appId}/instances/{instanceId}/config` | Patch `organization_settings` (with `?dry_run=true` when `--dry-run` set) |
+| PATCH  | `/v1/instance/organization_settings`                              | Keyless mode: patch org settings directly on the unclaimed application    |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/orgs/README.md` around lines 65 - 71, Update
the “Clerk API endpoints” reference table in the README to include the keyless
BAPI PATCH /v1/instance/organization_settings endpoint, using a description that
reflects its organization_settings patch behavior. Keep the existing PLAPI
endpoint rows unchanged.

Source: Path instructions

packages/cli-core/src/commands/config/io.ts (1)

58-83: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

writeInstanceConfig silently drops --destructive for keyless targets; PUT/PATCH invariant relies on caller discipline.

writeInstanceConfig's keyless branch ignores options.method and options.destructive, trusting comments-only invariants ("PUT is rejected before reaching here", "payload was validated before the diff was shown") instead of enforcing them. This surfaces today in push.ts: clerk config patch --destructive against a keyless target silently no-ops the flag with no warning, and any future caller that skips assertPayloadWritable or passes method: "PUT" for a keyless target would get an unvalidated payload sent to BAPI or a silent PATCH instead of the expected error.

  • packages/cli-core/src/commands/config/io.ts#L58-L83: have writeInstanceConfig warn (or throw) when destructive is set for a keyless target, and/or assert options.method === "PATCH" before calling patchKeylessConfig, rather than relying solely on caller-side comments.
  • packages/cli-core/src/commands/config/push.ts#L119-L126: until the above is added, consider warning the user (or stripping destructive) when target.kind === "keyless" before this call, since the flag currently has no effect but no feedback either.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/config/io.ts` around lines 58 - 83, Update
writeInstanceConfig in packages/cli-core/src/commands/config/io.ts (lines 58-83)
to enforce keyless-target invariants: reject non-PATCH methods and explicitly
handle destructive instead of silently dropping it before calling
patchKeylessConfig. Update packages/cli-core/src/commands/config/push.ts (lines
119-126) to provide user feedback or strip destructive for keyless targets; this
site may be made redundant if the centralized validation fully handles the flag.
packages/cli-core/src/commands/whoami/index.ts (1)

76-93: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Wrap the BAPI call with withApiContext for consistent error messaging.

describeKeyless calls bapiRequest directly, unlike the analogous keyless-aware calls added elsewhere in this PR (e.g., schema.ts's fetchInstanceConfigSchema, env/pull.ts's fetchApplication), which wrap the network call in withApiContext for a friendly failure message. A raw network failure here (revoked key, connectivity issue) will surface without that context.

♻️ Proposed fix
 async function describeKeyless(keyless: KeylessTarget): Promise<Identity> {
   const instance = await withSpinner("Fetching instance info...", async () => {
-    const response = await bapiRequest({
-      method: "GET",
-      path: "/v1/instance",
-      secretKey: keyless.secretKey,
-    });
+    const response = await withApiContext(
+      bapiRequest({ method: "GET", path: "/v1/instance", secretKey: keyless.secretKey }),
+      "Failed to fetch instance info",
+    );
     return response.body as { id?: string; environment_type?: string };
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/whoami/index.ts` around lines 76 - 93, Update
describeKeyless to wrap its bapiRequest call for /v1/instance with
withApiContext, matching the established keyless-aware request pattern and
preserving the existing response handling and identity mapping.
packages/cli-core/src/commands/init/strategy.test.ts (1)

248-259: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test isolation relies on a manual workaround instead of fixing the root cause.

Both tests re-mock config.resolveProfile specifically to guard against "leakage from earlier tests that spy on resolveProfile ... but don't track those spies for restoration" (per the inline comment). That's an order-dependent test suite — untracked ad hoc spyOn calls elsewhere can bleed into unrelated tests, and the defensive re-mock only patches the symptom for these two cases.

Consider having tests consistently pipe ad hoc spyOn calls through the harness's track() helper (or wrapping spyOn in the harness to auto-track), so afterEach restores everything and defensive re-mocking becomes unnecessary.

Also applies to: 274-292

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/init/strategy.test.ts` around lines 248 - 259,
Fix test isolation in the harness by ensuring ad hoc spyOn calls, including
config.resolveProfile spies used by neighboring tests, are registered through
the harness track() helper or automatically tracked by its spy wrapper so
afterEach restores them. Then remove the defensive config.resolveProfile
re-mocks from the authenticated-flow tests at lines 248-259 and 274-292,
preserving their intended setup and assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli-core/src/commands/config/README.md`:
- Around line 204-213: Update the “API Endpoints (keyless mode)” table to
include the documented routes for the four omitted groups: communication,
protect, oauth_application_settings, and instance_settings. Keep their endpoint
paths, readability values, and coverage descriptions consistent with the
corresponding Payload shape entries and keyless.test.ts behavior.

In `@packages/cli-core/src/commands/init/index.ts`:
- Around line 160-184: Update assertUsableFlags to reject options.template
combined with options.app, using the same usage-error pattern as the existing
--keyless/--app validation. Ensure template requests are not silently discarded
when runStrategy selects a non-keyless path; preserve the existing
--template/--login validation and, if strategy selection is adjusted, make
pickStrategy or runStrategy explicitly warn or force keyless when a template is
provided without --keyless.

---

Nitpick comments:
In `@packages/cli-core/src/commands/config/io.ts`:
- Around line 58-83: Update writeInstanceConfig in
packages/cli-core/src/commands/config/io.ts (lines 58-83) to enforce
keyless-target invariants: reject non-PATCH methods and explicitly handle
destructive instead of silently dropping it before calling patchKeylessConfig.
Update packages/cli-core/src/commands/config/push.ts (lines 119-126) to provide
user feedback or strip destructive for keyless targets; this site may be made
redundant if the centralized validation fully handles the flag.

In `@packages/cli-core/src/commands/init/strategy.test.ts`:
- Around line 248-259: Fix test isolation in the harness by ensuring ad hoc
spyOn calls, including config.resolveProfile spies used by neighboring tests,
are registered through the harness track() helper or automatically tracked by
its spy wrapper so afterEach restores them. Then remove the defensive
config.resolveProfile re-mocks from the authenticated-flow tests at lines
248-259 and 274-292, preserving their intended setup and assertions.

In `@packages/cli-core/src/commands/orgs/README.md`:
- Around line 65-71: Update the “Clerk API endpoints” reference table in the
README to include the keyless BAPI PATCH /v1/instance/organization_settings
endpoint, using a description that reflects its organization_settings patch
behavior. Keep the existing PLAPI endpoint rows unchanged.

In `@packages/cli-core/src/commands/whoami/index.ts`:
- Around line 76-93: Update describeKeyless to wrap its bapiRequest call for
/v1/instance with withApiContext, matching the established keyless-aware request
pattern and preserving the existing response handling and identity mapping.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 45807ed9-c06a-4293-a40a-630c1b79b426

📥 Commits

Reviewing files that changed from the base of the PR and between 699f7dc and 92f9de8.

📒 Files selected for processing (28)
  • .changeset/keyless-mode-default.md
  • packages/cli-core/src/commands/billing/README.md
  • packages/cli-core/src/commands/billing/index.ts
  • packages/cli-core/src/commands/config/README.md
  • packages/cli-core/src/commands/config/apply-patch.ts
  • packages/cli-core/src/commands/config/io.ts
  • packages/cli-core/src/commands/config/keyless.test.ts
  • packages/cli-core/src/commands/config/keyless.ts
  • packages/cli-core/src/commands/config/pull.ts
  • packages/cli-core/src/commands/config/push.ts
  • packages/cli-core/src/commands/config/schema.ts
  • packages/cli-core/src/commands/env/README.md
  • packages/cli-core/src/commands/env/pull.ts
  • packages/cli-core/src/commands/init/README.md
  • packages/cli-core/src/commands/init/heuristics.ts
  • packages/cli-core/src/commands/init/index.test.ts
  • packages/cli-core/src/commands/init/index.ts
  • packages/cli-core/src/commands/init/strategy.test.ts
  • packages/cli-core/src/commands/orgs/README.md
  • packages/cli-core/src/commands/orgs/index.ts
  • packages/cli-core/src/commands/whoami/README.md
  • packages/cli-core/src/commands/whoami/index.ts
  • packages/cli-core/src/lib/credential-store.ts
  • packages/cli-core/src/lib/keyless-target.ts
  • packages/cli-core/src/lib/keyless.ts
  • packages/cli-core/src/test/integration/lib/harness.ts
  • packages/cli-core/src/test/lib/init-harness.ts
  • packages/cli-core/src/test/lib/stubs.ts
🔗 Linked repositories identified

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

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)

Comment thread packages/cli-core/src/commands/config/README.md
Comment thread packages/cli-core/src/commands/init/index.ts
@rafa-thayto rafa-thayto changed the title feat(keyless): make keyless the default again and let agents configure a keyless instance feat(keyless): make keyless the default and let agents configure a keyless instance Jul 24, 2026
@rafa-thayto
rafa-thayto marked this pull request as ready for review July 29, 2026 14:14

@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.

Actionable comments posted: 6

🧹 Nitpick comments (3)
packages/cli-core/src/commands/users/open.test.ts (1)

87-98: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fixture error shape doesn't match production.

resolveAppContext rejecting with a plain Error short-circuits resolveKnownUserDashboardTarget immediately. In production it throws CliError with ERROR_CODE.NOT_LINKED, which that function swallows and then continues into resolveUsersInstanceContext — so the failure that actually reaches open()'s catch (and thus assertNotUnclaimedKeyless) comes from a different call. Using the real error shape keeps the test honest if that fall-through ever changes.

♻️ Suggested fixture
-      mockResolveAppContext.mockRejectedValue(
-        new Error("No Clerk project linked to this directory."),
-      );
+      mockResolveAppContext.mockRejectedValue(
+        new CliError("No Clerk project linked to this directory.", {
+          code: ERROR_CODE.NOT_LINKED,
+        }),
+      );

Also confirm resolveUsersInstanceContext is stubbed to fail in this suite so the keyless branch is reached the same way it is at runtime.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/users/open.test.ts` around lines 87 - 98,
Update the unclaimed keyless application fixture in the relevant open test to
reject mockResolveAppContext with the production CliError using
ERROR_CODE.NOT_LINKED, so resolveKnownUserDashboardTarget falls through
correctly. Verify that mockResolveUsersInstanceContext is also stubbed to fail,
ensuring open() reaches the keyless branch and assertNotUnclaimedKeyless through
the same runtime path.
packages/cli-core/src/commands/open/keyless-claim.ts (1)

90-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract a shared helper for keyless-instance BAPI lookup. Both sites independently fetch /v1/instance with the keyless secret key and map the response into a KeylessInstanceInfo-shaped object, but with inconsistent field naming (instanceId vs id) and slightly different error handling (raw error casting vs the shared errorMessage() helper).

  • packages/cli-core/src/commands/open/keyless-claim.ts#L90-L99: replace describeKeylessInstance()'s body with a call to a shared helper (e.g. moved to lib/keyless-target.ts or a new lib/keyless-instance.ts) so both call sites share one implementation and one field-naming convention.
  • packages/cli-core/src/commands/doctor/context.ts#L62-L85: have getKeylessInstance() call the same shared helper instead of re-implementing the bapiRequest/mapping/try-catch logic inline.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/open/keyless-claim.ts` around lines 90 - 99,
Create a shared keyless-instance lookup helper that performs the /v1/instance
request, maps fields consistently, and uses errorMessage() for failures. Update
describeKeylessInstance() in
packages/cli-core/src/commands/open/keyless-claim.ts:90-99 and
getKeylessInstance() in packages/cli-core/src/commands/doctor/context.ts:62-85
to delegate to that helper, removing their duplicated request, mapping, and
try-catch logic.
packages/cli-core/src/commands/config/keyless.ts (1)

259-281: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Leaf verification is key-order sensitive for object-valued array elements.

collectVerifiedLeaves compares non-object leaves with JSON.stringify(value) === JSON.stringify(observedValue). This works fine for primitives and arrays of primitives, but if a sent array ever contains objects (e.g. a future or other group's field shaped like [{a: 1, b: 2}]), a semantically-identical value returned with keys in a different order would serialize differently and get misreported as a droppedField — a false "this wasn't applied" warning for a write that actually succeeded. None of today's INSTANCE_FIELDS hit this (arrays here are string arrays), but this function runs across every payload group, not just instance.

♻️ Suggested fix

Use a deep-equality comparison instead of relying on JSON.stringify's key-order sensitivity, e.g. via a small recursive deepEqual helper (or a well-known utility) for the leaf-array case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/config/keyless.ts` around lines 259 - 281,
Update collectVerifiedLeaves to compare leaf values with deep structural
equality rather than JSON.stringify, including arrays containing objects whose
key order differs. Add or reuse a recursive deepEqual-style helper and use it
for the matched calculation, while preserving the existing handling of
primitives, objects, and array leaves.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/keyless-demos/lib/demo-env.sh`:
- Line 101: Update the retry failure logging around the keyless key-minting flow
to remove the raw $response from the stderr message. Emit only a sanitized
failure message, optionally including a non-sensitive HTTP status, so secret_key
or other credential material cannot appear in recordings or CI logs.
- Around line 86-88: Bound the curl request in the accountless application flow
by adding both a connection timeout and an overall request timeout to the curl
invocation inside the retry loop. Keep the existing response capture and
retry/fail-loudly behavior unchanged, ensuring stalled DNS, TLS, or HTTP
connections return control to the loop.

In `@docs/keyless-demos/tapes/09-health-and-users.tape`:
- Around line 31-34: Update the `clerk open --print` step in the
health-and-users tape to avoid displaying the claim token. Record only a
redacted claim URL or its origin/path while preserving the command’s intended
demonstration flow.

In `@packages/cli-core/src/commands/open/README.md`:
- Around line 41-45: Add the text language specifier to the fenced code block
containing the unclaimed-application message in the README, preserving the
message content and formatting.

In `@packages/cli-core/src/lib/bapi-command.ts`:
- Around line 21-33: Update describeBapiTarget to handle options.secretKey
before calling resolveKeylessTarget, matching resolveBapiSecretKey’s
highest-precedence branch. Return a description for the explicitly supplied
secret key first, and only perform the keyless lookup when no secretKey option
is provided.

In `@packages/cli-core/src/lib/credential-store.ts`:
- Around line 305-319: Update isUnrecoverableRefreshFailure to exclude HTTP 429
from the terminal 4xx range, preserving it as a transient refresh failure while
continuing to classify other unrecoverable 4xx responses as expired sessions.

---

Nitpick comments:
In `@packages/cli-core/src/commands/config/keyless.ts`:
- Around line 259-281: Update collectVerifiedLeaves to compare leaf values with
deep structural equality rather than JSON.stringify, including arrays containing
objects whose key order differs. Add or reuse a recursive deepEqual-style helper
and use it for the matched calculation, while preserving the existing handling
of primitives, objects, and array leaves.

In `@packages/cli-core/src/commands/open/keyless-claim.ts`:
- Around line 90-99: Create a shared keyless-instance lookup helper that
performs the /v1/instance request, maps fields consistently, and uses
errorMessage() for failures. Update describeKeylessInstance() in
packages/cli-core/src/commands/open/keyless-claim.ts:90-99 and
getKeylessInstance() in packages/cli-core/src/commands/doctor/context.ts:62-85
to delegate to that helper, removing their duplicated request, mapping, and
try-catch logic.

In `@packages/cli-core/src/commands/users/open.test.ts`:
- Around line 87-98: Update the unclaimed keyless application fixture in the
relevant open test to reject mockResolveAppContext with the production CliError
using ERROR_CODE.NOT_LINKED, so resolveKnownUserDashboardTarget falls through
correctly. Verify that mockResolveUsersInstanceContext is also stubbed to fail,
ensuring open() reaches the keyless branch and assertNotUnclaimedKeyless through
the same runtime path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 2f290259-1834-46e7-a903-01fee15ea2b8

📥 Commits

Reviewing files that changed from the base of the PR and between 92f9de8 and d5df0c0.

⛔ Files ignored due to path filters (6)
  • docs/keyless-demos/screenshots/01-signin-before.png is excluded by !**/*.png
  • docs/keyless-demos/screenshots/02-signup-before.png is excluded by !**/*.png
  • docs/keyless-demos/screenshots/03-orgs-enabled-after.png is excluded by !**/*.png
  • docs/keyless-demos/screenshots/04-orgs-disabled-after.png is excluded by !**/*.png
  • docs/keyless-demos/screenshots/05-signup-restricted-after.png is excluded by !**/*.png
  • docs/keyless-demos/screenshots/06-signup-allowed-after.png is excluded by !**/*.png
📒 Files selected for processing (52)
  • .changeset/keyless-mode-default.md
  • docs/keyless-demos/.gitignore
  • docs/keyless-demos/README.md
  • docs/keyless-demos/lib/demo-env.sh
  • docs/keyless-demos/record.sh
  • docs/keyless-demos/screenshots/README.md
  • docs/keyless-demos/tapes/01-bootstrap.tape
  • docs/keyless-demos/tapes/02-read-config.tape
  • docs/keyless-demos/tapes/03-change-config.tape
  • docs/keyless-demos/tapes/04-toggles.tape
  • docs/keyless-demos/tapes/05-templates.tape
  • docs/keyless-demos/tapes/06-guardrails.tape
  • docs/keyless-demos/tapes/07-agent-mode.tape
  • docs/keyless-demos/tapes/08-sso-connections.tape
  • docs/keyless-demos/tapes/09-health-and-users.tape
  • docs/keyless-demos/tapes/_common.tape
  • packages/cli-core/src/commands/api/README.md
  • packages/cli-core/src/commands/config/README.md
  • packages/cli-core/src/commands/config/apply-patch.ts
  • packages/cli-core/src/commands/config/io.ts
  • packages/cli-core/src/commands/config/keyless.test.ts
  • packages/cli-core/src/commands/config/keyless.ts
  • packages/cli-core/src/commands/config/push.ts
  • packages/cli-core/src/commands/doctor/README.md
  • packages/cli-core/src/commands/doctor/checks.ts
  • packages/cli-core/src/commands/doctor/context.test.ts
  • packages/cli-core/src/commands/doctor/context.ts
  • packages/cli-core/src/commands/doctor/doctor.test.ts
  • packages/cli-core/src/commands/doctor/types.ts
  • packages/cli-core/src/commands/env/README.md
  • packages/cli-core/src/commands/env/pull.test.ts
  • packages/cli-core/src/commands/env/pull.ts
  • packages/cli-core/src/commands/init/README.md
  • packages/cli-core/src/commands/init/heuristics.ts
  • packages/cli-core/src/commands/init/index.ts
  • packages/cli-core/src/commands/init/strategy.test.ts
  • packages/cli-core/src/commands/open/README.md
  • packages/cli-core/src/commands/open/index.test.ts
  • packages/cli-core/src/commands/open/index.ts
  • packages/cli-core/src/commands/open/keyless-claim.test.ts
  • packages/cli-core/src/commands/open/keyless-claim.ts
  • packages/cli-core/src/commands/users/README.md
  • packages/cli-core/src/commands/users/open.test.ts
  • packages/cli-core/src/commands/users/open.ts
  • packages/cli-core/src/commands/whoami/README.md
  • packages/cli-core/src/commands/whoami/index.test.ts
  • packages/cli-core/src/commands/whoami/index.ts
  • packages/cli-core/src/lib/bapi-command.test.ts
  • packages/cli-core/src/lib/bapi-command.ts
  • packages/cli-core/src/lib/credential-store.ts
  • packages/cli-core/src/lib/keyless-target.test.ts
  • packages/cli-core/src/lib/keyless-target.ts
🔗 Linked repositories identified

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

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/cli-core/src/commands/whoami/README.md
  • .changeset/keyless-mode-default.md
  • packages/cli-core/src/commands/config/push.ts
  • packages/cli-core/src/commands/whoami/index.ts
  • packages/cli-core/src/commands/init/index.ts
  • packages/cli-core/src/commands/env/pull.ts
  • packages/cli-core/src/commands/config/README.md

Comment thread docs/keyless-demos/lib/demo-env.sh Outdated
Comment thread docs/keyless-demos/lib/demo-env.sh Outdated
Comment thread docs/keyless-demos/tapes/09-health-and-users.tape Outdated
Comment thread packages/cli-core/src/commands/open/README.md Outdated
Comment thread packages/cli-core/src/lib/bapi-command.ts
Comment thread packages/cli-core/src/lib/credential-store.ts
@rafa-thayto
rafa-thayto requested a review from wyattjoh July 29, 2026 20:05
…e a keyless instance

Restores keyless mode as the default for unauthenticated `clerk init` (reversing
PR #268), and extends keyless so an agent can bootstrap and configure Clerk end
to end without a Clerk account.

Init:
- Unauthenticated runs on keyless-capable frameworks use keyless again — human
  bootstrap and all agent runs. Existing-project human runs still log in.
- `--login` forces the authenticated flow; `--keyless` now forces keyless even
  when signed in. `--keyless --login`, `--keyless --app`, and agent `--login`
  while signed out are usage errors.
- `--template <b2b-saas|b2c-saas|native|waitlist>` pre-configures the keyless
  application at creation.

Operating a keyless instance (Backend API, instance secret key only):
- `clerk config pull` and `clerk config patch` cover seven resource groups.
- `clerk enable/disable orgs`, `clerk whoami`, and `clerk env pull` work with no
  account. Billing stays account-only and says why.
- Keys are also discovered from `.clerk/.tmp/keyless.json`, so an application a
  Clerk SDK minted for itself is reachable from the CLI.

Account credentials are deliberately not part of the keyless decision: only
`--app` or a linked profile selects the account path, so these commands work
with or without a platform API key or login session.
…an account

Close the gaps between "keyless is the default" and "keyless actually works
with no human intervention":

- config patch validates instance fields against the Backend API's own schema
  before sending — PATCH /v1/instance answers 204 and silently discards
  unknown fields, so a typo or an account-only setting (password, social,
  second_factors, ...) used to print "Config pushed successfully" and change
  nothing. The refusal names the accepted fields and, for settings BAPI has
  no route for at all, says claiming is the only path.
- config patch no longer prints a stale eventually-consistent re-read as the
  result of a write; bodyless groups are reported as unconfirmed.
- allowlist_identifiers/blocklist_identifiers point at `clerk api` (which
  reaches them keyless) instead of demanding a login.
- users open explains that an unclaimed app has no dashboard page and offers
  `clerk api /users/<id>`, instead of blaming "not linked" and suggesting
  remedies that need an application ID that doesn't exist yet.
- whoami/env pull detect publishable/secret keys that belong to different
  applications; env pull refuses to write a mismatched pair.
- doctor, open, users, api, env pull, whoami, enable/disable orgs and
  templates verified end to end on a live unclaimed instance, all under
  --mode agent with non-TTY stdin: no hangs, no prompts, no browser.

docs/keyless-demos: before/after screenshots proving CLI writes reach the
live hosted UI pre-claim (orgs step inserted into the sign-in flow,
allowlist enforced then lifted), with a README on the FAPI /v1/environment
oracle and its limits.
…r classification

- demo-env.sh: bound the accountless-application curl request with connect/max
  timeouts, and stop echoing the raw minting response (it can contain a live
  secret_key when only one of the two fields fails to parse).
- 09-health-and-users.tape: redact the claim token before it's recorded by
  piping `clerk open --print` through sed.
- open/README.md: add a language to the fenced usage-error example (MD040).
- bapi-command.ts: describeBapiTarget now checks options.secretKey first,
  mirroring resolveBapiSecretKey's actual precedence.
- credential-store.ts: exclude 429 from isUnrecoverableRefreshFailure so a
  rate-limited token refresh isn't reported as an expired session.
@rafa-thayto
rafa-thayto force-pushed the rafa-thayto/keyless-mode-default branch from d5df0c0 to 3d52adf Compare July 30, 2026 12:17

@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.

Actionable comments posted: 9

🧹 Nitpick comments (7)
packages/cli-core/src/commands/config/io.ts (1)

74-104: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

writeInstanceConfig silently ignores options.method for keyless targets.

The keyless branch always executes a PATCH via patchKeylessConfig, regardless of what options.method is. The "PUT is rejected before reaching here" invariant is only enforced by callers today (configPush checks it explicitly; applyConfigPatch hardcodes "PATCH"). If a future caller passes method: "PUT" with a keyless target directly to this function, it would silently perform a PATCH instead of failing loudly.

♻️ Proposed defensive check
   if (target.kind === "keyless") {
+    if (options.method === "PUT") {
+      throw new Error("writeInstanceConfig: PUT is not supported for a keyless target");
+    }
     // PUT is rejected before reaching here: there is no full document to
     // replace when the instance is addressed by its own key. The payload was
     // validated by `assertPayloadWritable` before the diff was shown.
     const { applied, verification } = await patchKeylessConfig(
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/config/io.ts` around lines 74 - 104, Update
writeInstanceConfig so the keyless branch validates options.method before
calling patchKeylessConfig: reject method "PUT" explicitly with an error, while
preserving PATCH behavior and the existing non-keyless flow.
packages/cli-core/src/commands/config/keyless.test.ts (1)

650-662: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

requests.every(...) passes vacuously if nothing was requested.

The dry-run assertion holds even when requests is empty, so it can't distinguish "only GETs" from "no calls at all". Adding expect(requests.length).toBeGreaterThan(0) (or asserting the exact expected GET set) makes the intent explicit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/config/keyless.test.ts` around lines 650 -
662, Strengthen the “patch --dry-run sends nothing” test by asserting that
requests is non-empty before checking requests.every(...). Update the test
around configPatch so it verifies at least one GET request occurred while
preserving the existing dry-run log assertion.
packages/cli-core/src/commands/init/index.ts (1)

204-227: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Agent --login runs validation twice.

assertUsableFlags calls isAuthenticatedForAgent() (Line 207) and init calls it again at Line 118 for the same run, so an agent --login invocation performs two getValidToken() + fetchUserInfo round-trips. Memoizing the result (or passing the already-computed value into pickStrategy) removes the redundant network call.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/init/index.ts` around lines 204 - 227, Avoid
duplicate authentication validation for agent-mode --login by reusing the result
from assertUsableFlags instead of calling isAuthenticatedForAgent() again during
init/pickStrategy. Memoize the validation or pass its computed boolean through
the existing flow, preserving the unauthenticated guard behavior while ensuring
getAuthenticatedEmail() runs only once per invocation.
packages/cli-core/src/test/lib/init-harness.ts (1)

118-183: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Track the tracked spies and hold gatherContextSpy explicitly.

track() pushes the spy onto the original array before setup() reassigns spies, so those tracked spies are not restored for the current describe block. Keep existing tracked spies when building the fresh list instead. Also, Bun getMockName() is set via mockName() for the spyOn() spied property name, so the name lookup is safe — but storing the gatherContextSpy in setup state avoids an extra lookup/re-creation entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/test/lib/init-harness.ts` around lines 118 - 183,
Update useInitHarness so setup preserves the existing spies array entries when
constructing the new spy list, ensuring spies added through track are restored
by afterEach. Hold gatherContextSpy in setup state rather than locating it
through getMockName in setupBootstrapSuccess, and have that helper reuse the
current setup spy without creating a fallback spy.
packages/cli-core/src/commands/open/keyless-claim.ts (1)

95-98: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Use the shared errorMessage() helper instead of (error as Error).message.

A non-Error rejection (string, undefined) would throw inside the catch block. errorMessage from ../../lib/errors.ts already handles this and is used elsewhere in this PR.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/open/keyless-claim.ts` around lines 95 - 98,
Update the catch block in the keyless claim flow to use the shared
errorMessage() helper from ../../lib/errors.ts when constructing the log
message, replacing the unsafe (error as Error).message access while preserving
the existing fallback return.
packages/cli-core/src/commands/users/open.ts (1)

85-90: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider ERROR_CODE.NOT_LINKED for consistency with the keyless path in open/index.ts.

openKeylessDashboard (packages/cli-core/src/commands/open/index.ts, lines 130-142) reports the same "unclaimed keyless, no dashboard page" state as NOT_LINKED, while this throws INSTANCE_NOT_FOUND. Agents/scripts branching on code would see two different codes for one condition.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/users/open.ts` around lines 85 - 90, Update
the error code in the unclaimed keyless application branch of open.ts to use
ERROR_CODE.NOT_LINKED instead of ERROR_CODE.INSTANCE_NOT_FOUND, matching
openKeylessDashboard in open/index.ts while preserving the existing message and
error behavior.
packages/cli-core/src/commands/open/index.ts (1)

153-169: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

--print pays for a BAPI round-trip it never uses.

instance is only consumed by the agent and human branches, but it's resolved before the options.print early return — adding a /v1/instance call (plus secret-key discovery) to the scriptable path. Move the lookup below the print branch.

♻️ Proposed reordering
   const { url, source } = destination;
 
+  if (options.print) {
+    log.data(url);
+    return;
+  }
+
   // Best-effort only: the claim link is already fully formed, so a bad or
   // missing secret key shouldn't block opening it — it just means the
   // output won't include instance details.
   const instance = await resolveKeylessTarget({ cwd })
     .then((keyless) => (keyless ? describeKeylessInstance(keyless.secretKey) : null))
     .catch(() => null);
 
-  // Output strategy mirrors the linked-app flow above:
-  //   --print → plain URL on stdout (scriptable)
-  //   agent mode → JSON object with full context (parseable)
-  //   human mode → intro/outro logging flow with browser open
-  if (options.print) {
-    log.data(url);
-    return;
-  }
+  // Output strategy mirrors the linked-app flow above:
+  //   agent mode → JSON object with full context (parseable)
+  //   human mode → intro/outro logging flow with browser open
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/open/index.ts` around lines 153 - 169, Move
the resolveKeylessTarget/describeKeylessInstance lookup into the agent/human
output path after the options.print early return, while preserving its existing
best-effort error handling and leaving the plain URL print behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/keyless-demos/record.sh`:
- Line 21: Require Bun before recording starts by adding a command -v bun
preflight alongside the existing VHS check in
docs/keyless-demos/record.sh:21-21. Document Bun as a required dependency in
docs/keyless-demos/README.md:14-14.

In `@docs/keyless-demos/tapes/03-change-config.tape`:
- Around line 26-30: Update the read-back command in the tape after the
configuration patch to pull the readable organization_settings group instead of
the write-only instance group, preserving the existing patch/read-back flow.

In `@packages/cli-core/src/commands/doctor/checks.ts`:
- Around line 90-94: Make claimHint() use a non-destructive keyless breadcrumb
read so doctor never clears .clerk/keyless.json or logs mutation warnings.
Ensure the breadcrumb is resolved once and reused by checkLoggedIn and
checkProjectLinked, preventing contradictory hints when both checks run in the
same doctor invocation.

In `@packages/cli-core/src/commands/env/README.md`:
- Around line 74-78: Update the sequence diagram in the environment command
README to distinguish the local keyless path from the standard flow: show local
publishable-key discovery, the optional authenticated GET /v1/domains
verification, and that a pairing mismatch performs no write. Ensure the diagram
no longer implies every run fetches an application from the Platform API, while
preserving the documented standard application-fetch flow.

In `@packages/cli-core/src/commands/init/README.md`:
- Line 80: Update the “Unauthenticated mode” step to apply only when the
resolved strategy is keyless, including both minting a keyless application and
preserving/reporting an existing unclaimed keyless app. Align the wording with
the authenticated-flow behavior described for reruns in the surrounding
initialization steps.
- Line 269: Revise the keyless breadcrumb rationale in the README to distinguish
loss of .clerk/keyless.json and environment keys from deletion of server-side
application state. State that these files preserve local recovery and claim
references, while configurations and resources already stored in the unclaimed
application persist even if the references are lost.

In `@packages/cli-core/src/commands/open/keyless-claim.ts`:
- Around line 37-48: Update readSdkClaimUrl to parse claimUrl with URL and
return it only when valid and using the https: protocol; otherwise return
undefined without allowing malformed or unsafe values to reach openBrowser. In
openKeylessDashboard, encode breadcrumb.claimToken with encodeURIComponent
before interpolating it into the query string.

In `@packages/cli-core/src/lib/keyless-target.ts`:
- Around line 152-182: Update hasKeyPairMismatch to catch BAPI and network
failures from the bapiRequest call, returning false when the domains request
fails or returns a non-2xx response. Preserve the existing domain matching logic
and ensure env pull remains nonblocking while reporting no mismatch on
unavailable BAPI data.
- Around line 79-97: Update findKeyInProject so file-based lookup honors names
priority rather than the last matching line: for each env file, collect matching
entries and select the value whose key appears earliest in names, while
preserving ENV_FILES precedence and the existing env-var lookup behavior. Return
the selected entry for the highest-priority name in the first applicable file.

---

Nitpick comments:
In `@packages/cli-core/src/commands/config/io.ts`:
- Around line 74-104: Update writeInstanceConfig so the keyless branch validates
options.method before calling patchKeylessConfig: reject method "PUT" explicitly
with an error, while preserving PATCH behavior and the existing non-keyless
flow.

In `@packages/cli-core/src/commands/config/keyless.test.ts`:
- Around line 650-662: Strengthen the “patch --dry-run sends nothing” test by
asserting that requests is non-empty before checking requests.every(...). Update
the test around configPatch so it verifies at least one GET request occurred
while preserving the existing dry-run log assertion.

In `@packages/cli-core/src/commands/init/index.ts`:
- Around line 204-227: Avoid duplicate authentication validation for agent-mode
--login by reusing the result from assertUsableFlags instead of calling
isAuthenticatedForAgent() again during init/pickStrategy. Memoize the validation
or pass its computed boolean through the existing flow, preserving the
unauthenticated guard behavior while ensuring getAuthenticatedEmail() runs only
once per invocation.

In `@packages/cli-core/src/commands/open/index.ts`:
- Around line 153-169: Move the resolveKeylessTarget/describeKeylessInstance
lookup into the agent/human output path after the options.print early return,
while preserving its existing best-effort error handling and leaving the plain
URL print behavior unchanged.

In `@packages/cli-core/src/commands/open/keyless-claim.ts`:
- Around line 95-98: Update the catch block in the keyless claim flow to use the
shared errorMessage() helper from ../../lib/errors.ts when constructing the log
message, replacing the unsafe (error as Error).message access while preserving
the existing fallback return.

In `@packages/cli-core/src/commands/users/open.ts`:
- Around line 85-90: Update the error code in the unclaimed keyless application
branch of open.ts to use ERROR_CODE.NOT_LINKED instead of
ERROR_CODE.INSTANCE_NOT_FOUND, matching openKeylessDashboard in open/index.ts
while preserving the existing message and error behavior.

In `@packages/cli-core/src/test/lib/init-harness.ts`:
- Around line 118-183: Update useInitHarness so setup preserves the existing
spies array entries when constructing the new spy list, ensuring spies added
through track are restored by afterEach. Hold gatherContextSpy in setup state
rather than locating it through getMockName in setupBootstrapSuccess, and have
that helper reuse the current setup spy without creating a fallback spy.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: eea038a1-5a00-49fc-8b42-a68e29b37968

📥 Commits

Reviewing files that changed from the base of the PR and between d5df0c0 and 3d52adf.

⛔ Files ignored due to path filters (6)
  • docs/keyless-demos/screenshots/01-signin-before.png is excluded by !**/*.png
  • docs/keyless-demos/screenshots/02-signup-before.png is excluded by !**/*.png
  • docs/keyless-demos/screenshots/03-orgs-enabled-after.png is excluded by !**/*.png
  • docs/keyless-demos/screenshots/04-orgs-disabled-after.png is excluded by !**/*.png
  • docs/keyless-demos/screenshots/05-signup-restricted-after.png is excluded by !**/*.png
  • docs/keyless-demos/screenshots/06-signup-allowed-after.png is excluded by !**/*.png
📒 Files selected for processing (64)
  • .changeset/keyless-mode-default.md
  • docs/keyless-demos/.gitignore
  • docs/keyless-demos/README.md
  • docs/keyless-demos/lib/demo-env.sh
  • docs/keyless-demos/record.sh
  • docs/keyless-demos/screenshots/README.md
  • docs/keyless-demos/tapes/01-bootstrap.tape
  • docs/keyless-demos/tapes/02-read-config.tape
  • docs/keyless-demos/tapes/03-change-config.tape
  • docs/keyless-demos/tapes/04-toggles.tape
  • docs/keyless-demos/tapes/05-templates.tape
  • docs/keyless-demos/tapes/06-guardrails.tape
  • docs/keyless-demos/tapes/07-agent-mode.tape
  • docs/keyless-demos/tapes/08-sso-connections.tape
  • docs/keyless-demos/tapes/09-health-and-users.tape
  • docs/keyless-demos/tapes/_common.tape
  • packages/cli-core/src/commands/api/README.md
  • packages/cli-core/src/commands/billing/README.md
  • packages/cli-core/src/commands/billing/index.ts
  • packages/cli-core/src/commands/config/README.md
  • packages/cli-core/src/commands/config/apply-patch.ts
  • packages/cli-core/src/commands/config/io.ts
  • packages/cli-core/src/commands/config/keyless.test.ts
  • packages/cli-core/src/commands/config/keyless.ts
  • packages/cli-core/src/commands/config/pull.ts
  • packages/cli-core/src/commands/config/push.ts
  • packages/cli-core/src/commands/config/schema.ts
  • packages/cli-core/src/commands/doctor/README.md
  • packages/cli-core/src/commands/doctor/checks.ts
  • packages/cli-core/src/commands/doctor/context.test.ts
  • packages/cli-core/src/commands/doctor/context.ts
  • packages/cli-core/src/commands/doctor/doctor.test.ts
  • packages/cli-core/src/commands/doctor/types.ts
  • packages/cli-core/src/commands/env/README.md
  • packages/cli-core/src/commands/env/pull.test.ts
  • packages/cli-core/src/commands/env/pull.ts
  • packages/cli-core/src/commands/init/README.md
  • packages/cli-core/src/commands/init/heuristics.ts
  • packages/cli-core/src/commands/init/index.test.ts
  • packages/cli-core/src/commands/init/index.ts
  • packages/cli-core/src/commands/init/strategy.test.ts
  • packages/cli-core/src/commands/open/README.md
  • packages/cli-core/src/commands/open/index.test.ts
  • packages/cli-core/src/commands/open/index.ts
  • packages/cli-core/src/commands/open/keyless-claim.test.ts
  • packages/cli-core/src/commands/open/keyless-claim.ts
  • packages/cli-core/src/commands/orgs/README.md
  • packages/cli-core/src/commands/orgs/index.ts
  • packages/cli-core/src/commands/users/README.md
  • packages/cli-core/src/commands/users/open.test.ts
  • packages/cli-core/src/commands/users/open.ts
  • packages/cli-core/src/commands/whoami/README.md
  • packages/cli-core/src/commands/whoami/index.test.ts
  • packages/cli-core/src/commands/whoami/index.ts
  • packages/cli-core/src/lib/bapi-command.test.ts
  • packages/cli-core/src/lib/bapi-command.ts
  • packages/cli-core/src/lib/credential-store.test.ts
  • packages/cli-core/src/lib/credential-store.ts
  • packages/cli-core/src/lib/keyless-target.test.ts
  • packages/cli-core/src/lib/keyless-target.ts
  • packages/cli-core/src/lib/keyless.ts
  • packages/cli-core/src/test/integration/lib/harness.ts
  • packages/cli-core/src/test/lib/init-harness.ts
  • packages/cli-core/src/test/lib/stubs.ts
🔗 Linked repositories identified

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

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)
🚧 Files skipped from review as they are similar to previous changes (12)
  • docs/keyless-demos/.gitignore
  • packages/cli-core/src/commands/orgs/README.md
  • packages/cli-core/src/commands/doctor/README.md
  • docs/keyless-demos/screenshots/README.md
  • packages/cli-core/src/commands/billing/README.md
  • packages/cli-core/src/commands/whoami/README.md
  • packages/cli-core/src/commands/open/README.md
  • packages/cli-core/src/commands/api/README.md
  • packages/cli-core/src/commands/doctor/types.ts
  • packages/cli-core/src/commands/users/README.md
  • .changeset/keyless-mode-default.md
  • packages/cli-core/src/commands/config/README.md

Comment thread docs/keyless-demos/record.sh Outdated
Comment thread docs/keyless-demos/tapes/03-change-config.tape Outdated
Comment thread packages/cli-core/src/commands/doctor/checks.ts Outdated
Comment thread packages/cli-core/src/commands/env/README.md
Comment thread packages/cli-core/src/commands/init/README.md Outdated
Comment thread packages/cli-core/src/commands/init/README.md Outdated
Comment thread packages/cli-core/src/commands/open/keyless-claim.ts
Comment thread packages/cli-core/src/lib/keyless-target.ts
Comment thread packages/cli-core/src/lib/keyless-target.ts

@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.

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/cli-core/src/commands/open/keyless-claim.ts (1)

43-48: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Return the normalized URL, not the raw SDK value.

Line 48 returns the untrusted original string after validation. A parseable HTTPS URL can contain a literal ", which new URL() normalizes but the Windows launcher still receives raw; that can break out of its quoted cmd /c start argument. Return url.toString() and add a quote-containing HTTPS regression case.

Proposed fix
-    const claimUrl = typeof parsed.claimUrl === "string" ? parsed.claimUrl : undefined;
-    if (claimUrl && !isHttpsUrl(claimUrl)) {
+    const rawClaimUrl = typeof parsed.claimUrl === "string" ? parsed.claimUrl : undefined;
+    const claimUrl = rawClaimUrl === undefined ? undefined : normalizeHttpsUrl(rawClaimUrl);
+    if (rawClaimUrl !== undefined && !claimUrl) {
       log.debug(`open: ignoring non-https claimUrl in ${SDK_KEYLESS_FILE.join("/")}`);
       return undefined;
     }
     return claimUrl;
-function isHttpsUrl(value: string): boolean {
+function normalizeHttpsUrl(value: string): string | undefined {
   try {
-    return new URL(value).protocol === "https:";
+    const url = new URL(value);
+    return url.protocol === "https:" ? url.toString() : undefined;
   } catch {
-    return false;
+    return undefined;
   }
 }
#!/bin/bash
set -euo pipefail

bun -e '
const raw = `https://dashboard.clerk.com/apps/claim?token=x" & echo injected`;
const parsed = new URL(raw);
console.log({ raw, normalized: parsed.toString(), protocol: parsed.protocol });
if (parsed.protocol !== "https:" || raw === parsed.toString()) process.exit(1);
'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/open/keyless-claim.ts` around lines 43 - 48,
Update the claim URL parsing flow around parsed.claimUrl and isHttpsUrl to
retain the successfully parsed URL object and return its normalized toString()
value instead of the raw string. Preserve rejection of non-HTTPS URLs, and add a
regression case covering an HTTPS claim URL containing a quote to verify the
returned value is normalized before launcher use.
🧹 Nitpick comments (1)
packages/cli-core/src/lib/keyless-target.test.ts (1)

2-2: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use Bun’s file-writing API for fixtures.

Replace the new node:fs/promises writeFile calls with Bun.write; the test already runs on Bun.

Proposed fix
-import { mkdtemp, rm, writeFile } from "node:fs/promises";
+import { mkdtemp, rm } from "node:fs/promises";

-await writeFile(join(tempDir, ".env.local"), content);
+await Bun.write(join(tempDir, ".env.local"), content);

As per coding guidelines, “Prefer Bun.file over node:fs's readFile/writeFile for file operations.”

Also applies to: 108-145

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/lib/keyless-target.test.ts` at line 2, Replace the
imported node:fs/promises writeFile usage in the keyless-target test fixtures
with Bun.write, updating each affected fixture setup in the test cases around
the referenced range. Remove writeFile from the import while preserving the
existing fixture paths and contents; leave mkdtemp and rm unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli-core/src/lib/keyless-target.test.ts`:
- Around line 105-112: Update the keyless-target test setup around beforeEach to
capture the original NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY and CLERK_PUBLISHABLE_KEY
values, then restore those values after each test rather than unconditionally
deleting them. Preserve the current per-test environment setup while ensuring
inherited or CI-injected keys are returned unchanged.

---

Duplicate comments:
In `@packages/cli-core/src/commands/open/keyless-claim.ts`:
- Around line 43-48: Update the claim URL parsing flow around parsed.claimUrl
and isHttpsUrl to retain the successfully parsed URL object and return its
normalized toString() value instead of the raw string. Preserve rejection of
non-HTTPS URLs, and add a regression case covering an HTTPS claim URL containing
a quote to verify the returned value is normalized before launcher use.

---

Nitpick comments:
In `@packages/cli-core/src/lib/keyless-target.test.ts`:
- Line 2: Replace the imported node:fs/promises writeFile usage in the
keyless-target test fixtures with Bun.write, updating each affected fixture
setup in the test cases around the referenced range. Remove writeFile from the
import while preserving the existing fixture paths and contents; leave mkdtemp
and rm unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bba1ab17-ae35-4cad-8552-b0dcbdafa7f9

📥 Commits

Reviewing files that changed from the base of the PR and between 3d52adf and d3681a4.

📒 Files selected for processing (12)
  • packages/cli-core/src/commands/doctor/checks.ts
  • packages/cli-core/src/commands/doctor/context.ts
  • packages/cli-core/src/commands/doctor/doctor.test.ts
  • packages/cli-core/src/commands/doctor/types.ts
  • packages/cli-core/src/commands/env/README.md
  • packages/cli-core/src/commands/init/README.md
  • packages/cli-core/src/commands/open/keyless-claim.test.ts
  • packages/cli-core/src/commands/open/keyless-claim.ts
  • packages/cli-core/src/lib/keyless-target.test.ts
  • packages/cli-core/src/lib/keyless-target.ts
  • packages/cli-core/src/lib/keyless.test.ts
  • packages/cli-core/src/lib/keyless.ts
🔗 Linked repositories identified

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

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/cli-core/src/commands/env/README.md
  • packages/cli-core/src/commands/doctor/doctor.test.ts
  • packages/cli-core/src/commands/init/README.md

Comment thread packages/cli-core/src/lib/keyless-target.test.ts
Comment thread .changeset/keyless-mode-default.md
Comment thread packages/cli-core/src/commands/billing/index.ts
Comment thread packages/cli-core/src/commands/config/apply-patch.ts Outdated
Comment thread packages/cli-core/src/commands/config/apply-patch.ts Outdated
Comment thread packages/cli-core/src/commands/config/keyless.ts
Comment thread packages/cli-core/src/commands/whoami/index.test.ts
Comment thread packages/cli-core/src/commands/orgs/index.ts
Comment thread packages/cli-core/src/commands/config/README.md Outdated
Comment thread packages/cli-core/src/commands/doctor/README.md Outdated
Comment thread packages/cli-core/src/commands/init/index.ts
…and doctor diagnostics

- Restore CLERK_SECRET_KEY's precedence: the env var wins over a linked
  profile again, and --instance next to it stays a no-op instead of a usage
  error. Routing it through resolveKeylessTarget had silently changed both,
  breaking invocations that work on main; real env-var-path tests pin it now.
- init's keep-the-existing-app guard also recognizes an application the SDK
  minted for itself (.clerk/.tmp/keyless.json), instead of silently orphaning
  it with a replacement.
- doctor reports a malformed local secret key as one named failing check with
  a remedy, instead of five anonymous 'Check crashed' lines.
- whoami: keyless next-steps no longer point at clerk link (which cannot work
  pre-claim), and an unlinked directory holding a local secret key is called
  out, since clerk api/users will use that key's instance, not the account.
- Smaller review items: billing asserts on an already-resolved target,
  config schema resolves its target once, apply-patch dry-run messages reuse
  the verb, claim-url validation inlined, keylessTargetStubs covers every
  export for module mocks, positive test for the non-429 refresh refusal,
  keyless disable-orgs test, README corrections, changeset trimmed.
One function per sentence with typed parameters, so a future i18n layer can
swap the module's bodies without touching call sites; pluralisation and
agreement rules move with the prose. No visible string changes.
@rafa-thayto
rafa-thayto requested a review from wyattjoh July 30, 2026 22:36
Signed-out `clerk init` changes behaviour — it now mints a keyless
application instead of launching the browser login — so the release is not
purely additive even with CLERK_SECRET_KEY precedence restored.

@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.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli-core/src/commands/doctor/checks.ts`:
- Around line 110-119: Update checkLoggedIn so it resolves and checks
ctx.getKeylessKeyError() before returning success for a stored account token.
Preserve the existing malformed-key failure diagnosis and token-success behavior
when no key error exists.

In `@packages/cli-core/src/commands/whoami/index.ts`:
- Around line 103-112: Update the whoami flow around findLocalSecretKey and the
localSecretKeySource field so exported CLERK_SECRET_KEY overrides are detected
and reported even when a profile resolves. Match the precedence used by
resolveBapiSecretKey, while preserving the existing unlinked-directory behavior,
and add coverage for a linked profile with an exported secret key.

In `@packages/cli-core/src/lib/copy.ts`:
- Around line 29-30: Update apiReachableKeysLine so the hint includes a direct
clerk api example for every key in keys, rather than only keys[0], while
preserving the existing singular/plural wording and unclaimed-application
context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0bc7a305-60c3-4040-a7c2-fbdb4b48ebbb

📥 Commits

Reviewing files that changed from the base of the PR and between d3681a4 and 21f9572.

📒 Files selected for processing (30)
  • .changeset/keyless-mode-default.md
  • packages/cli-core/src/commands/billing/index.ts
  • packages/cli-core/src/commands/config/README.md
  • packages/cli-core/src/commands/config/apply-patch.ts
  • packages/cli-core/src/commands/config/keyless.test.ts
  • packages/cli-core/src/commands/config/keyless.ts
  • packages/cli-core/src/commands/config/push.ts
  • packages/cli-core/src/commands/config/schema.ts
  • packages/cli-core/src/commands/doctor/README.md
  • packages/cli-core/src/commands/doctor/checks.ts
  • packages/cli-core/src/commands/doctor/context.test.ts
  • packages/cli-core/src/commands/doctor/context.ts
  • packages/cli-core/src/commands/doctor/doctor.test.ts
  • packages/cli-core/src/commands/doctor/types.ts
  • packages/cli-core/src/commands/init/index.ts
  • packages/cli-core/src/commands/init/strategy.test.ts
  • packages/cli-core/src/commands/open/index.test.ts
  • packages/cli-core/src/commands/open/keyless-claim.ts
  • packages/cli-core/src/commands/users/open.test.ts
  • packages/cli-core/src/commands/users/open.ts
  • packages/cli-core/src/commands/whoami/index.test.ts
  • packages/cli-core/src/commands/whoami/index.ts
  • packages/cli-core/src/lib/bapi-command.test.ts
  • packages/cli-core/src/lib/bapi-command.ts
  • packages/cli-core/src/lib/copy.ts
  • packages/cli-core/src/lib/credential-store.test.ts
  • packages/cli-core/src/lib/keyless-target.ts
  • packages/cli-core/src/lib/next-steps.ts
  • packages/cli-core/src/test/lib/init-harness.ts
  • packages/cli-core/src/test/lib/stubs.ts
🔗 Linked repositories identified

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

  • clerk/clerk_go (manual)
  • clerk/dashboard (manual)
  • clerk/accounts (manual)
  • clerk/backoffice (manual)
  • clerk/clerk (manual)
  • clerk/clerk-docs (manual)
  • clerk/cloudflare-workers (manual)
🚧 Files skipped from review as they are similar to previous changes (3)
  • .changeset/keyless-mode-default.md
  • packages/cli-core/src/commands/doctor/README.md
  • packages/cli-core/src/commands/config/README.md

Comment on lines +110 to 119
// A local key that isn't a secret key at all is the one keyless state that
// is genuinely broken — report it here as the named diagnosis, once, rather
// than letting it crash every keyless-aware check (see getKeylessKeyError).
const keyError = await ctx.getKeylessKeyError();
if (keyError) {
return check.fail(`Not logged in, and the local secret key is unusable: ${keyError.message}`, {
remedy: "Fix or remove the malformed key, or run `clerk auth login` to authenticate.",
fixable: false,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Surface malformed local keys before accepting a stored token.

checkLoggedIn returns at Line 98 before resolving the keyless target. In an unlinked project with both a stored account token and an invalid local CLERK_SECRET_KEY, doctor reports success instead of the malformed-key diagnosis—even though keyless resolution still rejects that local key. Resolve/check getKeylessKeyError() before the token-success return.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/doctor/checks.ts` around lines 110 - 119,
Update checkLoggedIn so it resolves and checks ctx.getKeylessKeyError() before
returning success for a stored account token. Preserve the existing
malformed-key failure diagnosis and token-success behavior when no key error
exists.

Comment on lines +103 to +112
// Unlinked but holding a local secret key: the Backend API commands will use
// that key, not this account, so whoami must say so or its answer is wrong
// for half the command surface.
const localKey = profile ? undefined : await findLocalSecretKey(process.cwd());

return {
kind: "account",
email: userInfo.email,
profile,
localSecretKeySource: localKey?.source ?? null,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Report exported secret-key overrides for linked directories.

When a profile resolves, Line 106 skips discovery entirely. But resolveBapiSecretKey still returns exported CLERK_SECRET_KEY before profile resolution, so whoami can claim the linked application while clerk api and clerk users target a different instance. Detect and surface the exported-key override even when profile exists, and add linked-profile coverage.

Proposed direction
-  const localKey = profile ? undefined : await findLocalSecretKey(process.cwd());
+  const localKey = profile
+    ? process.env.CLERK_SECRET_KEY
+      ? { source: "CLERK_SECRET_KEY environment variable" }
+      : undefined
+    : await findLocalSecretKey(process.cwd());
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/whoami/index.ts` around lines 103 - 112,
Update the whoami flow around findLocalSecretKey and the localSecretKeySource
field so exported CLERK_SECRET_KEY overrides are detected and reported even when
a profile resolves. Match the precedence used by resolveBapiSecretKey, while
preserving the existing unlinked-directory behavior, and add coverage for a
linked profile with an exported secret key.

Comment on lines +29 to +30
apiReachableKeysLine: (keys: string[]): string =>
`${keys.join(", ")} ${keys.length === 1 ? "is" : "are"} already reachable on an unclaimed application — use \`clerk api /${keys[0]}\` directly instead of this config document.`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Incomplete clerk api hint when multiple keys are reachable.

When unknown keys are all API-reachable and there's more than one, the sentence lists every key but only shows the clerk api /<path> example for keys[0], leaving guidance for the other key(s) unstated.

✏️ Proposed fix
-  apiReachableKeysLine: (keys: string[]): string =>
-    `${keys.join(", ")} ${keys.length === 1 ? "is" : "are"} already reachable on an unclaimed application — use \`clerk api /${keys[0]}\` directly instead of this config document.`,
+  apiReachableKeysLine: (keys: string[]): string =>
+    `${keys.join(", ")} ${keys.length === 1 ? "is" : "are"} already reachable on an unclaimed application — use ${keys.map((key) => `\`clerk api /${key}\``).join(" or ")} directly instead of this config document.`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
apiReachableKeysLine: (keys: string[]): string =>
`${keys.join(", ")} ${keys.length === 1 ? "is" : "are"} already reachable on an unclaimed application — use \`clerk api /${keys[0]}\` directly instead of this config document.`,
apiReachableKeysLine: (keys: string[]): string =>
`${keys.join(", ")} ${keys.length === 1 ? "is" : "are"} already reachable on an unclaimed application — use ${keys.map((key) => `\`clerk api /${key}\``).join(" or ")} directly instead of this config document.`,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/lib/copy.ts` around lines 29 - 30, Update
apiReachableKeysLine so the hint includes a direct clerk api example for every
key in keys, rather than only keys[0], while preserving the existing
singular/plural wording and unclaimed-application context.

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.

2 participants