diff --git a/.changeset/agent-init-keyless.md b/.changeset/agent-init-keyless.md new file mode 100644 index 00000000..9ddb1985 --- /dev/null +++ b/.changeset/agent-init-keyless.md @@ -0,0 +1,5 @@ +--- +"clerk": patch +--- + +Allow `clerk init` to run in agent mode without requiring `--app`. For keyless-capable frameworks, agent init now uses keyless setup when no real Clerk app target is provided; explicit `--app` or an existing linked profile still uses the authenticated app-linking flow. Agent init no longer creates, auto-selects, or auto-links a Clerk application when no app target is provided. diff --git a/packages/cli-core/src/commands/init/README.md b/packages/cli-core/src/commands/init/README.md index 8cdea2d5..a29efc10 100644 --- a/packages/cli-core/src/commands/init/README.md +++ b/packages/cli-core/src/commands/init/README.md @@ -38,6 +38,8 @@ When running in agent mode (`--mode agent` or non-TTY), the command runs the ful - For **existing projects**: framework and package manager are auto-detected, no flags required - For **new projects** (`--starter` or blank directory): `--framework` is required (no way to auto-detect in an empty dir). Package manager is auto-selected by availability (bun → pnpm → yarn → npm) unless `--pm` is provided - Project name defaults to the framework's default (e.g. `my-clerk-next-app`) unless `--name` is provided +- For keyless-capable frameworks with no `--app` and no linked profile, init uses keyless and does not require auth +- For frameworks that require API keys, init will not pick or create an app in agent mode; pass `--app ` or link the project first to pull real keys Use `--prompt` to output a setup prompt for an AI agent without running init. @@ -45,11 +47,12 @@ Use `--prompt` to output a setup prompt for an AI agent without running init. 1. **`--prompt`**: outputs a framework-specific prompt, then exits 2. Gathers project context (framework, router variant, TypeScript, `src/` directory, package manager) -3. Determines auth mode from credential presence (no user prompt): - - **Authenticated** (OAuth token or `CLERK_PLATFORM_API_KEY` set): uses the authenticated flow — runs `clerk link` if not already linked and pulls real API keys into `.env` at the end - - **Bootstrap + keyless-capable framework + not authenticated**: automatically uses keyless mode — the app runs on auto-generated dev keys and the user can connect a Clerk account later with `clerk auth login` - - **Bootstrap + non-keyless framework + not authenticated** (with `--yes` or agent mode): skips authentication and prints manual setup instructions (run `clerk auth login` / `clerk link` / `clerk env pull` when ready) - - **Existing project + not authenticated**: runs the authenticated flow, which triggers an interactive login so real keys can be pulled +3. Determines auth mode: + - **Real app target** (`--app` or linked profile): authenticates, links if needed, and pulls real API keys into `.env` + - **Agent + keyless-capable framework + no real app target**: uses keyless mode — the app runs on auto-generated dev keys and the user can connect a Clerk account later with `clerk auth login` + - **Agent + non-keyless framework + no real app target**: scaffolds locally and prints manual setup instructions instead of selecting or creating an app + - **Human mode + bootstrap + keyless-capable framework + not authenticated**: uses keyless mode + - **Human mode + existing project + not authenticated**: runs the authenticated flow, which triggers an interactive login so real keys can be pulled 4. **Authenticated mode only**: authenticates via `clerk auth login` (skipped if already authenticated) and links the project via `clerk link` (skipped if already linked) 5. Displays detected framework and variant 6. Detects existing auth libraries (NextAuth, Auth0, Supabase, Firebase, Passport, Better Auth, Kinde) and shows migration guidance @@ -83,7 +86,7 @@ Detects the project's framework from `package.json` dependencies (checked top-to | `express` | Express | `@clerk/express` | `CLERK_PUBLISHABLE_KEY` | No | | `fastify` | Fastify | `@clerk/fastify` | `CLERK_PUBLISHABLE_KEY` | No | -The **Keyless** column indicates whether the framework's Clerk SDK supports keyless mode (auto-generated temporary dev keys). Keyless auto-selection only applies during bootstrap (new projects) — re-running `clerk init` in an existing project always uses the authenticated flow (prompting login when signed out) so real keys can be pulled via `clerk env pull`. During bootstrap of a non-keyless framework with `--yes` and no credentials, `clerk init` skips authentication and prints manual setup instructions instead of blocking on a login prompt. +The **Keyless** column indicates whether the framework's Clerk SDK supports keyless mode (auto-generated temporary dev keys). In human mode, keyless auto-selection only applies during bootstrap (new projects). In agent mode, keyless-capable frameworks use keyless whenever no real app target is provided by `--app` or a linked profile. For non-keyless frameworks without a real app target, agent mode prints manual setup instructions instead of selecting or creating an app. Package manager is detected from lock files: `bun.lockb`/`bun.lock` → bun, `yarn.lock` → yarn, `pnpm-lock.yaml` → pnpm, else npm. @@ -229,15 +232,15 @@ Implementation lives in [`skills.ts`](./skills.ts). Note that the E2E fixture se ## API Endpoints -| Step | Method | Base URL | Endpoint | Description | -| ---------------------- | ------ | ------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------- | -| Create accountless app | `POST` | `CLERK_BAPI_URL` (default BAPI) | `/v1/accountless_applications` | Creates a temporary keyless Clerk application; returns `publishable_key`, `secret_key`, and `claim_url`. Only called in the keyless bootstrap path. | +| Step | Method | Base URL | Endpoint | Description | +| ---------------------- | ------ | ------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | +| Create accountless app | `POST` | `CLERK_BAPI_URL` (default BAPI) | `/v1/accountless_applications` | Creates a temporary keyless Clerk application; returns `publishable_key`, `secret_key`, and `claim_url`. Only called in keyless mode. | See [auth/README.md](../auth/README.md), [link/README.md](../link/README.md), and [env/README.md](../env/README.md) for the API endpoints used by each step. ## Keyless breadcrumb -In the keyless bootstrap path, after calling `POST /v1/accountless_applications`, `clerk init` writes `.clerk/keyless.json` to the project root. This file records the claim token extracted from `claim_url` so that `clerk auth login` can automatically claim the temporary application the next time the user authenticates. +In keyless mode, after calling `POST /v1/accountless_applications`, `clerk init` writes `.clerk/keyless.json` to the project root. This file records the claim token extracted from `claim_url` so that `clerk auth login` can automatically claim the temporary application the next time the user authenticates. ```json { diff --git a/packages/cli-core/src/commands/init/index.test.ts b/packages/cli-core/src/commands/init/index.test.ts index 2f05a203..0c108ffb 100644 --- a/packages/cli-core/src/commands/init/index.test.ts +++ b/packages/cli-core/src/commands/init/index.test.ts @@ -351,6 +351,117 @@ describe("init", () => { expect(linkMod.link).not.toHaveBeenCalled(); }); + test("agent mode with keyless framework uses keyless without an app target", async () => { + setup({ isAgent: true, email: "user@example.com" }); + + const keylessCtx = { + ...FAKE_CTX, + existingClerk: false, + framework: { ...FAKE_CTX.framework, supportsKeyless: true }, + }; + spyOn(context, "gatherContext").mockResolvedValue(keylessCtx); + spyOn(scaffoldMod, "scaffold").mockResolvedValue({ + actions: [{ type: "create", path: "middleware.ts", content: "", description: "" }], + postInstructions: [], + }); + + await init({}); + + expect(heuristics.printKeylessInfo).toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + expect(pullMod.pull).not.toHaveBeenCalled(); + }); + + test("agent mode with keyless framework uses linked profile as a real app target", async () => { + setup({ isAgent: true, email: "user@example.com" }); + + const keylessCtx = { + ...FAKE_CTX, + existingClerk: false, + framework: { ...FAKE_CTX.framework, supportsKeyless: true }, + }; + spyOn(context, "gatherContext").mockResolvedValue(keylessCtx); + spyOn(config, "resolveProfile").mockResolvedValue({ + profile: { appId: "app_123" }, + } as never); + spyOn(scaffoldMod, "scaffold").mockResolvedValue({ + actions: [{ type: "create", path: "middleware.ts", content: "", description: "" }], + postInstructions: [], + }); + + await init({}); + + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env", cwd: keylessCtx.cwd }); + }); + + test("agent mode with keyless framework and --app uses real app flow", async () => { + setup({ isAgent: true, email: "user@example.com" }); + + const keylessCtx = { + ...FAKE_CTX, + existingClerk: false, + framework: { ...FAKE_CTX.framework, supportsKeyless: true }, + }; + spyOn(context, "gatherContext").mockResolvedValue(keylessCtx); + spyOn(scaffoldMod, "scaffold").mockResolvedValue({ + actions: [{ type: "create", path: "middleware.ts", content: "", description: "" }], + postInstructions: [], + }); + + await init({ app: "app_abc" }); + + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + expect(linkMod.link).toHaveBeenCalledWith({ + skipIfLinked: true, + app: "app_abc", + cwd: keylessCtx.cwd, + }); + expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env", cwd: keylessCtx.cwd }); + }); + + test("agent mode with non-keyless framework and no app target prints manual setup", async () => { + const { captured } = setup({ isAgent: true, email: "user@example.com" }); + + const noKeylessCtx = { + ...FAKE_CTX, + existingClerk: false, + framework: { + dep: "vue", + name: "Vue", + sdk: "@clerk/vue", + envVar: "VITE_CLERK_PUBLISHABLE_KEY", + envFile: ".env.local" as const, + }, + envFile: ".env.local", + }; + spyOn(context, "gatherContext").mockResolvedValue(noKeylessCtx); + spyOn(scaffoldMod, "scaffold").mockResolvedValue({ + actions: [{ type: "create", path: "src/main.ts", content: "", description: "" }], + postInstructions: [], + }); + + await captured.run(() => init({})); + + expect(linkMod.link).not.toHaveBeenCalled(); + expect(pullMod.pull).not.toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + expect(captured.err).toContain("clerk init --app "); + }); + + test("agent mode with real app target and no auth fails without launching login", async () => { + setup({ isAgent: true }); + spyOn(context, "gatherContext").mockResolvedValue(FAKE_CTX); + + await expect(init({ app: "app_abc" })).rejects.toThrow( + "clerk init in agent mode requires CLERK_PLATFORM_API_KEY", + ); + + expect(loginMod.login).not.toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + }); + test("-y flag skips auth prompt and defaults to unauthenticated mode", async () => { setup(); setupBootstrapSuccess(); diff --git a/packages/cli-core/src/commands/init/index.ts b/packages/cli-core/src/commands/init/index.ts index 79e35ace..5d6a3ce3 100644 --- a/packages/cli-core/src/commands/init/index.ts +++ b/packages/cli-core/src/commands/init/index.ts @@ -3,7 +3,7 @@ import { link } from "../link/index.js"; import { pull } from "../env/pull.js"; import { isAgent } from "../../mode.js"; import { dim, bold } from "../../lib/color.js"; -import { throwUserAbort, CliError, errorMessage } from "../../lib/errors.js"; +import { throwUserAbort, CliError, errorMessage, throwUsageError } from "../../lib/errors.js"; import { lookupFramework, type FrameworkInfo } from "../../lib/framework.js"; import { resolveProfile } from "../../lib/config.js"; import { log } from "../../lib/log.js"; @@ -58,6 +58,7 @@ type InitOptions = { export async function init(options: InitOptions = {}) { const cwd = process.cwd(); + const agent = isAgent(); const frameworkOverride = options.framework ? (lookupFramework(options.framework) ?? undefined) @@ -72,7 +73,7 @@ export async function init(options: InitOptions = {}) { // In agent mode, implicitly enable --yes to skip all confirmation prompts. const overrides: BootstrapOverrides = { - skipConfirm: options.yes || isAgent(), + skipConfirm: options.yes || agent, pmOverride: options.pm, nameOverride: options.name, }; @@ -94,14 +95,23 @@ export async function init(options: InitOptions = {}) { await enrichProjectContext(ctx); const authed = await isAuthenticated(); - const keyless = resolveKeylessMode(bootstrap, ctx, authed); + const linkedProfile = agent && !options.app ? await resolveProfile(ctx.cwd) : undefined; + const hasRealAppTarget = Boolean(options.app || linkedProfile); + const keyless = resolveKeylessMode({ + agent, + bootstrap, + ctx, + authed, + hasRealAppTarget, + }); ctx.keyless = keyless; - const skipAuth = !keyless && bootstrap != null && overrides.skipConfirm && !authed; + const manualSetup = + !keyless && (agent ? !hasRealAppTarget : bootstrap != null && overrides.skipConfirm && !authed); - if (!keyless && !skipAuth) { + if (!keyless && !manualSetup) { bar(); - await authenticateAndLink(ctx.cwd, options.app); + await authenticateAndLink(ctx.cwd, options.app, { allowInteractiveLogin: !agent }); } // Short-circuit on a fully-clean re-run so env pull / skills prompt don't @@ -113,12 +123,15 @@ export async function init(options: InitOptions = {}) { if (alreadySetUp) { log.success("\nClerk is already set up in this project."); + if (agent && manualSetup) { + printBootstrapManualSetupInfo(ctx.framework.name); + } outro("Done"); return; } bar(); - if (skipAuth) { + if (manualSetup) { printBootstrapManualSetupInfo(ctx.framework.name); } else if (!keyless) { await pull({ file: ctx.envFile, cwd: ctx.cwd }); @@ -214,8 +227,7 @@ function printBootstrapNextSteps( function printBootstrapManualSetupInfo(frameworkName: string): void { const lines = [ `\n ${frameworkName} requires API keys — set them up manually:`, - " clerk auth login", - " clerk link", + " clerk init --app ", " clerk env pull", ]; log.info(lines.map(dim).join("\n")); @@ -223,19 +235,27 @@ function printBootstrapManualSetupInfo(frameworkName: string): void { // --- Keyless --- -function resolveKeylessMode( - bootstrap: BootstrapResult | null, - ctx: ProjectContext, - authed: boolean, -): boolean { - // Auto-keyless is scoped to bootstrap (new-project) flows only. For existing - // projects, fall through to the authenticated flow so `clerk init` still - // runs `authenticateAndLink` and pulls real keys — even when signed out the - // user gets a login prompt rather than being silently dropped into keyless - // (which would skip `env pull` and could overwrite permissive middleware). - if (!bootstrap) return false; - +function resolveKeylessMode({ + agent, + bootstrap, + ctx, + authed, + hasRealAppTarget, +}: { + agent: boolean; + bootstrap: BootstrapResult | null; + ctx: ProjectContext; + authed: boolean; + hasRealAppTarget: boolean; +}): boolean { if (ctx.framework.supportsKeyless) { + if (agent) return !hasRealAppTarget; + + // Auto-keyless is scoped to bootstrap (new-project) flows only in human + // mode. Existing projects keep the authenticated flow so real keys can be + // pulled unless an agent explicitly chooses keyless by omitting an app. + if (!bootstrap) return false; + // Authenticated (OAuth token or CLERK_PLATFORM_API_KEY) — use the // authenticated flow so real keys get pulled into .env. Otherwise fall // back to keyless: the app runs on auto-generated dev keys and the user @@ -253,19 +273,33 @@ function resolveKeylessMode( // --- Auth --- -async function resolveAuthLabel(): Promise { +async function resolveAuthLabel({ + allowInteractiveLogin, +}: { + allowInteractiveLogin: boolean; +}): Promise { const hasApiKey = Boolean(process.env.CLERK_PLATFORM_API_KEY); if (hasApiKey) return "Using API key"; const email = await getAuthenticatedEmail(); if (email) return `Logged in as ${email}`; + if (!allowInteractiveLogin) { + throwUsageError( + "clerk init in agent mode requires CLERK_PLATFORM_API_KEY or a valid stored session when using a real Clerk application. Pass --app only with auth available, or omit --app for keyless-capable frameworks.", + ); + } + await login({ showNextSteps: false }); return ""; } -async function authenticateAndLink(cwd: string, app: string | undefined): Promise { - const label = await resolveAuthLabel(); +async function authenticateAndLink( + cwd: string, + app: string | undefined, + options: { allowInteractiveLogin: boolean }, +): Promise { + const label = await resolveAuthLabel(options); const profile = await resolveProfile(cwd); const alreadyOnRequestedApp = profile && (!app || profile.profile.appId === app); diff --git a/packages/cli-core/src/test/integration/agent-mode.test.ts b/packages/cli-core/src/test/integration/agent-mode.test.ts index bf7b4b60..b983eb06 100644 --- a/packages/cli-core/src/test/integration/agent-mode.test.ts +++ b/packages/cli-core/src/test/integration/agent-mode.test.ts @@ -4,9 +4,73 @@ */ import { test, expect } from "bun:test"; -import { useIntegrationTestHarness, http, clerk, readConfig, MOCK_APP } from "./lib/harness.ts"; +import { mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { + useIntegrationTestHarness, + http, + clerk, + readConfig, + MOCK_APP, + getInstance, + parseEnvFile, +} from "./lib/harness.ts"; -useIntegrationTestHarness(); +const h = useIntegrationTestHarness(); + +async function writeNextAppProject() { + await Bun.write( + join(h.tempDir, "package.json"), + JSON.stringify({ + name: "test-next-app", + dependencies: { + "@clerk/nextjs": "latest", + next: "15.0.0", + react: "19.0.0", + "react-dom": "19.0.0", + }, + }), + ); + await Bun.write(join(h.tempDir, "tsconfig.json"), "{}"); + await mkdir(join(h.tempDir, "app"), { recursive: true }); + await Bun.write( + join(h.tempDir, "app/layout.tsx"), + `export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} +`, + ); +} + +async function writeReactProject() { + await Bun.write( + join(h.tempDir, "package.json"), + JSON.stringify({ + name: "test-react-app", + dependencies: { + "@clerk/react": "latest", + "@vitejs/plugin-react": "latest", + vite: "6.0.0", + react: "19.0.0", + "react-dom": "19.0.0", + }, + }), + ); + await Bun.write(join(h.tempDir, "tsconfig.json"), "{}"); + await mkdir(join(h.tempDir, "src"), { recursive: true }); + await Bun.write( + join(h.tempDir, "src/main.tsx"), + `import ReactDOM from "react-dom/client"; +import App from "./App"; + +ReactDOM.createRoot(document.getElementById("root")!).render(); +`, + ); +} test("init --prompt outputs structured agent prompt without API calls", async () => { const { stdout } = await clerk("init", "--prompt"); @@ -45,3 +109,50 @@ test("unlink --yes removes the profile in agent mode", async () => { const config = await readConfig(); expect(config.profiles["github.com/test/project"]).toBeUndefined(); }); + +test("init uses keyless for keyless framework without an app target in agent mode", async () => { + await writeNextAppProject(); + http.mock({ + "/v1/accountless_applications": { + publishable_key: "pk_test_keyless", + secret_key: "sk_test_keyless", + claim_url: "/apps/claim?token=keyless_token", + }, + }); + + await clerk("--mode", "agent", "init", "--no-skills"); + + const env = parseEnvFile(await Bun.file(join(h.tempDir, ".env.local")).text(), ".env.local"); + expect(env.get("NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY")).toBe("pk_test_keyless"); + expect(env.get("CLERK_SECRET_KEY")).toBe("sk_test_keyless"); + + const config = await readConfig(); + expect(config.profiles["github.com/test/project"]).toBeUndefined(); + expect(http.requests.some((r) => r.url.includes("/v1/platform/applications"))).toBe(false); +}); + +test("init prints manual setup for non-keyless framework without an app target in agent mode", async () => { + await writeReactProject(); + + const { stderr } = await clerk("--mode", "agent", "init", "--no-skills"); + + expect(stderr).toContain("clerk init --app "); + expect(http.requests).toHaveLength(0); +}); + +test("init with --app uses real app flow in agent mode", async () => { + await writeReactProject(); + const devInstance = getInstance(MOCK_APP, "development"); + http.mock({ + [`/applications/${MOCK_APP.application_id}`]: MOCK_APP, + }); + + await clerk("--mode", "agent", "init", "--app", MOCK_APP.application_id, "--no-skills"); + + const config = await readConfig(); + expect(config.profiles["github.com/test/project"]?.appId).toBe(MOCK_APP.application_id); + + const env = parseEnvFile(await Bun.file(join(h.tempDir, ".env.local")).text(), ".env.local"); + expect(env.get("VITE_CLERK_PUBLISHABLE_KEY")).toBe(devInstance.publishable_key); + expect(env.get("CLERK_SECRET_KEY")).toBe(devInstance.secret_key); +}); diff --git a/skills/clerk/SKILL.md b/skills/clerk/SKILL.md index c4362d72..61083c2e 100644 --- a/skills/clerk/SKILL.md +++ b/skills/clerk/SKILL.md @@ -198,6 +198,7 @@ The CLI auto-detects agent mode when stdout is not a TTY, or when `--mode agent` - **Host-sensitive operations emit a sandbox warning once per invocation.** Home-directory Clerk state, keychain access, networked Clerk calls, browser launch, and localhost OAuth callback setup can trigger the warning shown above. If it appears, rerun the same command on the host before trusting the result. - **If your harness does not clearly present as agent mode, force it.** Use `--mode agent` or `CLERK_MODE=agent` when you want the CLI's non-interactive behavior and sandbox warning path to apply deterministically. - **`link` supports deterministic agent flows.** In agent mode, `clerk link --app ` links directly. Without `--app`, the CLI will try silent key-based autolink first; if it cannot determine the app unambiguously, it exits and tells you to pass `--app`. +- **`init` never selects or creates a real Clerk app for you in agent mode.** For keyless-capable frameworks, omit `--app` to use keyless. For frameworks that require API keys, pass `--app ` or link the project first. - **`unlink` requires `--yes` in agent mode.** This preserves the same safety bar as other destructive commands while still letting an agent complete the unlink non-interactively. - **Mutations still require `--yes`** unless you accept per-call confirmation is impossible. - **`doctor --fix` is ignored.** Parse `doctor --json` output's `remedy` field and act on it yourself. diff --git a/skills/clerk/references/agent-mode.md b/skills/clerk/references/agent-mode.md index 8ccb990d..dae9c561 100644 --- a/skills/clerk/references/agent-mode.md +++ b/skills/clerk/references/agent-mode.md @@ -48,24 +48,24 @@ Force human mode with `--mode human` or `CLERK_MODE=human`. Typical AI-agent inv ## What changes in agent mode -| Behavior | Human mode | Agent mode | -| ---------------------------------------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Interactive pickers (`link` without `--app`, `api` with no args) | Show a TUI picker | Print structured guidance and exit, or auto-resolve | -| `clerk link --app ` | Links directly | Links directly | -| `clerk link` without `--app` | Interactive picker / create UI | Tries silent autolink from detected publishable keys; if no deterministic match exists, exits with a usage error telling the caller to pass `--app` | -| Confirmation prompts (`unlink`, `config patch`, `api -X DELETE`) | Prompt y/n | Require `--yes`, otherwise error | -| `clerk doctor --fix` | Interactively offers fixes | **Ignored**; output the `remedy` field and let the caller act | -| `clerk apps list` default output | Table | JSON (when piped) | -| `clerk apps create ` output | Human-readable summary | JSON (auto-detected, same as `apps list`); `--json` also works explicitly | -| `clerk open [subpath]` | Opens the browser to the URL | Does not open a browser. Prints a JSON descriptor (`{url, appId, appName, instanceId, instanceLabel, subpath, opened: false}`) on stdout so the agent can surface it | -| `clerk auth login` when already authenticated | Prompt to re-auth | Silent no-op | -| `clerk init` | Full interactive scaffold flow | Skips the interactive scaffold and either runs non-interactively with `--yes` or, with `--prompt`, emits a short agent handoff pointing the agent at `clerk init -y`. | -| Color / spinners | Enabled | Disabled | +| Behavior | Human mode | Agent mode | +| ---------------------------------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Interactive pickers (`link` without `--app`, `api` with no args) | Show a TUI picker | Print structured guidance and exit, or auto-resolve | +| `clerk link --app ` | Links directly | Links directly | +| `clerk link` without `--app` | Interactive picker / create UI | Tries silent autolink from detected publishable keys; if no deterministic match exists, exits with a usage error telling the caller to pass `--app` | +| Confirmation prompts (`unlink`, `config patch`, `api -X DELETE`) | Prompt y/n | Require `--yes`, otherwise error | +| `clerk doctor --fix` | Interactively offers fixes | **Ignored**; output the `remedy` field and let the caller act | +| `clerk apps list` default output | Table | JSON (when piped) | +| `clerk apps create ` output | Human-readable summary | JSON (auto-detected, same as `apps list`); `--json` also works explicitly | +| `clerk open [subpath]` | Opens the browser to the URL | Does not open a browser. Prints a JSON descriptor (`{url, appId, appName, instanceId, instanceLabel, subpath, opened: false}`) on stdout so the agent can surface it | +| `clerk auth login` when already authenticated | Prompt to re-auth | Silent no-op | +| `clerk init` | Full interactive scaffold flow | Runs non-interactively. With no `--app`/linked profile, keyless-capable frameworks use keyless and non-keyless frameworks print manual setup guidance. | +| Color / spinners | Enabled | Disabled | In addition, sandboxed agent-mode invocations may emit the warning above once per CLI invocation when a host-sensitive operation is blocked. -**Rule of thumb:** always pass `--yes` for mutations, `--json` for structured output where available, and `--app` / `--instance` explicitly instead of relying on pickers. +**Rule of thumb:** always pass `--yes` for mutations and `--json` for structured output where available. Pass `--app` / `--instance` when you intentionally target a real app; omit `--app` for keyless `clerk init` flows. ## Passing options as JSON: `--input-json`