import { useState } from "react"; import Button from "../ui/Button"; import Modal from "../ui/Modal"; import StatusIndicator, { type StatusTone } from "../ui/StatusIndicator"; import { selectClass } from "../ui/Field"; import ClaudeAuthModal from "./ClaudeAuthModal"; import { clearClaudeToken } from "../../lib/tauri-commands"; import { useProjects } from "../../hooks/useProjects"; import { useAppState } from "../../store/appState"; import { authErrorMessage, useClaudeTokenStatus } from "../../hooks/useClaudeAuth"; const STATUS_DISPLAY: Record< string, { tone: StatusTone; label: string; detail: string } > = { checking: { tone: "unknown", label: "Checking", detail: "Looking for a stored token in the OS keychain.", }, stored: { tone: "ok", label: "Authenticated", detail: "A shared token is stored. Anthropic-backend projects use it from their next container start.", }, absent: { tone: "off", label: "Not authenticated", detail: "No shared token yet, so each Anthropic-backend project still needs its own `claude login`.", }, unavailable: { tone: "error", label: "Unknown", detail: "The OS keychain could not be read.", }, }; /** * Host-level control for the one long-lived Claude Code token shared by every * project. Acquisition needs a running container to run the CLI in, so the * user picks which project lends one. */ export default function SharedAuthSettings() { const { projects } = useProjects(); const pushToast = useAppState((s) => s.pushToast); const { status, error, refresh } = useClaudeTokenStatus(); const [pickedId, setPickedId] = useState(null); const [authOpen, setAuthOpen] = useState(false); const [confirmRevoke, setConfirmRevoke] = useState(false); const [revoking, setRevoking] = useState(false); // `claude setup-token` runs inside a container, so only running projects can // host the flow. const runnable = projects.filter( (p) => p.status === "running" && p.container_id !== null, ); const host = runnable.find((p) => p.id === pickedId) ?? runnable[0] ?? null; const display = STATUS_DISPLAY[status]; const handleRevoke = async () => { setRevoking(true); try { await clearClaudeToken(); setConfirmRevoke(false); await refresh(); pushToast({ kind: "success", message: "Shared Claude token removed from the keychain.", }); } catch (e) { pushToast({ kind: "error", message: "Could not remove the shared Claude token.", detail: authErrorMessage( e, "The OS keychain rejected the delete. The token may still be stored.", ), }); } finally { setRevoking(false); } }; return (
Shared Claude authentication

Authenticate once and every project on the Anthropic backend signs in with that token, instead of each container running its own{" "} claude login. The token is held in your OS keychain and injected into containers as an environment variable.

{display.detail}

{error &&

{error}

}
{runnable.length > 1 && (
)}
{status === "stored" && ( )}
{!host && (

No project is running. Signing in runs{" "} claude setup-token inside a container, so start a project first — any one will do, it only lends its container.

)} {host && (

The sign-in runs in{" "} {host.name}’s container, but the resulting token is shared by all projects.

)} {authOpen && host && ( setAuthOpen(false)} onAuthenticated={() => { void refresh(); }} /> )} {confirmRevoke && ( setConfirmRevoke(false)} footer={ <> } >

This deletes the shared token from your OS keychain. Anthropic-backend projects fall back to their own{" "} claude login the next time their container starts. Existing running containers keep working until they are restarted.

)}
); }