diff --git a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs index 5e1b0f3020..2d1eae6c06 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/bridge.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/bridge.rs @@ -699,6 +699,7 @@ pub struct SettingsSnapshot { float_bar_show_cost: bool, promote_tray_icon: bool, claude_daily_routines_usage_visible: bool, + claude_allow_reading_claude_code_credentials: bool, alibaba_token_plan_region: String, weekly_progress_work_days: Option, cost_summary_display_style: &'static str, @@ -810,6 +811,8 @@ impl From for SettingsSnapshot { float_bar_show_cost: settings.float_bar_show_cost, promote_tray_icon: settings.promote_tray_icon, claude_daily_routines_usage_visible: settings.claude_daily_routines_usage_visible, + claude_allow_reading_claude_code_credentials: settings + .claude_allow_reading_claude_code_credentials, alibaba_token_plan_region: settings.alibaba_token_plan_region, weekly_progress_work_days: settings.weekly_progress_work_days, cost_summary_display_style: cost_summary_display_style_label( diff --git a/apps/desktop-tauri/src-tauri/src/commands/settings.rs b/apps/desktop-tauri/src-tauri/src/commands/settings.rs index df9ebb6a01..dfd92edce9 100644 --- a/apps/desktop-tauri/src-tauri/src/commands/settings.rs +++ b/apps/desktop-tauri/src-tauri/src/commands/settings.rs @@ -53,6 +53,7 @@ pub struct SettingsUpdate { pub tray_scale_percent: Option, pub powertoys_status_pipe_enabled: Option, pub claude_avoid_keychain_prompts: Option, + pub claude_allow_reading_claude_code_credentials: Option, pub codex_spark_usage_visible: Option, pub disable_keychain_access: Option, /// Map of provider CLI name → metric preference label. @@ -80,6 +81,7 @@ impl SettingsUpdate { fn refreshes_provider_data(&self) -> bool { self.enabled_providers.is_some() || self.claude_daily_routines_usage_visible.is_some() + || self.claude_allow_reading_claude_code_credentials.is_some() || self.alibaba_token_plan_region.is_some() || self.weekly_progress_work_days.is_some() } @@ -324,6 +326,9 @@ impl SettingsUpdate { if let Some(v) = self.claude_avoid_keychain_prompts { settings.set_claude_avoid_keychain_prompts(v); } + if let Some(v) = self.claude_allow_reading_claude_code_credentials { + settings.claude_allow_reading_claude_code_credentials = v; + } if let Some(v) = self.codex_spark_usage_visible { settings.set_codex_spark_usage_visible(v); } @@ -516,6 +521,13 @@ mod tests { } .refreshes_provider_data() ); + assert!( + SettingsUpdate { + claude_allow_reading_claude_code_credentials: Some(true), + ..Default::default() + } + .refreshes_provider_data() + ); assert!( !SettingsUpdate { provider_metrics: Some(Default::default()), @@ -526,6 +538,26 @@ mod tests { ); } + #[test] + fn apply_advanced_settings_sets_claude_code_credentials_consent() { + let mut settings = Settings::default(); + assert!(!settings.claude_allow_reading_claude_code_credentials); + + SettingsUpdate { + claude_allow_reading_claude_code_credentials: Some(true), + ..Default::default() + } + .apply_advanced_settings(&mut settings); + assert!(settings.claude_allow_reading_claude_code_credentials); + + SettingsUpdate { + claude_allow_reading_claude_code_credentials: Some(false), + ..Default::default() + } + .apply_advanced_settings(&mut settings); + assert!(!settings.claude_allow_reading_claude_code_credentials); + } + #[test] fn display_settings_that_affect_tray_trigger_presentation_refresh() { assert!( diff --git a/apps/desktop-tauri/src/App.test.tsx b/apps/desktop-tauri/src/App.test.tsx index 6320ae1d6e..c179a799c0 100644 --- a/apps/desktop-tauri/src/App.test.tsx +++ b/apps/desktop-tauri/src/App.test.tsx @@ -122,6 +122,7 @@ function settings(overrides: Partial = {}): SettingsSnapshot { floatBarShowResetInline: false, floatBarShowCost: false, claudeDailyRoutinesUsageVisible: true, + claudeAllowReadingClaudeCodeCredentials: false, alibabaTokenPlanRegion: "cn", weeklyProgressWorkDays: null, costSummaryDisplayStyle: "compact", diff --git a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx index a7235838ae..567ea0bff7 100644 --- a/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx +++ b/apps/desktop-tauri/src/floatbar/FloatBar.test.tsx @@ -183,6 +183,7 @@ function settings(overrides: Partial = {}): SettingsSnapshot { floatBarShowResetInline: false, floatBarShowCost: false, claudeDailyRoutinesUsageVisible: true, + claudeAllowReadingClaudeCodeCredentials: false, alibabaTokenPlanRegion: "cn", weeklyProgressWorkDays: null, costSummaryDisplayStyle: "compact", diff --git a/apps/desktop-tauri/src/floatbar/SettingsSection.test.tsx b/apps/desktop-tauri/src/floatbar/SettingsSection.test.tsx index b481e12f8b..ba16810e5d 100644 --- a/apps/desktop-tauri/src/floatbar/SettingsSection.test.tsx +++ b/apps/desktop-tauri/src/floatbar/SettingsSection.test.tsx @@ -15,6 +15,7 @@ const settings = { floatBarStyle: "floating", floatBarShowCost: false, claudeDailyRoutinesUsageVisible: true, + claudeAllowReadingClaudeCodeCredentials: false, alibabaTokenPlanRegion: "cn", weeklyProgressWorkDays: null, floatBarShowResetInline: false, diff --git a/apps/desktop-tauri/src/i18n/keys.ts b/apps/desktop-tauri/src/i18n/keys.ts index 98b3b42e41..9cf5dca6a8 100644 --- a/apps/desktop-tauri/src/i18n/keys.ts +++ b/apps/desktop-tauri/src/i18n/keys.ts @@ -299,6 +299,8 @@ export const ALL_LOCALE_KEYS = [ "ProviderClaudeAvoidKeychainPromptsHelp", "ProviderClaudeDailyRoutinesUsage", "ProviderClaudeDailyRoutinesUsageHelp", + "ProviderClaudeAllowReadingClaudeCodeCredentials", + "ProviderClaudeAllowReadingClaudeCodeCredentialsHelp", "ProviderCodexSparkUsage", "ProviderCodexSparkUsageHelp", "CodexAccountsTitle", diff --git a/apps/desktop-tauri/src/surfaces/PopOutPanel.test.tsx b/apps/desktop-tauri/src/surfaces/PopOutPanel.test.tsx index 5c1f8ac476..816bfae1a1 100644 --- a/apps/desktop-tauri/src/surfaces/PopOutPanel.test.tsx +++ b/apps/desktop-tauri/src/surfaces/PopOutPanel.test.tsx @@ -176,6 +176,7 @@ function settings(): SettingsSnapshot { floatBarShowResetInline: false, floatBarShowCost: false, claudeDailyRoutinesUsageVisible: true, + claudeAllowReadingClaudeCodeCredentials: false, alibabaTokenPlanRegion: "cn", weeklyProgressWorkDays: null, costSummaryDisplayStyle: "compact", diff --git a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx index 0c0c8f9b4d..678e621888 100644 --- a/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx +++ b/apps/desktop-tauri/src/surfaces/TrayPanel.test.tsx @@ -163,6 +163,7 @@ function settings(overrides: Partial = {}): SettingsSnapshot { floatBarShowResetInline: false, floatBarShowCost: false, claudeDailyRoutinesUsageVisible: true, + claudeAllowReadingClaudeCodeCredentials: false, alibabaTokenPlanRegion: "cn", weeklyProgressWorkDays: null, costSummaryDisplayStyle: "compact", diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/credentials/ClaudeCreds.test.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/credentials/ClaudeCreds.test.tsx new file mode 100644 index 0000000000..5658bef21c --- /dev/null +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/credentials/ClaudeCreds.test.tsx @@ -0,0 +1,74 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { SettingsSnapshot } from "../../../../../types/bridge"; +import { ClaudeCreds } from "./ClaudeCreds"; + +const tauriMocks = vi.hoisted(() => ({ + getSettingsSnapshot: vi.fn(), + updateSettings: vi.fn(), +})); + +vi.mock("../../../../../lib/tauri", async (importOriginal) => ({ + ...(await importOriginal()), + ...tauriMocks, +})); + +type ClaudeSettingsSnapshot = Pick< + SettingsSnapshot, + | "claudeAvoidKeychainPrompts" + | "claudeDailyRoutinesUsageVisible" + | "claudeAllowReadingClaudeCodeCredentials" +>; + +function snapshot( + overrides: Partial = {}, +): ClaudeSettingsSnapshot { + return { + claudeAvoidKeychainPrompts: false, + claudeDailyRoutinesUsageVisible: true, + claudeAllowReadingClaudeCodeCredentials: false, + ...overrides, + }; +} + +describe("ClaudeCreds", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("renders the Claude Code credentials consent toggle, off by default", async () => { + tauriMocks.getSettingsSnapshot.mockResolvedValue(snapshot()); + + render( key} />); + + const checkbox = await screen.findByRole("checkbox", { + name: "ProviderClaudeAllowReadingClaudeCodeCredentials ProviderClaudeAllowReadingClaudeCodeCredentialsHelp", + }); + expect(checkbox).not.toBeChecked(); + expect( + screen.getByText("ProviderClaudeAllowReadingClaudeCodeCredentialsHelp"), + ).toBeInTheDocument(); + }); + + it("toggling the consent checkbox calls updateSettings and reflects the response", async () => { + tauriMocks.getSettingsSnapshot.mockResolvedValue(snapshot()); + tauriMocks.updateSettings.mockResolvedValue( + snapshot({ claudeAllowReadingClaudeCodeCredentials: true }), + ); + + render( key} />); + + const checkbox = await screen.findByRole("checkbox", { + name: "ProviderClaudeAllowReadingClaudeCodeCredentials ProviderClaudeAllowReadingClaudeCodeCredentialsHelp", + }); + + fireEvent.click(checkbox); + + await waitFor(() => + expect(tauriMocks.updateSettings).toHaveBeenCalledWith({ + claudeAllowReadingClaudeCodeCredentials: true, + }), + ); + await waitFor(() => expect(checkbox).toBeChecked()); + }); +}); diff --git a/apps/desktop-tauri/src/surfaces/settings/providers/sections/credentials/ClaudeCreds.tsx b/apps/desktop-tauri/src/surfaces/settings/providers/sections/credentials/ClaudeCreds.tsx index 84d91d15fe..71ed639291 100644 --- a/apps/desktop-tauri/src/surfaces/settings/providers/sections/credentials/ClaudeCreds.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/providers/sections/credentials/ClaudeCreds.tsx @@ -11,7 +11,9 @@ interface Props { * * Port of the `ProviderId::Claude` branch of the "Options" block in * `rust/src/native_ui/preferences.rs::render_provider_detail_panel`. - * Exposes "Avoid keychain prompts" and "Show Daily Routines usage". + * Exposes "Avoid keychain prompts", "Show Daily Routines usage", and + * "Allow reading Claude Code's credentials" (OAuth consent gate added in + * 76d3f010 — this toggle is the UI surface for that setting). * The broader `disable_keychain_access` master switch lives in Advanced. */ export function ClaudeCreds({ t }: Props) { @@ -19,6 +21,8 @@ export function ClaudeCreds({ t }: Props) { const [showDailyRoutines, setShowDailyRoutines] = useState( null, ); + const [allowReadingClaudeCodeCredentials, setAllowReadingClaudeCodeCredentials] = + useState(null); const [error, setError] = useState(null); const [saving, setSaving] = useState(false); @@ -29,6 +33,9 @@ export function ClaudeCreds({ t }: Props) { if (cancelled) return; setAvoidKeychain(s.claudeAvoidKeychainPrompts); setShowDailyRoutines(s.claudeDailyRoutinesUsageVisible ?? true); + setAllowReadingClaudeCodeCredentials( + s.claudeAllowReadingClaudeCodeCredentials ?? false, + ); }) .catch((e) => !cancelled && setError(String(e))); return () => { @@ -64,7 +71,28 @@ export function ClaudeCreds({ t }: Props) { } }; - if (avoidKeychain === null || showDailyRoutines === null) return null; + const toggleAllowReadingClaudeCodeCredentials = async (next: boolean) => { + setSaving(true); + try { + const updated = await updateSettings({ + claudeAllowReadingClaudeCodeCredentials: next, + }); + setAllowReadingClaudeCodeCredentials( + updated.claudeAllowReadingClaudeCodeCredentials ?? next, + ); + } catch (e) { + setError(String(e)); + } finally { + setSaving(false); + } + }; + + if ( + avoidKeychain === null || + showDailyRoutines === null || + allowReadingClaudeCodeCredentials === null + ) + return null; return (
@@ -101,6 +129,24 @@ export function ClaudeCreds({ t }: Props) { + {error &&
{error}
}
); diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx index 21c8e5a138..9837634fde 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/AboutTab.test.tsx @@ -96,6 +96,7 @@ const settings: SettingsSnapshot = { floatBarShowResetInline: false, floatBarShowCost: false, claudeDailyRoutinesUsageVisible: true, + claudeAllowReadingClaudeCodeCredentials: false, alibabaTokenPlanRegion: "cn", weeklyProgressWorkDays: null, costSummaryDisplayStyle: "compact", diff --git a/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.test.tsx b/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.test.tsx index 86799583d4..b7592a6170 100644 --- a/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.test.tsx +++ b/apps/desktop-tauri/src/surfaces/settings/tabs/GeneralTab.test.tsx @@ -86,6 +86,7 @@ const settings: SettingsSnapshot = { floatBarShowResetInline: false, floatBarShowCost: false, claudeDailyRoutinesUsageVisible: true, + claudeAllowReadingClaudeCodeCredentials: false, alibabaTokenPlanRegion: "cn", weeklyProgressWorkDays: null, costSummaryDisplayStyle: "compact", diff --git a/apps/desktop-tauri/src/types/bridge.test.ts b/apps/desktop-tauri/src/types/bridge.test.ts index d2e56e8051..27b0d37e99 100644 --- a/apps/desktop-tauri/src/types/bridge.test.ts +++ b/apps/desktop-tauri/src/types/bridge.test.ts @@ -115,6 +115,7 @@ describe("Language type", () => { floatBarShowResetInline: false, floatBarShowCost: false, claudeDailyRoutinesUsageVisible: true, + claudeAllowReadingClaudeCodeCredentials: false, alibabaTokenPlanRegion: "cn", weeklyProgressWorkDays: null, costSummaryDisplayStyle: "compact", diff --git a/apps/desktop-tauri/src/types/bridge.ts b/apps/desktop-tauri/src/types/bridge.ts index 3d1fe339f4..147c13b151 100644 --- a/apps/desktop-tauri/src/types/bridge.ts +++ b/apps/desktop-tauri/src/types/bridge.ts @@ -245,6 +245,13 @@ export interface SettingsSnapshot { promoteTrayIcon?: boolean; /** When true, show Claude Daily Routines quota row (default true). */ claudeDailyRoutinesUsageVisible: boolean; + /** + * Explicit consent to read (and refresh) Claude Code's own OAuth + * credentials for the Claude provider. Default false — without consent + * OAuth stays closed and Auto falls back to labeled reduced-fidelity CLI + * usage (upstream #2634/#2745). + */ + claudeAllowReadingClaudeCodeCredentials: boolean; /** Alibaba Token Plan region: cn | intl | cn-personal | intl-personal. */ alibabaTokenPlanRegion: string; /** Optional work-week length [2,6] for session-equivalent weekly forecast. */ @@ -305,6 +312,7 @@ export interface SettingsUpdate { trayScalePercent?: number; powertoysStatusPipeEnabled?: boolean; claudeAvoidKeychainPrompts?: boolean; + claudeAllowReadingClaudeCodeCredentials?: boolean; codexSparkUsageVisible?: boolean; disableKeychainAccess?: boolean; /** Map of provider CLI name → metric preference label. */ diff --git a/rust/src/locale.rs b/rust/src/locale.rs index e141ccc82b..86cecb7286 100644 --- a/rust/src/locale.rs +++ b/rust/src/locale.rs @@ -517,6 +517,8 @@ locale_keys! { ProviderClaudeAvoidKeychainPromptsHelp, ProviderClaudeDailyRoutinesUsage, ProviderClaudeDailyRoutinesUsageHelp, + ProviderClaudeAllowReadingClaudeCodeCredentials, + ProviderClaudeAllowReadingClaudeCodeCredentialsHelp, ProviderCodexSparkUsage, ProviderCodexSparkUsageHelp, CodexAccountsTitle, diff --git a/rust/src/locale/en-US.ftl b/rust/src/locale/en-US.ftl index 118f479bed..38e788e4e5 100644 --- a/rust/src/locale/en-US.ftl +++ b/rust/src/locale/en-US.ftl @@ -286,6 +286,8 @@ ProviderClaudeAvoidKeychainPrompts = Avoid Keychain prompts ProviderClaudeAvoidKeychainPromptsHelp = Use /usr/bin/security to read Claude credentials and avoid CodexBar keychain prompts. ProviderClaudeDailyRoutinesUsage = Show Daily Routines usage ProviderClaudeDailyRoutinesUsageHelp = Show the Daily Routines quota row for Claude web and OAuth usage. +ProviderClaudeAllowReadingClaudeCodeCredentials = Allow reading Claude Code's credentials +ProviderClaudeAllowReadingClaudeCodeCredentialsHelp = Lets CodexBar read (and refresh) Claude Code's own OAuth credentials for higher-fidelity usage. Off by default; without it, Auto falls back to reduced-fidelity CLI usage. ProviderCodexSparkUsage = Show Codex Spark usage ProviderCodexSparkUsageHelp = Show Codex Spark quota rows without hiding credits or other extra usage. CodexAccountsTitle = Codex Accounts diff --git a/rust/src/locale/tr-TR.ftl b/rust/src/locale/tr-TR.ftl index 62d2fa6f62..3e8884c4c6 100644 --- a/rust/src/locale/tr-TR.ftl +++ b/rust/src/locale/tr-TR.ftl @@ -286,6 +286,8 @@ ProviderClaudeAvoidKeychainPrompts = Anahtar Zinciri istemlerinden kaçın ProviderClaudeAvoidKeychainPromptsHelp = Claude kimlik bilgilerini okumak ve CodexBar Anahtar Zinciri istemlerini önlemek için /usr/bin/security kullanın. ProviderClaudeDailyRoutinesUsage = Günlük Rutinler kullanımını göster ProviderClaudeDailyRoutinesUsageHelp = Claude web ve OAuth kullanımı için Günlük Rutinler kota satırını göster. +ProviderClaudeAllowReadingClaudeCodeCredentials = Claude Code kimlik bilgilerini okumaya izin ver +ProviderClaudeAllowReadingClaudeCodeCredentialsHelp = CodexBar'ın daha isabetli kullanım verisi için Claude Code'un kendi OAuth kimlik bilgilerini okumasına (ve yenilemesine) izin verir. Varsayılan olarak kapalıdır; kapalıyken Otomatik mod, daha düşük isabetli CLI kullanımına düşer. ProviderCodexSparkUsage = Codex Spark kullanımını göster ProviderCodexSparkUsageHelp = Kredileri veya diğer ek kullanımları gizlemeden Codex Spark kota satırlarını göster. CodexAccountsTitle = Codex Hesapları diff --git a/rust/src/settings/tests.rs b/rust/src/settings/tests.rs index 418ab53956..1a468a939f 100644 --- a/rust/src/settings/tests.rs +++ b/rust/src/settings/tests.rs @@ -22,6 +22,7 @@ fn test_settings_default() { assert!(!settings.float_bar_show_cost); assert!(settings.promote_tray_icon); assert!(settings.claude_daily_routines_usage_visible); + assert!(!settings.claude_allow_reading_claude_code_credentials); assert_eq!( settings.low_power_mode_preference, LowPowerModePreference::Off