From 5ecb65cbbed94d7c6b0e76cc12d0b23a01cef88e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 2 Jan 2026 13:46:48 +0100 Subject: [PATCH] fix: persist gateway token for local CLI auth --- CHANGELOG.md | 1 + docs/configuration.md | 3 ++- docs/onboarding.md | 4 ++++ docs/wizard.md | 1 + src/commands/onboard-interactive.ts | 19 ++++++++++++++++--- src/config/config.ts | 3 +++ src/gateway/auth.ts | 2 +- src/gateway/call.ts | 16 +++++++++++----- src/gateway/server.ts | 4 ++-- 9 files changed, 41 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd4368499..2e465db48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ ### Fixes - Chat UI: keep the chat scrolled to the latest message after switching sessions. +- CLI onboarding: persist gateway token in config so local CLI auth works; recommend auth Off unless you need multi-machine access. - Chat UI: add extra top padding before the first message bubble in Web Chat (macOS/iOS/Android). - Control UI: refine Web Chat session selector styling (chevron spacing + background). - WebChat: stream live updates for sessions even when runs start outside the chat UI. diff --git a/docs/configuration.md b/docs/configuration.md index 5a90a0a53..7d7f30964 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -555,7 +555,7 @@ Defaults: mode: "local", // or "remote" bind: "loopback", // controlUi: { enabled: true } - // auth: { mode: "token" | "password" } + // auth: { mode: "token", token: "your-token" } // token is for multi-machine CLI access // tailscale: { mode: "off" | "serve" | "funnel" } } } @@ -566,6 +566,7 @@ Notes: Auth and Tailscale: - `gateway.auth.mode` sets the handshake requirements (`token` or `password`). +- `gateway.auth.token` stores the shared token for token auth (used by the CLI on the same machine). - When `gateway.auth.mode` is set, only that method is accepted (plus optional Tailscale headers). - `gateway.auth.password` can be set here, or via `CLAWDIS_GATEWAY_PASSWORD` (recommended). - `gateway.auth.allowTailscale` controls whether Tailscale identity headers can satisfy auth. diff --git a/docs/onboarding.md b/docs/onboarding.md index ee11f6dcf..f47d786e3 100644 --- a/docs/onboarding.md +++ b/docs/onboarding.md @@ -22,6 +22,10 @@ First question: where does the **Gateway** run? - **Local (this Mac):** onboarding can run the Anthropic OAuth flow and write the Clawdis token store locally. - **Remote (over SSH/tailnet):** onboarding must not run OAuth locally, because credentials must exist on the **gateway host**. +Gateway auth tip: +- If you only use Clawdis on this Mac (loopback gateway), keep auth **Off**. +- Use **Token** for multi-machine access or non-loopback binds. + Implementation note (2025-12-19): in local mode, the macOS app bundles the Gateway and enables it via a per-user launchd LaunchAgent (no global npm install/Node requirement for the user). ## 2) Local-only: Connect Claude (Anthropic OAuth) diff --git a/docs/wizard.md b/docs/wizard.md index 3e319fd6d..2b84c6ef7 100644 --- a/docs/wizard.md +++ b/docs/wizard.md @@ -58,6 +58,7 @@ It does **not** install or change anything on the remote host. 4) **Gateway** - Port, bind, auth mode, tailscale exposure. + - Auth recommendation: keep **Off** for single-machine loopback setups. Use **Token** for multi-machine access or non-loopback binds. - Non‑loopback binds require auth. 5) **Providers** diff --git a/src/commands/onboard-interactive.ts b/src/commands/onboard-interactive.ts index 39f9c4a53..3edf022bc 100644 --- a/src/commands/onboard-interactive.ts +++ b/src/commands/onboard-interactive.ts @@ -280,8 +280,16 @@ export async function runInteractiveOnboarding( await select({ message: "Gateway auth", options: [ - { value: "off", label: "Off (loopback only)" }, - { value: "token", label: "Token" }, + { + value: "off", + label: "Off (loopback only)", + hint: "Recommended for single-machine setups", + }, + { + value: "token", + label: "Token", + hint: "Use for multi-machine access or non-loopback binds", + }, { value: "password", label: "Password" }, ], }), @@ -344,6 +352,7 @@ export async function runInteractiveOnboarding( const tokenInput = guardCancel( await text({ message: "Gateway token (blank to generate)", + placeholder: "Needed for multi-machine or non-loopback access", initialValue: randomToken(), }), runtime, @@ -375,7 +384,11 @@ export async function runInteractiveOnboarding( ...nextConfig, gateway: { ...nextConfig.gateway, - auth: { ...nextConfig.gateway?.auth, mode: "token" }, + auth: { + ...nextConfig.gateway?.auth, + mode: "token", + token: gatewayToken, + }, }, }; } diff --git a/src/config/config.ts b/src/config/config.ts index 3525ebcf7..cd979570b 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -351,6 +351,8 @@ export type GatewayAuthMode = "token" | "password"; export type GatewayAuthConfig = { /** Authentication mode for Gateway connections. Defaults to token when set. */ mode?: GatewayAuthMode; + /** Shared token for token mode (stored locally for CLI auth). */ + token?: string; /** Shared password for password mode (consider env instead). */ password?: string; /** Allow Tailscale identity headers when serve mode is enabled. */ @@ -1097,6 +1099,7 @@ const ClawdisSchema = z.object({ auth: z .object({ mode: z.union([z.literal("token"), z.literal("password")]).optional(), + token: z.string().optional(), password: z.string().optional(), allowTailscale: z.boolean().optional(), }) diff --git a/src/gateway/auth.ts b/src/gateway/auth.ts index cb179e7d5..b2ec9324e 100644 --- a/src/gateway/auth.ts +++ b/src/gateway/auth.ts @@ -101,7 +101,7 @@ function isTailscaleProxyRequest(req?: IncomingMessage): boolean { export function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void { if (auth.mode === "token" && !auth.token) { throw new Error( - "gateway auth mode is token, but CLAWDIS_GATEWAY_TOKEN is not set", + "gateway auth mode is token, but no token was configured (set gateway.auth.token or CLAWDIS_GATEWAY_TOKEN)", ); } if (auth.mode === "password" && !auth.password) { diff --git a/src/gateway/call.ts b/src/gateway/call.ts index 4fc92e303..599638b4e 100644 --- a/src/gateway/call.ts +++ b/src/gateway/call.ts @@ -25,8 +25,8 @@ export async function callGateway( ): Promise { const timeoutMs = opts.timeoutMs ?? 10_000; const config = loadConfig(); - const remote = - config.gateway?.mode === "remote" ? config.gateway.remote : undefined; + const isRemoteMode = config.gateway?.mode === "remote"; + const remote = isRemoteMode ? config.gateway.remote : undefined; const url = (typeof opts.url === "string" && opts.url.trim().length > 0 ? opts.url.trim() @@ -39,9 +39,15 @@ export async function callGateway( (typeof opts.token === "string" && opts.token.trim().length > 0 ? opts.token.trim() : undefined) || - (typeof remote?.token === "string" && remote.token.trim().length > 0 - ? remote.token.trim() - : undefined); + (isRemoteMode + ? typeof remote?.token === "string" && remote.token.trim().length > 0 + ? remote.token.trim() + : undefined + : process.env.CLAWDIS_GATEWAY_TOKEN?.trim() || + (typeof config.gateway?.auth?.token === "string" && + config.gateway.auth.token.trim().length > 0 + ? config.gateway.auth.token.trim() + : undefined)); const password = (typeof opts.password === "string" && opts.password.trim().length > 0 ? opts.password.trim() diff --git a/src/gateway/server.ts b/src/gateway/server.ts index d29974391..e3b89bf2e 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -660,7 +660,6 @@ type DedupeEntry = { error?: ErrorShape; }; -const getGatewayToken = () => process.env.CLAWDIS_GATEWAY_TOKEN; function formatForLog(value: unknown): string { try { @@ -1371,7 +1370,8 @@ export async function startGatewayServer( ...tailscaleOverrides, }; const tailscaleMode = tailscaleConfig.mode ?? "off"; - const token = getGatewayToken(); + const token = + authConfig.token ?? process.env.CLAWDIS_GATEWAY_TOKEN ?? undefined; const password = authConfig.password ?? process.env.CLAWDIS_GATEWAY_PASSWORD ?? undefined; const authMode: ResolvedGatewayAuth["mode"] =