diff --git a/docs/cli/index.md b/docs/cli/index.md
index 5abfdf62c..f8eaf41db 100644
--- a/docs/cli/index.md
+++ b/docs/cli/index.md
@@ -177,7 +177,11 @@ Options:
- `--workspace
`
- `--non-interactive`
- `--mode `
-- `--auth-choice `
+- `--auth-choice `
+- `--token-provider ` (non-interactive; used with `--auth-choice token`)
+- `--token ` (non-interactive; used with `--auth-choice token`)
+- `--token-profile-id ` (non-interactive; default: `:manual`)
+- `--token-expires-in ` (non-interactive; e.g. `365d`, `12h`)
- `--anthropic-api-key `
- `--openai-api-key `
- `--gemini-api-key `
diff --git a/docs/gateway/authentication.md b/docs/gateway/authentication.md
index 8c6b4d705..b591651f4 100644
--- a/docs/gateway/authentication.md
+++ b/docs/gateway/authentication.md
@@ -29,6 +29,19 @@ clawdbot models status
clawdbot doctor
```
+Alternative: run the wrapper (also updates Clawdbot config):
+
+```bash
+clawdbot models auth setup-token --provider anthropic
+```
+
+Manual token entry (any provider; writes `auth-profiles.json` + updates config):
+
+```bash
+clawdbot models auth paste-token --provider anthropic
+clawdbot models auth paste-token --provider openrouter
+```
+
## Recommended: long‑lived Claude Code token
Run this on the **gateway host** (the machine running the Gateway):
@@ -92,13 +105,15 @@ Use `--agent ` to target a specific agent; omit it to use the configured def
2. **Clawdbot** syncs those into
`~/.clawdbot/agents//agent/auth-profiles.json` when the auth store is
loaded.
-3. OAuth refresh happens automatically on use if a token is expired.
+3. Refreshable OAuth profiles can be refreshed automatically on use. Static
+ token profiles (including Claude CLI setup-token) are not refreshable by
+ Clawdbot.
## Troubleshooting
### “No credentials found”
-If the Anthropic OAuth profile is missing, run `claude setup-token` on the
+If the Anthropic token profile is missing, run `claude setup-token` on the
**gateway host**, then re-check:
```bash
diff --git a/src/cli/program.ts b/src/cli/program.ts
index 1124d7847..3b8292195 100644
--- a/src/cli/program.ts
+++ b/src/cli/program.ts
@@ -240,7 +240,23 @@ export function buildProgram() {
.option("--mode ", "Wizard mode: local|remote")
.option(
"--auth-choice ",
- "Auth: oauth|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip",
+ "Auth: setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip",
+ )
+ .option(
+ "--token-provider ",
+ "Token provider id (non-interactive; used with --auth-choice token)",
+ )
+ .option(
+ "--token ",
+ "Token value (non-interactive; used with --auth-choice token)",
+ )
+ .option(
+ "--token-profile-id ",
+ "Auth profile id (non-interactive; default: :manual)",
+ )
+ .option(
+ "--token-expires-in ",
+ "Optional token expiry duration (e.g. 365d, 12h)",
)
.option("--anthropic-api-key ", "Anthropic API key")
.option("--openai-api-key ", "OpenAI API key")
@@ -270,6 +286,7 @@ export function buildProgram() {
mode: opts.mode as "local" | "remote" | undefined,
authChoice: opts.authChoice as
| "oauth"
+ | "setup-token"
| "claude-cli"
| "token"
| "openai-codex"
@@ -282,6 +299,10 @@ export function buildProgram() {
| "minimax"
| "skip"
| undefined,
+ tokenProvider: opts.tokenProvider as string | undefined,
+ token: opts.token as string | undefined,
+ tokenProfileId: opts.tokenProfileId as string | undefined,
+ tokenExpiresIn: opts.tokenExpiresIn as string | undefined,
anthropicApiKey: opts.anthropicApiKey as string | undefined,
openaiApiKey: opts.openaiApiKey as string | undefined,
geminiApiKey: opts.geminiApiKey as string | undefined,
diff --git a/src/commands/auth-choice-options.ts b/src/commands/auth-choice-options.ts
index c4203b5d5..96855f9e0 100644
--- a/src/commands/auth-choice-options.ts
+++ b/src/commands/auth-choice-options.ts
@@ -64,17 +64,23 @@ export function buildAuthChoiceOptions(params: {
if (claudeCli?.type === "oauth" || claudeCli?.type === "token") {
options.push({
value: "claude-cli",
- label: "Anthropic OAuth (Claude CLI)",
+ label: "Anthropic token (Claude CLI)",
hint: formatOAuthHint(claudeCli.expires),
});
} else if (params.includeClaudeCliIfMissing && platform === "darwin") {
options.push({
value: "claude-cli",
- label: "Anthropic OAuth (Claude CLI)",
+ label: "Anthropic token (Claude CLI)",
hint: "requires Keychain access",
});
}
+ options.push({
+ value: "setup-token",
+ label: "Anthropic token (run setup-token)",
+ hint: "Runs `claude setup-token`",
+ });
+
options.push({
value: "token",
label: "Anthropic token (paste setup-token)",
diff --git a/src/commands/auth-choice.ts b/src/commands/auth-choice.ts
index 897ae3003..499f332f3 100644
--- a/src/commands/auth-choice.ts
+++ b/src/commands/auth-choice.ts
@@ -216,7 +216,68 @@ export async function applyAuthChoice(params: {
provider: "anthropic",
mode: "token",
});
- } else if (params.authChoice === "token" || params.authChoice === "oauth") {
+ } else if (
+ params.authChoice === "setup-token" ||
+ params.authChoice === "oauth"
+ ) {
+ await params.prompter.note(
+ [
+ "This will run `claude setup-token` to create a long-lived Anthropic token.",
+ "Requires an interactive TTY and a Claude Pro/Max subscription.",
+ ].join("\n"),
+ "Anthropic setup-token",
+ );
+
+ if (!process.stdin.isTTY) {
+ await params.prompter.note(
+ "`claude setup-token` requires an interactive TTY.",
+ "Anthropic setup-token",
+ );
+ return { config: nextConfig, agentModelOverride };
+ }
+
+ const proceed = await params.prompter.confirm({
+ message: "Run `claude setup-token` now?",
+ initialValue: true,
+ });
+ if (!proceed) return { config: nextConfig, agentModelOverride };
+
+ const res = await (async () => {
+ const { spawnSync } = await import("node:child_process");
+ return spawnSync("claude", ["setup-token"], { stdio: "inherit" });
+ })();
+ if (res.error) {
+ await params.prompter.note(
+ `Failed to run claude: ${String(res.error)}`,
+ "Anthropic setup-token",
+ );
+ return { config: nextConfig, agentModelOverride };
+ }
+ if (typeof res.status === "number" && res.status !== 0) {
+ await params.prompter.note(
+ `claude setup-token failed (exit ${res.status})`,
+ "Anthropic setup-token",
+ );
+ return { config: nextConfig, agentModelOverride };
+ }
+
+ const store = ensureAuthProfileStore(params.agentDir, {
+ allowKeychainPrompt: true,
+ });
+ if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
+ await params.prompter.note(
+ `No Claude CLI credentials found after setup-token. Expected ${CLAUDE_CLI_PROFILE_ID}.`,
+ "Anthropic setup-token",
+ );
+ return { config: nextConfig, agentModelOverride };
+ }
+
+ nextConfig = applyAuthProfileConfig(nextConfig, {
+ profileId: CLAUDE_CLI_PROFILE_ID,
+ provider: "anthropic",
+ mode: "token",
+ });
+ } else if (params.authChoice === "token") {
const provider = (await params.prompter.select({
message: "Token provider",
options: [{ value: "anthropic", label: "Anthropic (only supported)" }],
diff --git a/src/commands/configure.ts b/src/commands/configure.ts
index 2ff885478..a50f6cd4f 100644
--- a/src/commands/configure.ts
+++ b/src/commands/configure.ts
@@ -352,6 +352,7 @@ async function promptAuthConfig(
runtime,
) as
| "oauth"
+ | "setup-token"
| "claude-cli"
| "token"
| "openai-codex"
@@ -403,7 +404,68 @@ async function promptAuthConfig(
provider: "anthropic",
mode: "token",
});
- } else if (authChoice === "token" || authChoice === "oauth") {
+ } else if (authChoice === "setup-token" || authChoice === "oauth") {
+ note(
+ [
+ "This will run `claude setup-token` to create a long-lived Anthropic token.",
+ "Requires an interactive TTY and a Claude Pro/Max subscription.",
+ ].join("\n"),
+ "Anthropic setup-token",
+ );
+
+ if (!process.stdin.isTTY) {
+ note(
+ "`claude setup-token` requires an interactive TTY.",
+ "Anthropic setup-token",
+ );
+ return next;
+ }
+
+ const runNow = guardCancel(
+ await confirm({
+ message: "Run `claude setup-token` now?",
+ initialValue: true,
+ }),
+ runtime,
+ );
+ if (!runNow) return next;
+
+ const res = await (async () => {
+ const { spawnSync } = await import("node:child_process");
+ return spawnSync("claude", ["setup-token"], { stdio: "inherit" });
+ })();
+ if (res.error) {
+ note(
+ `Failed to run claude: ${String(res.error)}`,
+ "Anthropic setup-token",
+ );
+ return next;
+ }
+ if (typeof res.status === "number" && res.status !== 0) {
+ note(
+ `claude setup-token failed (exit ${res.status})`,
+ "Anthropic setup-token",
+ );
+ return next;
+ }
+
+ const store = ensureAuthProfileStore(undefined, {
+ allowKeychainPrompt: true,
+ });
+ if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
+ note(
+ `No Claude CLI credentials found after setup-token. Expected ${CLAUDE_CLI_PROFILE_ID}.`,
+ "Anthropic setup-token",
+ );
+ return next;
+ }
+
+ next = applyAuthProfileConfig(next, {
+ profileId: CLAUDE_CLI_PROFILE_ID,
+ provider: "anthropic",
+ mode: "token",
+ });
+ } else if (authChoice === "token") {
const provider = guardCancel(
await select({
message: "Token provider",
@@ -726,6 +788,7 @@ async function promptAuthConfig(
: (next.agents?.defaults?.model?.primary ?? "");
const preferAnthropic =
authChoice === "claude-cli" ||
+ authChoice === "setup-token" ||
authChoice === "token" ||
authChoice === "oauth" ||
authChoice === "apiKey";
diff --git a/src/commands/onboard-non-interactive.ts b/src/commands/onboard-non-interactive.ts
index c27f00eb3..db924e9ff 100644
--- a/src/commands/onboard-non-interactive.ts
+++ b/src/commands/onboard-non-interactive.ts
@@ -1,10 +1,14 @@
+import { spawnSync } from "node:child_process";
import path from "node:path";
import {
CLAUDE_CLI_PROFILE_ID,
CODEX_CLI_PROFILE_ID,
ensureAuthProfileStore,
+ upsertAuthProfile,
} from "../agents/auth-profiles.js";
import { resolveEnvApiKey } from "../agents/model-auth.js";
+import { normalizeProviderId } from "../agents/model-selection.js";
+import { parseDurationMs } from "../cli/parse-duration.js";
import {
type ClawdbotConfig,
CONFIG_PATH_CLAWDBOT,
@@ -206,18 +210,82 @@ export async function runNonInteractiveOnboarding(
nextConfig = applyOpenAICodexModelDefault(nextConfig).next;
} else if (authChoice === "minimax") {
nextConfig = applyMinimaxConfig(nextConfig);
- } else if (
- authChoice === "token" ||
- authChoice === "oauth" ||
- authChoice === "openai-codex" ||
- authChoice === "antigravity"
- ) {
+ } else if (authChoice === "setup-token" || authChoice === "oauth") {
+ if (!process.stdin.isTTY) {
+ runtime.error("`claude setup-token` requires an interactive TTY.");
+ runtime.exit(1);
+ return;
+ }
+
+ const res = spawnSync("claude", ["setup-token"], { stdio: "inherit" });
+ if (res.error) throw res.error;
+ if (typeof res.status === "number" && res.status !== 0) {
+ runtime.error(`claude setup-token failed (exit ${res.status})`);
+ runtime.exit(1);
+ return;
+ }
+
+ const store = ensureAuthProfileStore(undefined, {
+ allowKeychainPrompt: true,
+ });
+ if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
+ runtime.error(
+ `No Claude CLI credentials found after setup-token. Expected auth profile ${CLAUDE_CLI_PROFILE_ID}.`,
+ );
+ runtime.exit(1);
+ return;
+ }
+
+ nextConfig = applyAuthProfileConfig(nextConfig, {
+ profileId: CLAUDE_CLI_PROFILE_ID,
+ provider: "anthropic",
+ mode: "token",
+ });
+ } else if (authChoice === "token") {
+ const providerRaw = opts.tokenProvider?.trim();
+ const tokenRaw = opts.token?.trim();
+ if (!providerRaw) {
+ runtime.error(
+ "Missing --token-provider (required for --auth-choice token).",
+ );
+ runtime.exit(1);
+ return;
+ }
+ if (!tokenRaw) {
+ runtime.error("Missing --token (required for --auth-choice token).");
+ runtime.exit(1);
+ return;
+ }
+
+ const provider = normalizeProviderId(providerRaw);
+ const profileId = (
+ opts.tokenProfileId?.trim() || `${provider}:manual`
+ ).trim();
+ const expires =
+ opts.tokenExpiresIn?.trim() && opts.tokenExpiresIn.trim().length > 0
+ ? Date.now() +
+ parseDurationMs(String(opts.tokenExpiresIn).trim(), {
+ defaultUnit: "d",
+ })
+ : undefined;
+
+ upsertAuthProfile({
+ profileId,
+ credential: {
+ type: "token",
+ provider,
+ token: tokenRaw,
+ ...(expires ? { expires } : {}),
+ },
+ });
+ nextConfig = applyAuthProfileConfig(nextConfig, {
+ profileId,
+ provider,
+ mode: "token",
+ });
+ } else if (authChoice === "openai-codex" || authChoice === "antigravity") {
const label =
- authChoice === "antigravity"
- ? "Antigravity"
- : authChoice === "token"
- ? "Token"
- : "OAuth";
+ authChoice === "antigravity" ? "Antigravity" : "OpenAI Codex OAuth";
runtime.error(`${label} requires interactive mode.`);
runtime.exit(1);
return;
diff --git a/src/commands/onboard-types.ts b/src/commands/onboard-types.ts
index 3ebbe85a9..c52f7d99a 100644
--- a/src/commands/onboard-types.ts
+++ b/src/commands/onboard-types.ts
@@ -3,7 +3,9 @@ import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
export type OnboardMode = "local" | "remote";
export type AuthChoice =
+ // Legacy alias for `setup-token` (kept for backwards CLI compatibility).
| "oauth"
+ | "setup-token"
| "claude-cli"
| "token"
| "openai-codex"
@@ -27,6 +29,14 @@ export type OnboardOptions = {
workspace?: string;
nonInteractive?: boolean;
authChoice?: AuthChoice;
+ /** Used when `authChoice=token` in non-interactive mode. */
+ tokenProvider?: string;
+ /** Used when `authChoice=token` in non-interactive mode. */
+ token?: string;
+ /** Used when `authChoice=token` in non-interactive mode. */
+ tokenProfileId?: string;
+ /** Used when `authChoice=token` in non-interactive mode. */
+ tokenExpiresIn?: string;
anthropicApiKey?: string;
openaiApiKey?: string;
geminiApiKey?: string;
diff --git a/src/commands/onboard.ts b/src/commands/onboard.ts
index d1b55e6af..2608e80c1 100644
--- a/src/commands/onboard.ts
+++ b/src/commands/onboard.ts
@@ -10,6 +10,11 @@ export async function onboardCommand(
runtime: RuntimeEnv = defaultRuntime,
) {
assertSupportedRuntime(runtime);
+ const authChoice =
+ opts.authChoice === "oauth" ? ("setup-token" as const) : opts.authChoice;
+ const normalizedOpts =
+ authChoice === opts.authChoice ? opts : { ...opts, authChoice };
+
if (process.platform === "win32") {
runtime.log(
[
@@ -20,12 +25,12 @@ export async function onboardCommand(
);
}
- if (opts.nonInteractive) {
- await runNonInteractiveOnboarding(opts, runtime);
+ if (normalizedOpts.nonInteractive) {
+ await runNonInteractiveOnboarding(normalizedOpts, runtime);
return;
}
- await runInteractiveOnboarding(opts, runtime);
+ await runInteractiveOnboarding(normalizedOpts, runtime);
}
export type { OnboardOptions } from "./onboard-types.js";