From 9227ae30919ed194eea3124d3f5fadd375e77094 Mon Sep 17 00:00:00 2001 From: Paul Anduschus <157472224+DreamyTalesPAN@users.noreply.github.com> Date: Wed, 28 Oct 2026 17:24:18 +0100 Subject: [PATCH] Claude Auto: a session key in the environment reads only claude.ai (#273) VibeTV hands the sign-in of its own Claude window to the engine as CLAUDE_AI_SESSION_KEY. Auto answered from a signed-in Claude Code first, so a second account won on Windows. With the key set, Auto now reads claude.ai with it and returns that error, with no OAuth, CLI or browser fallback. Co-Authored-By: Claude Sonnet 5.5 --- rust/src/providers/claude/mod.rs | 8 +++++ rust/src/providers/claude/web_api.rs | 44 +++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/rust/src/providers/claude/mod.rs b/rust/src/providers/claude/mod.rs index a2857d55f28..0062c273d4c 100755 --- a/rust/src/providers/claude/mod.rs +++ b/rust/src/providers/claude/mod.rs @@ -968,6 +968,14 @@ impl ClaudeProvider { return Ok(result); } + // A session key in the environment is the sign-in of VibeTV's own + // Claude window (#273). It names the account the customer chose, so a + // signed-in Claude Code (another account) must not answer instead, and + // an expired window sign-in stays an error rather than falling back. + if ClaudeWebApiFetcher::resolve_session_key_from_env().is_some() { + return self.fetch_via_web(ctx).await; + } + // Upstream 0.50.1 #2516: track whether OAuth failed with a revocation. let oauth_result = self.fetch_via_oauth(ctx).await; let oauth_revoked = oauth_result diff --git a/rust/src/providers/claude/web_api.rs b/rust/src/providers/claude/web_api.rs index 444f3870ec3..b5108c80eba 100755 --- a/rust/src/providers/claude/web_api.rs +++ b/rust/src/providers/claude/web_api.rs @@ -473,7 +473,7 @@ impl ClaudeWebApiFetcher { headers } - fn resolve_session_key_from_env() -> Option { + pub(super) fn resolve_session_key_from_env() -> Option { for env_name in ["CLAUDE_AI_SESSION_KEY", "CLAUDE_WEB_SESSION_KEY"] { let Ok(value) = std::env::var(env_name) else { continue; @@ -960,6 +960,48 @@ mod tests { } } + // A session key in the environment is the sign-in of VibeTV's own Claude + // window (#273). Auto must read claude.ai with it and nothing else, so a + // signed-in Claude Code (a second account) cannot answer instead. + #[test] + fn auto_with_a_window_session_returns_only_the_web_error() { + use crate::core::{FetchContext, Provider}; + + let _guard = env_lock().lock().expect("env lock"); + // SAFETY: env_lock() held for this whole test; restored before release. + unsafe { + std::env::remove_var("CLAUDE_WEB_SESSION_KEY"); + std::env::set_var("CLAUDE_AI_SESSION_KEY", "sk-ant-window-session"); + } + + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("runtime"); + let result = runtime.block_on(crate::providers::ClaudeProvider::new().fetch_usage( + &FetchContext { + web_timeout: 5, + ..FetchContext::default() + }, + )); + + // SAFETY: same env_lock()-guarded mutation. + unsafe { + std::env::remove_var("CLAUDE_AI_SESSION_KEY"); + } + + // The fake key never works, so claude.ai (or the network) refuses it. + let message = result + .expect_err("a fake session key cannot read usage") + .to_string(); + assert!( + !message.contains("all configured sources") + && !message.contains("OAuth:") + && !message.contains("CLI:"), + "{message}" + ); + } + #[test] fn build_headers_include_required_browser_context() { let headers = ClaudeWebApiFetcher::build_headers("sessionKey=sk-ant-cookie-format");