Skip to content

feat: link Claude Code OAuth in onboarding#1727

Closed
Michaelyklam wants to merge 2 commits intonesquena:masterfrom
Michaelyklam:feat/issue-1362-claude-oauth
Closed

feat: link Claude Code OAuth in onboarding#1727
Michaelyklam wants to merge 2 commits intonesquena:masterfrom
Michaelyklam:feat/issue-1362-claude-oauth

Conversation

@Michaelyklam
Copy link
Copy Markdown
Contributor

Thinking Path

  • Issue feat(onboarding): in-app OAuth login for Codex and Claude (currently terminal-only) #1362 already shipped the Codex half; the remaining open tracker is Claude/Anthropic onboarding.
  • Claude Code credentials are host-owned, not browser-owned, so the safe WebUI shape is credential linking/detection rather than a browser token flow.
  • The existing onboarding OAuth endpoints already provide a server-owned start/poll/cancel state machine, so this PR extends that scaffold instead of adding a parallel flow.
  • Cancel/expiry/error paths need to remain secret-free and race-safe because OAuth onboarding is security-sensitive.
  • The result is an in-app Claude Code onboarding path that guides users to run host-side Claude Code auth when needed, then links Hermes without exposing tokens to the browser.

What Changed

  • Added Anthropic/Claude Code support to /api/onboarding/oauth/start, /poll, and /cancel, normalizing anthropic, claude, and claude-code to canonical anthropic.
  • Added server-side Claude Code credential detection/linking using existing Agent-compatible helpers, with secret-free credential_pool.anthropic marker storage.
  • Clears active-profile ANTHROPIC_TOKEN / ANTHROPIC_API_KEY values when linking Claude Code OAuth so env API keys do not override linked OAuth credentials.
  • Updated onboarding provider metadata/readiness/setup logic so Anthropic can be configured without an API key once linked OAuth exists.
  • Added the frontend “Use Claude Code OAuth instead” onboarding card, action-required polling state, cancel handling, and success/error rendering without window.open() or browser-visible tokens.
  • Added regression coverage for Anthropic alias normalization, secret-free payloads, credential linking, readiness, setup-without-key, cancel/expiry/error worker paths, cancel-during-link races, and frontend endpoint/no-token strings.
  • Added UI evidence for the new Claude Code onboarding state.

Why It Matters

Claude Code / Claude Pro users no longer have to infer that they must leave the WebUI, authenticate on the host, then manually refresh. The WebUI now offers a guided, server-owned flow that can detect/link host Claude Code credentials while preserving the important safety boundary: tokens and credential paths stay on the server.

Closes #1362.

Verification

env -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_issue1362_codex_oauth_onboarding.py -q
node --check static/onboarding.js
env -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_issue1362_codex_oauth_onboarding.py tests/test_onboarding_mvp.py tests/test_sprint40.py tests/test_issue1202_oauth_provider_status.py -q
env -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/ -q
git diff --cached --check
curl -L -s -o /dev/null -w '%{http_code} %{size_download}\n' https://raw.githubusercontent.com/Michaelyklam/hermes-webui/feat/issue-1362-claude-oauth/docs/pr-media/1362/claude-code-onboarding.png

Result:

22 passed in 3.78s
node --check static/onboarding.js passed
61 passed in 84.18s (0:01:24)
4540 passed, 2 skipped, 3 xpassed, 1 warning, 8 subtests passed in 437.09s (0:07:17)
git diff --cached --check passed
raw media: 200 161942

Manual verification:

  • Started an isolated local WebUI server with a temporary Hermes home/profile and verified the Anthropic onboarding card can enter the Claude Code action-required polling state.
  • Confirmed the visible WebUI state shows only setup instructions/status, not OAuth tokens, refresh tokens, API keys, credential paths, or env values.
  • Ran a focused Claude Code pre-PR review of the staged diff after the cancel-race hardening; result: NO BLOCKERS.

UI media:

Claude Code onboarding polling state

Risks / Follow-ups

  • This deliberately does not implement a browser-exposed Anthropic token/login flow. It links server-side Claude Code credentials only, so users without host credentials still need to run claude login or claude setup-token on the WebUI host.
  • A cancel that races after linking begins rolls back the secret-free marker and preserves cancelled flow status; active-profile Anthropic env values may already have been cleared as part of the linking attempt, which is still safer than leaving API-key env values overriding OAuth.
  • Other OAuth providers mentioned in the original issue body (Nous, Qwen, Gemini, MiniMax, Copilot) remain terminal-first/out of scope.

Model Used

AI assisted.

  • Provider: OpenAI Codex
  • Model: gpt-5.5
  • Notable tool use: Hermes file/terminal/browser tools, pytest, Node syntax check, git/gh, and Claude Code CLI for focused diff review.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Summary

Pulled the branch and walked the diff (api/oauth.py +315, api/onboarding.py +46, static/onboarding.js +138, regression tests +316). The shape is right: this is a credential-linking flow rather than a browser-exposed Anthropic token flow, and the public payloads stay token-free. The cross-repo agent contract checks out — agent/anthropic_adapter.py has the host-side credential reading already; this PR just wires WebUI to detect and mark it.

Cross-repo agent contract

Reading ~/.hermes/hermes-agent/agent/anthropic_adapter.py:706 (read_claude_code_credentials), :760 (is_claude_code_token_valid), and :942 (resolve_anthropic_token) — the agent already prefers Claude Code credentials when ANTHROPIC_TOKEN / ANTHROPIC_API_KEY are both unset:

def resolve_anthropic_token() -> Optional[str]:
    # Priority:
    #   1. ANTHROPIC_TOKEN env var (OAuth/setup token saved by Hermes)
    #   2. CLAUDE_CODE_OAUTH_TOKEN env var
    #   3. Claude Code credentials (~/.claude.json or ~/.claude/.credentials.json)
    #   4. ANTHROPIC_API_KEY env var

So the WebUI side's _clear_anthropic_env_values() at api/oauth.py:241-252 (clearing ANTHROPIC_TOKEN and ANTHROPIC_API_KEY from the active profile's .env and the live os.environ) is exactly what makes the agent fall through to step 3. That is the right contract layer — no new agent-side code needed.

Code reference

The credential-pool marker shape on api/oauth.py:262-296:

entries = pool.setdefault("anthropic", [])
# ...
entry = {
    "id": "anthropic-claude-code-" + uuid.uuid4().hex[:12],
    "label": "Claude Code (linked)",
    "auth_type": "oauth",
    "priority": 0,
    "source": "claude_code_linked",
    "created_at": now,
}
entries.insert(0, entry)

And the matching readiness check in api/onboarding.py:613-622:

for entry in entries:
    if _oauth_payload_has_token(entry):
        return True
    if (
        provider == "anthropic"
        and isinstance(entry, dict)
        and entry.get("auth_type") == "oauth"
        and entry.get("source") == "claude_code_linked"
    ):
        return True

Two things I like here: the marker entry has no token field at all (so a serialized auth.json dump is genuinely secret-free even for the linked case), and _oauth_payload_has_token() already covers the standard OAuth-token-in-pool path so existing providers keep working unchanged.

Diagnosis

A few observations from the diff:

  1. Alias normalization is centralized. _normalize_onboarding_oauth_provider() at api/oauth.py:60-65 collapses claude and claude-code into canonical anthropic. The _ANTHROPIC_PROVIDER_ALIASES set is reused at the rejection check on api/oauth.py:664 so the public API accepts all three aliases consistently. Good.

  2. Cancel-during-link races. The body mentions hardening for cancels racing the credential-link path. Reading the cancel worker, the marker write to auth.json and the env-clear are both synchronous inside _link_anthropic_credentials(), so a cancel arriving mid-link can't tear the marker write — but the env values may already have been cleared. The PR body acknowledges this tradeoff; in practice, clearing env values that would override OAuth is still the safer state, so I think this is fine as documented.

  3. Frontend secret discipline. Reading static/onboarding.js:760-793, the polling/start UI never displays a token and only shows status strings + a claude setup-token instruction. The action-required state explicitly says "this page will detect the credentials automatically" rather than asking the user to paste anything. That matches the PR body's claim of no browser-visible tokens.

One small concern

The marker dedup loop scans entries for source == "claude_code_linked" and reuses an existing entry, but always re-prepends with entries.insert(0, entry) if it didn't find one. If entries already has another non-linked entry at position 0 (say, an existing Anthropic OAuth token from a different flow), the linked marker will jump in front and inherit priority: 0. That is probably fine because the readiness check returns True if either entry has a token or is the linked marker, but the runtime credential resolution order may now prefer the marker over a real token if anything else later sorts by priority. Worth a quick check that no consumer outside this PR sorts the pool by priority and treats claude_code_linked as a token source.

Verification

tests/test_issue1362_codex_oauth_onboarding.py adds 22 new test cases covering alias normalization, secret-free payloads, credential linking, readiness, setup-without-key, cancel/expiry/error worker paths, cancel-during-link races, and frontend strings. CI is green on all three Python versions. Closes #1362 cleanly. Looks good to merge.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Thanks @Michaelyklam — this shipped in v0.51.8 (commit 85d0279) as part of a 7-PR full-sweep batch release. Stage rebased your branch onto current master, ran the full pre-release gate (4584 pytest, browser tests, Opus advisor verdict SHIP), and merged via release PR #1737.

GitHub didn't auto-close because the merge commit only references the squash-merged stage branch, not your fork's commit directly — closing manually for hygiene.

Live now on https://get-hermes.ai/ and on existing installs after git pull + restart.

Release notes: https://github.com/nesquena/hermes-webui/releases/tag/v0.51.8

pull Bot pushed a commit to soitun/hermes-webui that referenced this pull request May 6, 2026
pull Bot pushed a commit to soitun/hermes-webui that referenced this pull request May 6, 2026
…p + test-isolation fix

Constituent PRs:
- nesquena#1725 (@Michaelyklam) — simplify compact Activity row summary
- nesquena#1726 (@Michaelyklam) — delegate generic provider catalogs to Hermes CLI (slice of nesquena#1240)
- nesquena#1727 (@Michaelyklam) — link Claude Code OAuth in onboarding (closes nesquena#1362)
- nesquena#1728 (@starship-s) — preserve profile context when starting chats
- nesquena#1729 (@Michaelyklam) — persist compact Activity disclosure state
- nesquena#1730 (@Michaelyklam) — prevent sticky sidebar hover drag state
- nesquena#1732 (@Sanjays2402) — unpin scroll on small upward motion during streaming (closes nesquena#1731)

Plus 2 in-stage absorbed fixes:
- test-isolation fix: monkeypatch.setattr(config, 'cfg', X) survives PR nesquena#1728's
  path/mtime-aware get_config() reload. Mandatory before tag (Opus stage-302).
- Opus SHOULD-FIX #1: _lastScrollTop reset on session switch (nesquena#1732 follow-up).

Tests: 4537 → 4584 passing (+47). 0 regressions. Full suite ~128s. Stably green.

Pre-release verification:
- All 7 PRs CI-green individually + rebased onto master
- pytest 4584 passed, 0 failed (multiple runs)
- node -c clean on all 4 modified .js files
- 11/11 browser API endpoints PASS on isolated port 8789
- 20 QA tests via webui_qa_agent.sh PASS
- Opus advisor: SHIP, 5/5 verification clean, 0 MUST-FIX, 1 SHOULD-FIX absorbed
  (_lastScrollTop reset), 1 SHOULD-FIX deferred (nesquena#1736 — _clear_anthropic_env_values
  race, onboarding-time-only)

Closes nesquena#1362, nesquena#1731.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(onboarding): in-app OAuth login for Codex and Claude (currently terminal-only)

2 participants