---
title: Add a Claude account
---

# Add a Claude account

## What it is

An **account** in Omniscio is a credential Omniscio uses to talk to Anthropic's API — either a Claude.ai subscription (login via OAuth) or a raw `sk-ant-...` API key. Sessions need an account to spawn the Claude CLI; AI features (titles, suggestions, spam filtering, automations) also need one. Omniscio supports holding more than one account at the same time and silently picks the freshest one per spawn (see [account-pool.md](account-pool.md)). The first account is added during onboarding; subsequent accounts are added from **Settings → Accounts**, which is the first item in the Settings sidebar.

There are two kinds of accounts. **Login accounts** sign in through the same PKCE OAuth flow as the Claude Code CLI — your Claude.ai subscription powers sessions, and Omniscio tracks 5-hour and 7-day usage caps per account. **API key accounts** use a raw key from the Anthropic Console; they pay per-token instead of consuming a subscription quota, and by default they are restricted to AI features only (the toggle that lets them spawn sessions is off out of the box).

## Where to find it

### How to use it

1. **Open Settings → Accounts.** Click the gear icon in the toolbar (or open the **Settings** virtual project from the Omniscio sidebar group). The first sidebar item is **Accounts** (User icon). The page lists every account you've already added under **Login Accounts** and **API Keys**, plus an **Add Account** card at the bottom.

2. **Sign in with Claude (OAuth — primary path).** In the **Add Account** card, click the prominent **Log In with Anthropic** button (LogIn icon, accent color). Omniscio opens your default browser to the Anthropic authorization page, you sign into Claude.ai, and you click **Authorize** on the consent screen. Omniscio's local callback server captures the authorization code, exchanges it for a refresh token, fetches your profile (email, display name, plan tier), and saves the account row. Total time is usually 10–20 seconds; the **Add Account** card shows a "Logging in..." state and a "try again" link below it that reopens the browser if the page didn't load. The login times out after 15 minutes.
   - On success, a toast confirms `Logged in as <email>` (or `Logged in as <email> — now active` if Omniscio switched the active account to the new one). The account lands in the **Login Accounts** list with a green dot if it's now the active account.
   - **Changed your mind? Cancel it.** Next to "try again" is a **cancel sign-in** link (and a **cancel** action in the toolbar account popover's footer). Closing the browser tab does not tell Omniscio anything on its own — so without cancelling, Omniscio keeps believing a sign-in is in progress for the full 15 minutes, and during that time the **Log In with Anthropic** button stays disabled on "Logging in..." and a fresh attempt is refused with "A login is already in progress". Cancelling releases it immediately so you can start again or sign in as a different account. Cancelling saves nothing — an account row is only ever written after a successful sign-in.
   - **A sign-in that doesn't finish leaves no account behind.** If a sign-in got far enough to receive tokens but Omniscio couldn't confirm *which* Claude account they belong to (a rate limit or a network blip at the wrong moment), the row is added unnamed and Omniscio keeps trying to identify it in the background. While that is happening the row reads **"Finishing sign-in…"**; if it can never be identified it reads **"Sign-in didn't finish"** with a plain-language note, offers no Switch (there is no account to switch to), and is cleaned up automatically. Historically these rows showed as `unknown@claude.ai` complete with a plan badge, looked like a second real account, and stayed forever — see [add-a-claude-account §Related](#related) and the [auth-login-contract](../../.claude/memory/contracts/auth-login-contract.md) for the invariant.
   - The OAuth scopes requested are `org:create_api_key`, `user:profile`, `user:inference`, `user:sessions:claude_code`, `user:mcp_servers`, and `user:file_upload`. The scopes match what the official Claude Code CLI requests so your Claude.ai subscription works the same way it does in `claude` on the terminal.
   - **Re-authorizing an existing account** targets the right identity automatically. When you click "Log In" on a specific account row, Omniscio passes that account's email as `login_hint` in the OAuth URL so Anthropic's consent screen pre-selects the correct account — you don't need to manually switch accounts in your browser first. The existing account row's tokens are updated in place instead of creating a duplicate. If you sign in to a different Claude.ai account in the browser (overriding the hint), Omniscio creates a new row and (if it was your only account before) flips it to active.

3. **Or add an API key.** Below the Log In button, click the small **Or add an API key manually** link (Key icon). A password-style input appears — paste your `sk-ant-...` key from [https://console.anthropic.com](https://console.anthropic.com) and click **Add API Key**. Omniscio saves the encrypted key to disk — it does **not** pre-validate the key, so use **Check Limits** on the row afterwards to confirm it works. A toast confirms `API key added`. If the key is invalid you'll see a `Could not add account: <reason>` error and the input stays open.
   - The first API key shows a separate prominent card at the top of the **API Keys** section labeled **Anthropic API Key** with a three-state badge: green **Configured** (a saved key Omniscio can actually read), amber **Can't be read — re-enter** (the key record exists but its encrypted-at-rest copy no longer decrypts — typically after a hardware/Windows change resets DPAPI; the key itself is still fine), or grey **Not configured** (no key at all). The card's inline paste form appears whenever the badge is **not** green — so an unreadable key can no longer hide the only way to re-enter (that was the bug). Re-pasting the same key while it's amber **repairs it in place** (no duplicate row) and the badge returns to green. Use either entry point; both call the same handler. You're also notified proactively: if a saved key can't be read, Omniscio raises a persistent **Inbox** alert at startup — independent of other features, so it shows even with the waiting double-check turned off — pointing you to re-enter the key; re-entering clears it. Full semantics: [apikey-badge-decrypt-health-contract.md](../../.claude/memory/contracts/apikey-badge-decrypt-health-contract.md).
   - **API key accounts cannot spawn sessions out of the box.** The setting **Allow API Keys to Run Sessions** lives just below the API key list and is **off by default**. While off, API keys only power AI features — session spawn falls through to a login account or surfaces the all-exhausted banner if no login account has capacity. Flip the toggle on if you want sessions to run on the API key (you'll be billed per token by Anthropic). See [account-pool.md](account-pool.md) for how the pool tier-list interacts with this setting.
   - **Replace a key in place.** Each row in the **API Keys** list has a **Replace** link (alongside Activate and the trash icon). Click it to open a paste box right on that row, paste a new `sk-ant-...` key, and click **Replace key** — the key is swapped on the **same** account in place (its name, cost history, and active status are kept), so you never have to add a second key and delete the old one, and the _last-account_ removal guard never gets in the way. The swap is **immediate and not pre-validated**, so use **Check Limits** afterwards to confirm the new key works; a _"Replaces your current key immediately"_ note sits under the box, and if the save fails the box stays open showing the error (it never falsely reports success). Replace shows only on a healthy key — an unreadable (amber) key keeps the _re-enter it above_ flow on the prominent card instead. Mechanics: [apikey-badge-decrypt-health-contract.md](../../.claude/memory/contracts/apikey-badge-decrypt-health-contract.md) (B9).

4. **(Optional) Pick which account is active.** After adding an account, the row in the **Login Accounts** or **API Keys** list shows an **Activate** link if it isn't already the active one. Clicking **Activate** flips the active-account flag, which the account pool uses as the preferred account for the next spawn. There's no separate "Pin" affordance — Activate is the pin. Nothing about your existing sessions or projects is automatically reassigned to the new account; pinning is purely about who powers the next spawn.

5. **Repeat for as many _login_ accounts as you want.** Omniscio has no cap on login accounts — the pool silently rotates through them based on usage headroom (see [account-pool.md](account-pool.md)). Two common patterns: keep two Claude.ai subscriptions to double your daily session capacity, or keep a Claude.ai login as primary and a single API key as a metered fallback for AI features. **API keys are the exception: Omniscio keeps exactly one.** Adding an API key when one already exists replaces it in place instead of stacking a second, and a pile left over from before is collapsed to the one already in use the next time Omniscio starts — multiple keys serve no purpose (only one is ever used) and would otherwise let different AI features bill to different keys. Full rules: [single-anthropic-apikey-contract.md](../../.claude/memory/contracts/single-anthropic-apikey-contract.md).

## How it behaves

### How it works

The page is [AccountSettings.tsx](../../src/renderer/src/features/settings/sections/accounts/AccountSettings.tsx), which reads from the [account-store.ts](../../src/renderer/src/stores/account-store.ts) Zustand slice (the same store that powers the toolbar's `AccountIndicator`). The OAuth flow is implemented entirely in [auth-service.ts](../../src/main/services/auth/auth-service.ts) — `authService.login()` is the single public entry point. It generates a PKCE code verifier + challenge, starts a local callback server on `localhost:<random-port>`, opens the Anthropic authorize URL via `shell.openExternal()`, waits for the redirect to `/callback`, and POSTs the authorization code to `https://platform.claude.com/v1/oauth/token` with `Content-Type: application/json` and a `JSON.stringify`'d body. The bind host is deliberately `localhost` and **not** `127.0.0.1` — Windows 11 resolves `localhost` to `::1`, so binding the literal IPv4 address can miss the browser's callback ([oauth-callback-server-core.ts](../../src/main/services/oauth-callback-server-core.ts)). The token endpoint format is non-negotiable — see CLAUDE.md "Token endpoint format" rule. The exchange does **not** retry: it runs with `maxRetries: 0`, because retrying the token endpoint is forbidden by the [auth-login-contract](../../.claude/memory/contracts/auth-login-contract.md) (`exchange-must-succeed-on-first-try`). The whole sign-in holds that endpoint, not just its HTTP round-trip: [auth-login-flow.ts](../../src/main/services/auth/auth-login-flow.ts) claims it on the gateway when the flow starts and releases it in its `finally`, and while it is held a background refresh is refused **before** it reaches the network — so it can neither spend the exchange's rate-limit budget nor arm the 429 backoff the exchange would then have to read. A background caller's 429 observed during that window does not arm that backoff either ([auth-login-contract](../../.claude/memory/contracts/auth-login-contract.md), `login-exchange-gets-absolute-priority`). Instead, when the exchange is rate-limited, a first-time login (no existing accounts) falls back to importing the Claude Code CLI's stored credentials. After the token exchange, `fetchProfile()` calls `/api/oauth/profile` to get the email, display name, and `rate_limit_tier`, and `saveAccount()` writes the account row into `config.json`. Renderer-side, the **Log In with Anthropic** button calls `addLoginAccount()` on the account store, which invokes `IPC.ACCOUNT_ADD_LOGIN`; the handler in [claude-provider.ts](../../src/main/ipc/account/claude-provider.ts) parses an optional `{ loginHint }` payload (validated by `addLoginAccountSchema`) and calls `authService.login(loginHint)`. When re-authenticating an existing account row, the renderer passes `account.email` as the hint; when adding a brand new account (the "+ Log In" button), no hint is sent. `authService.login()` appends `login_hint=<email>` to the OAuth authorization URL when a hint is provided, so Anthropic's consent screen pre-selects the correct account. The handler returns `{ account, accountSwitched }` so the toast can say "now active" when appropriate.

Identity resolution is the fragile step, and it is now bounded-retried rather than one-shot: `resolveProfileWithRetry` ([profile-resolve-policy.ts](../../src/main/services/auth/profile-resolve-policy.ts)) makes up to 3 profile attempts (~1.6s worst-case extra wait) before falling back to the `unknown@claude.ai` sentinel. That retry is safe because the profile API lives on `api.anthropic.com`, a different domain with independent rate limits from the token endpoint — it is deliberately **not** a retry of the token exchange, which the auth contract forbids. Any row that still lands on the sentinel is healed in the background by `reresolveStaleAccountProfiles()`; a row that can never be healed (its refresh token was quarantined, so the heal's `ensureTokenValid` gate can never pass) is removed by `reapLoginAccountResidue` ([auth-account-residue-reaper.ts](../../src/main/services/auth/auth-account-residue-reaper.ts)) at the end of that same pass. The reaper is deliberately conservative — a row must have an unresolved identity **and** provably dead credentials **and** be past a 10-minute grace window — and both it and the row's UI label read the same shared classifier ([login-account-residue.ts](../../src/shared/login-account-residue.ts)), so what the list says can never disagree with what gets swept. Cancelling an in-flight login goes through `IPC.ACCOUNT_CANCEL_LOGIN` → `authService.cancelLogin()`, which rejects the pending `login()` so its existing `finally` releases the in-progress lock and closes the loopback callback server.

API key adds go through `IPC.ACCOUNT_ADD_APIKEY` validated against `addApiKeyAccountSchema` (only `apiKey` is required; `name` and `email` default to `'API Key'` and `''`). `authService.addApiKey()` enforces the **single-key** model: with no api-key account it creates one (new UUID) and auto-activates if there's no active account yet; with one already present it writes the key into the canonical slot (the active apikey, else the oldest) **in place** via `replaceApiKey` and removes any other api-key rows, so a second never accumulates. A one-time `collapseApiKeyAccounts` pass inside `migrateAccountSystem` (at config load, after decryption) likewise collapses any pre-existing pile to that single usable/in-use key — removing empty-junk rows and extras, but never the last re-enterable key (so a key that's only temporarily unreadable after a DPAPI reset still has a slot to repair). Full rules + tests: [single-anthropic-apikey-contract.md](../../.claude/memory/contracts/single-anthropic-apikey-contract.md). When a saved key can no longer be decrypted (F065, see the badge note above), the schema also accepts an optional `replaceAccountId` so re-entry routes to `authService.replaceApiKey()` — an **in-place** `saveAccount` on the existing id that repairs the resolved (active-apikey-else-first) account instead of minting a duplicate, and clears the F065 flag. Targeting the resolved account matters: the AI features read the active apikey first (`resolveApiKeyCredential`), so adding a new inactive key would leave the badge green while the double-check still reads the dead one — see [apikey-badge-decrypt-health-contract.md](../../.claude/memory/contracts/apikey-badge-decrypt-health-contract.md) (B7). Separately, each api-key **row** exposes a **Replace** action (`handleReplaceApiKey`) that sends `ACCOUNT_ADD_APIKEY` with the **explicit clicked-row id** as `replaceAccountId` — the same in-place `replaceApiKey` swap, but it never runs the F065 `resolveApiKeyReplaceTarget` fallback, so it carries none of that insertion-order coupling (B9). Both account types are stored in the same `accounts[]` array in `config.json` (a discriminated union on `type: 'login' | 'apikey'`), with credentials encrypted via Electron's `safeStorage.encryptString()` and tagged with the `enc:` prefix per the security rule "Credential encryption" in CLAUDE.md. Tokens never reach the renderer — the renderer only sees `AccountSummary` objects with credential fields stripped. At session spawn time, the process manager reads `authService.getCredentials(accountId)` and injects either `CLAUDE_CODE_OAUTH_TOKEN` (login) or `ANTHROPIC_API_KEY` (api key) into the spawned CLI's environment. The `allowApiKeySessionSpawn` toggle is enforced in three layers: `filterAccountsForSpawn()` in `pickActiveAccount()`, `shouldBlockApiKeySpawn()` in `spawnLocalStreamJson()`, and `filterSpawnEligibleAccounts()` in `tryAutoSwitch()` (see [account-management.md](../../.claude/memory/account-management.md) "API Key Session Guard").

## Related

- [account-pool.md](account-pool.md) — once you have multiple accounts, the pool tier-list decides which one each session uses
- [switch-active-account.md](switch-active-account.md) — there's no "switch active account" button per se; this page explains why and how Activate interacts with the pool
- [remove-a-claude-account.md](remove-a-claude-account.md) — taking an account back out, including the only-account case
- [usage-forecast.md](usage-forecast.md) — the inline forecast row each login account row shows, predicting when its 5-hour bucket exhausts
