Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
ae4a51e
feat(security): add clerk security audit, fix, and checks
dominic-clerk Sep 9, 2026
dee5c4e
fix(security): address review: device-trust id, unknown lockout thres…
dominic-clerk Sep 9, 2026
32031a1
docs(security): drop the catalog provenance note
dominic-clerk Sep 9, 2026
dc3a2ee
docs(security): backup codes alone do not satisfy mfa
dominic-clerk Sep 10, 2026
c131d15
docs(security): describe what fix --all applies instead of claiming n…
dominic-clerk Sep 10, 2026
80c93ca
feat(security): name the plan-gated checks when production rejects a fix
dominic-clerk Sep 17, 2026
8e172dd
feat(security): drop --fail-on, --spotlight, and --check
dominic-clerk Sep 17, 2026
857bd05
fix(security): drop --spotlight from the post-fix next step
dominic-clerk Sep 17, 2026
98958f4
refactor(security): unexport module-private helpers
dominic-clerk Sep 18, 2026
9c7f135
feat(security): warn what custom flows must handle before applying a fix
dominic-clerk Sep 18, 2026
857a27c
docs(changeset): shorten the security entry
dominic-clerk Sep 23, 2026
1dbca25
refactor(security): drop suggestedPatch and raw values, one fix-comma…
dominic-clerk Sep 23, 2026
451bde0
fix(security): close fix selections and plan retries over the projection
dominic-clerk Sep 23, 2026
83bfb3e
fix(security): make the passwordless fix safe on its own
dominic-clerk Sep 23, 2026
e71b688
fix(security): gate passwordless detection on used_for_sign_in
dominic-clerk Oct 12, 2026
ea04648
fix(security): name the decision checks --all skips, drop no-op 402 r…
dominic-clerk Oct 19, 2026
4892088
refactor(security): share the OAuth and tier helpers, derive factor v…
dominic-clerk Oct 19, 2026
File filter

Filter by extension

Filter by extension

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

Add `clerk security`: `audit` grades an instance against Clerk's security recommendations, `fix` applies them as one config patch, and `checks` lists the catalog. Agent mode gets the report as JSON with the exact patch per finding.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ Commands:
link [options] Link this project to a Clerk application
mcp Manage the Clerk remote MCP server connection for AI editors and CLIs
open Open Clerk resources in your browser
security Audit an instance against Clerk's security recommendations
telemetry Control CLI usage telemetry (status, disable, enable)
unlink [options] Unlink this project from its Clerk application
update [options] Update the Clerk CLI to the latest version
Expand Down
2 changes: 2 additions & 0 deletions packages/cli-core/src/cli-program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { registerTelemetry } from "./commands/telemetry/index.ts";
import { registerToggles } from "./commands/toggles/index.ts";
import { registerApi } from "./commands/api/index.ts";
import { registerDoctor } from "./commands/doctor/index.ts";
import { registerSecurity } from "./commands/security/index.ts";
import { registerMcp } from "./commands/mcp/index.ts";
import { registerSwitchEnv } from "./commands/switch-env/index.ts";
import { registerCompletion } from "./commands/completion/index.ts";
Expand Down Expand Up @@ -79,6 +80,7 @@ const registrants: CommandRegistrant[] = [
registerToggles,
registerApi,
registerDoctor,
registerSecurity,
registerMcp,
registerSwitchEnv,
registerCompletion,
Expand Down
19 changes: 14 additions & 5 deletions packages/cli-core/src/commands/completion/__complete.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { CommandUnknownOpts, Option } from "@commander-js/extra-typings";
import { sources } from "../migrate/sources/registry.ts";
import { CHECKS, findCheck } from "../security/catalog.ts";
import { KNOWN_DASHBOARD_PATHS } from "../open/dashboard-paths.ts";

const DIRECTIVE = {
Expand Down Expand Up @@ -60,6 +61,10 @@ const KNOWN_OPTION_VALUES: Record<string, Completion[]> = {
{ name: "canary", description: "Latest canary (pre-release) build" },
],
"--source": SOURCE_COMPLETIONS,
"--factors": [
...findCheck("mfa")!.decision!.options.map((o) => ({ name: o.value, description: o.label })),
{ name: "authenticator,backup-code", description: "Authenticator app and backup codes" },
],
"--for": [
{ name: "orgs", description: "Organizations only" },
{ name: "users", description: "Users only" },
Expand All @@ -78,6 +83,7 @@ const KNOWN_POSITIONAL_COMPLETIONS: Record<string, Completion[]> = {
name: path,
description: "Dashboard subpath",
})),
"security fix": CHECKS.map((check) => ({ name: check.id, description: check.title })),
};

/**
Expand Down Expand Up @@ -219,11 +225,14 @@ function completeArguments(
consumedCount: number,
): CompletionResult {
const registeredArgs = cmd.registeredArguments;
if (consumedCount >= registeredArgs.length) {
return EMPTY_NO_FILE;
}

const arg = registeredArgs[consumedCount];
const last = registeredArgs.at(-1);
const arg =
consumedCount < registeredArgs.length
? registeredArgs[consumedCount]
: last?.variadic
? last
: undefined;
if (!arg) return EMPTY_NO_FILE;

// Prefer strict Commander choices when available.
if (arg?.argChoices) {
Expand Down
3 changes: 3 additions & 0 deletions packages/cli-core/src/commands/config/apply-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export interface ApplyPatchOptions {
warning?: string;
/** Pre-fetched current config; skips the extra GET when caller already has it. */
currentConfig?: Record<string, unknown>;
/** Receives the response body: the written document, or the projection under `--dry-run`. */
onWritten?: (body: Record<string, unknown>) => void;
}

/** Fetch + diff + confirm + PATCH, matching `clerk config patch` semantics. */
Expand Down Expand Up @@ -67,6 +69,7 @@ export async function applyConfigPatch(opts: ApplyPatchOptions): Promise<boolean
);

log.debug(`config: ${JSON.stringify(result.body)}`);
opts.onWritten?.(result.body);
if (dryRun) {
log.success("[dry-run] Validation passed — no changes applied");
} else {
Expand Down
3 changes: 1 addition & 2 deletions packages/cli-core/src/commands/deploy/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { clerkSubdomains } from "./copy.ts";
import { log } from "../../lib/log.ts";
import { wrap } from "../../lib/wrap.ts";
import { openBrowser } from "../../lib/open.ts";
import { OAUTH_KEY_PREFIX } from "../../lib/oauth-connections.ts";
import type {
ConfigSchemaProperty,
IOSApplication,
Expand Down Expand Up @@ -134,8 +135,6 @@ type ProviderWithOverride = keyof typeof PROVIDER_OVERRIDES;

const SYSTEM_FIELD_KEYS = new Set(["enabled", "authenticatable", "block_email_subaddresses"]);
const DEFAULT_FIELD_ORDER = ["client_id", "client_secret"];
export const OAUTH_KEY_PREFIX = "connection_oauth_";

const SHARED_OAUTH_METADATA = new Map<string, (typeof OAUTH_PROVIDERS)[number]>(
OAUTH_PROVIDERS.map((provider) => [provider.provider, provider]),
);
Expand Down
16 changes: 4 additions & 12 deletions packages/cli-core/src/commands/deploy/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
} from "./copy.ts";
import { mapDeployError } from "./errors.ts";
import {
OAUTH_KEY_PREFIX,
buildOAuthProviderDescriptors,
hasProviderRequiredCredentials,
inspectNativeAppleConfiguration,
Expand All @@ -41,6 +40,10 @@ import {
type OAuthProvider,
type OAuthProviderDescriptor,
} from "./providers.ts";
import {
discoverEnabledOAuthProviderSlugs,
OAUTH_KEY_PREFIX,
} from "../../lib/oauth-connections.ts";
import { pendingOAuthProviders, resolveActiveReportState } from "./report-state.ts";
import type { DeployContext, DeployOperationState } from "./state.ts";
import { recordDomainObservation, recordOAuthObservation } from "./telemetry.ts";
Expand Down Expand Up @@ -749,17 +752,6 @@ export async function loadProductionDomain(
return domains.data.find((domain) => !domain.is_satellite) ?? domains.data[0];
}

export function discoverEnabledOAuthProviderSlugs(config: Record<string, unknown>): string[] {
const providers: string[] = [];
for (const [key, value] of Object.entries(config)) {
if (!key.startsWith(OAUTH_KEY_PREFIX)) continue;
if (!value || typeof value !== "object") continue;
if ((value as Record<string, unknown>).enabled !== true) continue;
providers.push(key.slice(OAUTH_KEY_PREFIX.length));
}
return providers;
}

export async function waitForDeployStatus(
appId: string,
domainIdOrName: string,
Expand Down
Loading
Loading