From 63698333ff2ffda45889587f63a00db7272c174d Mon Sep 17 00:00:00 2001 From: Trey T Date: Sun, 3 May 2026 20:53:46 -0500 Subject: [PATCH] feat(settings): make Claude Code OAuth token configurable in UI The OAuth token that authenticates the spawned `claude` CLI was only readable from the container env, so an expired token meant editing .env on the Unraid host and rebuilding. Now it can be rotated from the Settings page like every other key. - Adds CLAUDE_CODE_OAUTH_TOKEN to the settings registry and a "Claude" card at the top of the settings UI. - loadPipelineEnv() injects the DB value into every spawned subprocess env (overrides the container env), covering both campaign launches and chat sessions. - checkIntegrationStatus() validates the token by hitting the Anthropic messages API with a 1-token call, surfacing 401s as "Token expired or invalid" instead of a generic "Not connected". Co-Authored-By: Claude Opus 4.7 (1M context) --- app/(dashboard)/settings/page.tsx | 12 +++++++++ lib/claude.ts | 1 + lib/settings.ts | 42 +++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+) diff --git a/app/(dashboard)/settings/page.tsx b/app/(dashboard)/settings/page.tsx index 41d8450..8ec890c 100644 --- a/app/(dashboard)/settings/page.tsx +++ b/app/(dashboard)/settings/page.tsx @@ -28,6 +28,13 @@ interface SettingConfig { } const SETTINGS_GROUPS: SettingsGroup[] = [ + { + name: "Claude", + description: + "OAuth access token for the Claude Code CLI subprocess that drives every pipeline agent. Tokens expire — refresh here when launches start failing with auth errors.", + docsUrl: "https://docs.claude.com/en/docs/claude-code/setup", + keys: ["CLAUDE_CODE_OAUTH_TOKEN"], + }, { name: "Postiz", description: @@ -59,6 +66,11 @@ const SETTINGS_GROUPS: SettingsGroup[] = [ ]; const SETTINGS_CONFIG: Record = { + CLAUDE_CODE_OAUTH_TOKEN: { + label: "Claude Code OAuth Token", + placeholder: "sk-ant-oat01-...", + secret: true, + }, POSTIZ_URL: { label: "Postiz URL", placeholder: "http://localhost:5000" }, POSTIZ_API_KEY: { label: "Postiz API Key", diff --git a/lib/claude.ts b/lib/claude.ts index c361788..fb80d2b 100644 --- a/lib/claude.ts +++ b/lib/claude.ts @@ -663,6 +663,7 @@ async function loadPipelineEnv(): Promise> { const settings = await getAllSettings(); const env: Record = {}; + if (settings.CLAUDE_CODE_OAUTH_TOKEN) env.CLAUDE_CODE_OAUTH_TOKEN = settings.CLAUDE_CODE_OAUTH_TOKEN; if (settings.TAVILY_API_KEY) env.TAVILY_API_KEY = settings.TAVILY_API_KEY; if (settings.POSTIZ_URL) env.POSTIZ_URL = settings.POSTIZ_URL; if (settings.POSTIZ_API_KEY) env.POSTIZ_API_KEY = settings.POSTIZ_API_KEY; diff --git a/lib/settings.ts b/lib/settings.ts index 1f03b0d..fd55133 100644 --- a/lib/settings.ts +++ b/lib/settings.ts @@ -2,6 +2,9 @@ import { prisma } from "./prisma"; // Settings keys and their env var fallbacks const SETTINGS_KEYS = { + // Claude Code CLI authentication (OAuth access token from Claude Max) + CLAUDE_CODE_OAUTH_TOKEN: { envVar: "CLAUDE_CODE_OAUTH_TOKEN", label: "Claude Code OAuth Token", placeholder: "sk-ant-oat01-...", secret: true }, + // Postiz POSTIZ_URL: { envVar: "POSTIZ_URL", label: "Postiz URL", placeholder: "http://localhost:5000" }, POSTIZ_API_KEY: { envVar: "POSTIZ_API_KEY", label: "Postiz API Key", placeholder: "your-postiz-api-key", secret: true }, @@ -23,6 +26,12 @@ export const SETTINGS_CONFIG = SETTINGS_KEYS; // Grouped for UI export const SETTINGS_GROUPS = [ + { + name: "Claude", + description: "OAuth access token for the Claude Code CLI subprocess that drives every pipeline agent. Get one with `claude setup-token` then `security find-generic-password -s 'Claude Code-credentials' -a $(whoami) -w` and use the `claudeAiOauth.accessToken` field. Tokens expire — refresh here when launches start failing with auth errors.", + docsUrl: "https://docs.claude.com/en/docs/claude-code/setup", + keys: ["CLAUDE_CODE_OAUTH_TOKEN"] as SettingKey[], + }, { name: "Postiz", description: "Self-hosted social media scheduling. Handles Instagram and TikTok publishing.", @@ -100,6 +109,39 @@ export async function checkIntegrationStatus(): Promise = {}; + // Claude OAuth token — validate by hitting the messages API with a 1-token call. + // A valid OAuth token returns 200; an expired/invalid one returns 401. + if (settings.CLAUDE_CODE_OAUTH_TOKEN) { + try { + const res = await fetch("https://api.anthropic.com/v1/messages", { + method: "POST", + headers: { + "Content-Type": "application/json", + "anthropic-version": "2023-06-01", + "anthropic-beta": "oauth-2025-04-20", + Authorization: `Bearer ${settings.CLAUDE_CODE_OAUTH_TOKEN}`, + }, + body: JSON.stringify({ + model: "claude-haiku-4-5-20251001", + max_tokens: 1, + messages: [{ role: "user", content: "." }], + }), + signal: AbortSignal.timeout(10000), + }); + if (res.ok) { + status.claude = { connected: true }; + } else if (res.status === 401) { + status.claude = { connected: false, error: "Token expired or invalid" }; + } else { + status.claude = { connected: false, error: `HTTP ${res.status}` }; + } + } catch (e) { + status.claude = { connected: false, error: e instanceof Error ? e.message : "Connection failed" }; + } + } else { + status.claude = { connected: false, error: "Not configured" }; + } + // Postiz if (settings.POSTIZ_URL && settings.POSTIZ_API_KEY) { try {