1use crate::constants::env::{ai, system};
6use std::collections::HashMap;
7use std::fs;
8use std::path::PathBuf;
9use std::sync::Mutex;
10
11use once_cell::sync::Lazy;
12
13static INSIDE_GET_CONFIG: Lazy<Mutex<bool>> = Lazy::new(|| Mutex::new(false));
16
17static GLOBAL_CONFIG_CACHE: Lazy<Mutex<GlobalConfigCache>> =
19 Lazy::new(|| Mutex::new(GlobalConfigCache::default()));
20
21#[derive(Default)]
22struct GlobalConfigCache {
23 config: Option<GlobalConfig>,
24 mtime: u64,
25}
26
27#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum InstallMethod {
31 #[default]
32 Unknown,
33 Local,
34 Native,
35 Global,
36}
37
38#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
40#[serde(rename_all = "lowercase")]
41pub enum ReleaseChannel {
42 #[default]
43 Stable,
44 Latest,
45}
46
47#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
49#[serde(rename_all = "lowercase")]
50pub enum DiffTool {
51 #[default]
52 Auto,
53 Terminal,
54}
55
56#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
58#[serde(rename_all = "lowercase")]
59pub enum EditorMode {
60 #[default]
61 Normal,
62 Emacs,
63 Vim,
64}
65
66#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
68#[serde(rename_all = "kebab-case")]
69pub enum NotificationChannel {
70 #[default]
71 Auto,
72 Terminal,
73 Native,
74}
75
76#[derive(Debug, Clone, PartialEq, Default, serde::Serialize, serde::Deserialize)]
78#[serde(rename_all = "lowercase")]
79pub enum ThemeSetting {
80 #[default]
81 Dark,
82 Light,
83 System,
84}
85
86#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
88pub struct AccountInfo {
89 pub account_uuid: String,
90 pub email_address: String,
91 pub organization_uuid: Option<String>,
92 pub organization_name: Option<String>,
93 pub organization_role: Option<String>,
94 pub workspace_role: Option<String>,
95 pub display_name: Option<String>,
96 pub has_extra_usage_enabled: Option<bool>,
97 pub billing_type: Option<String>,
98 pub account_created_at: Option<String>,
99 pub subscription_created_at: Option<String>,
100}
101
102#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
104pub struct McpServerConfig {
105 pub command: Option<String>,
106 pub args: Option<Vec<String>>,
107 pub env: Option<HashMap<String, String>>,
108}
109
110#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
112pub struct ProjectConfig {
113 pub allowed_tools: Vec<String>,
114 pub mcp_context_uris: Vec<String>,
115 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
116 pub last_api_duration: Option<u64>,
117 pub last_api_duration_without_retries: Option<u64>,
118 pub last_tool_duration: Option<u64>,
119 pub last_cost: Option<f64>,
120 pub last_duration: Option<u64>,
121 pub last_lines_added: Option<u32>,
122 pub last_lines_removed: Option<u32>,
123 pub last_total_input_tokens: Option<u32>,
124 pub last_total_output_tokens: Option<u32>,
125 pub last_total_cache_creation_input_tokens: Option<u32>,
126 pub last_total_cache_read_input_tokens: Option<u32>,
127 pub last_total_web_search_requests: Option<u32>,
128 pub last_fps_average: Option<f64>,
129 pub last_fps_low_1_pct: Option<f64>,
130 pub last_session_id: Option<String>,
131 pub last_model_usage: Option<HashMap<String, ModelUsage>>,
132 pub last_session_metrics: Option<HashMap<String, f64>>,
133 pub example_files: Option<Vec<String>>,
134 pub example_files_generated_at: Option<u64>,
135 pub has_trust_dialog_accepted: bool,
136 pub has_completed_project_onboarding: bool,
137 pub project_onboarding_seen_count: u32,
138 pub has_claude_md_external_includes_approved: bool,
139 pub has_claude_md_external_includes_warning_shown: bool,
140 pub enabled_mcpjson_servers: Option<Vec<String>>,
141 pub disabled_mcpjson_servers: Option<Vec<String>>,
142 pub enable_all_project_mcp_servers: Option<bool>,
143 pub disabled_mcp_servers: Option<Vec<String>>,
144 pub enabled_mcp_servers: Option<Vec<String>>,
145 pub active_worktree_session: Option<WorktreeSession>,
146 pub remote_control_spawn_mode: Option<String>,
147}
148
149#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
151pub struct ModelUsage {
152 pub input_tokens: u32,
153 pub output_tokens: u32,
154 pub cache_read_input_tokens: u32,
155 pub cache_creation_input_tokens: u32,
156 pub web_search_requests: u32,
157 pub cost_usd: f64,
158}
159
160#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
162pub struct WorktreeSession {
163 pub original_cwd: String,
164 pub worktree_path: String,
165 pub worktree_name: String,
166 pub original_branch: Option<String>,
167 pub session_id: String,
168 pub hook_based: Option<bool>,
169}
170
171#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
173pub struct GlobalConfig {
174 pub api_key_helper: Option<String>,
175 pub projects: Option<HashMap<String, ProjectConfig>>,
176 pub num_startups: u32,
177 pub install_method: Option<InstallMethod>,
178 pub auto_updates: Option<bool>,
179 pub auto_updates_protected_for_native: Option<bool>,
180 pub doctor_shown_at_session: Option<u32>,
181 pub user_id: Option<String>,
182 pub theme: ThemeSetting,
183 pub has_completed_onboarding: Option<bool>,
184 pub last_onboarding_version: Option<String>,
185 pub last_release_notes_seen: Option<String>,
186 pub changelog_last_fetched: Option<u64>,
187 pub cached_changelog: Option<String>,
188 pub mcp_servers: Option<HashMap<String, McpServerConfig>>,
189 pub claude_ai_mcp_ever_connected: Option<Vec<String>>,
190 pub preferred_notif_channel: NotificationChannel,
191 pub custom_notify_command: Option<String>,
192 pub verbose: bool,
193 pub custom_api_key_responses: Option<CustomApiKeyResponses>,
194 pub primary_api_key: Option<String>,
195 pub has_acknowledged_cost_threshold: Option<bool>,
196 pub has_seen_undercover_auto_notice: Option<bool>,
197 pub has_seen_ultraplan_terms: Option<bool>,
198 pub has_reset_auto_mode_opt_in_for_default_offer: Option<bool>,
199 pub oauth_account: Option<AccountInfo>,
200 pub iterm2_key_binding_installed: Option<bool>,
201 pub editor_mode: Option<EditorMode>,
202 pub bypass_permissions_mode_accepted: Option<bool>,
203 pub has_used_backslash_return: Option<bool>,
204 pub auto_compact_enabled: bool,
205 pub show_turn_duration: bool,
206 pub env: HashMap<String, String>,
207 pub has_seen_tasks_hint: Option<bool>,
208 pub has_used_stash: Option<bool>,
209 pub has_used_background_task: Option<bool>,
210 pub queued_command_up_hint_count: Option<u32>,
211 pub diff_tool: Option<DiffTool>,
212 pub iterm2_setup_in_progress: Option<bool>,
213 pub iterm2_backup_path: Option<String>,
214 pub apple_terminal_backup_path: Option<String>,
215 pub apple_terminal_setup_in_progress: Option<bool>,
216 pub shift_enter_key_binding_installed: Option<bool>,
217 pub option_as_meta_key_installed: Option<bool>,
218 pub auto_connect_ide: Option<bool>,
219 pub auto_install_ide_extension: Option<bool>,
220 pub has_ide_onboarding_been_shown: Option<HashMap<String, bool>>,
221 pub ide_hint_shown_count: Option<u32>,
222 pub has_ide_auto_connect_dialog_been_shown: Option<bool>,
223 pub tips_history: HashMap<String, u32>,
224 pub companion: Option<serde_json::Value>,
225 pub companion_muted: Option<bool>,
226 pub feedback_survey_state: Option<FeedbackSurveyState>,
227 pub transcript_share_dismissed: Option<bool>,
228 pub memory_usage_count: u32,
229 pub has_shown_s1m_welcome_v2: Option<HashMap<String, bool>>,
230 pub s1m_access_cache: Option<HashMap<String, S1mAccessCacheEntry>>,
231 pub s1m_non_subscriber_access_cache: Option<HashMap<String, S1mAccessCacheEntry>>,
232 pub passes_eligibility_cache: Option<HashMap<String, serde_json::Value>>,
233 pub grove_config_cache: Option<HashMap<String, GroveConfigCacheEntry>>,
234 pub passes_upsell_seen_count: Option<u32>,
235 pub has_visited_passes: Option<bool>,
236 pub passes_last_seen_remaining: Option<u32>,
237 pub overage_credit_grant_cache: Option<HashMap<String, OverageCreditCacheEntry>>,
238 pub overage_credit_upsell_seen_count: Option<u32>,
239 pub has_visited_extra_usage: Option<bool>,
240 pub voice_notice_seen_count: Option<u32>,
241 pub voice_lang_hint_shown_count: Option<u32>,
242 pub voice_lang_hint_last_language: Option<String>,
243 pub voice_footer_hint_seen_count: Option<u32>,
244 pub opus_1m_merge_notice_seen_count: Option<u32>,
245 pub experiment_notices_seen_count: Option<HashMap<String, u32>>,
246 pub has_shown_opus_plan_welcome: Option<HashMap<String, bool>>,
247 pub prompt_queue_use_count: u32,
248 pub btw_use_count: u32,
249 pub last_plan_mode_use: Option<u64>,
250 pub subscription_notice_count: Option<u32>,
251 pub has_available_subscription: Option<bool>,
252 pub subscription_upsell_seen_count: Option<u32>,
253 pub recommended_subscription: Option<String>,
254 pub todo_feature_enabled: bool,
255 pub show_expanded_todos: Option<bool>,
256 pub show_spinner_tree: Option<bool>,
257 pub first_start_time: Option<String>,
258 pub message_idle_notif_threshold_ms: u64,
259 pub github_action_setup_count: Option<u32>,
260 pub slack_app_install_count: Option<u32>,
261 pub file_checkpointing_enabled: bool,
262 pub terminal_progress_bar_enabled: bool,
263 pub show_status_in_terminal_tab: Option<bool>,
264 pub task_complete_notif_enabled: Option<bool>,
265 pub input_needed_notif_enabled: Option<bool>,
266 pub agent_push_notif_enabled: Option<bool>,
267 pub claude_code_first_token_date: Option<String>,
268 pub model_switch_callout_dismissed: Option<bool>,
269 pub model_switch_callout_last_shown: Option<u64>,
270 pub model_switch_callout_version: Option<String>,
271 pub effort_callout_dismissed: Option<bool>,
272 pub effort_callout_v2_dismissed: Option<bool>,
273 pub remote_dialog_seen: Option<bool>,
274 pub bridge_oauth_dead_expires_at: Option<u64>,
275 pub bridge_oauth_dead_fail_count: Option<u32>,
276 pub desktop_upsell_seen_count: Option<u32>,
277 pub desktop_upsell_dismissed: Option<bool>,
278 pub idle_return_dismissed: Option<bool>,
279 pub opus_pro_migration_complete: Option<bool>,
280 pub opus_pro_migration_timestamp: Option<u64>,
281 pub sonnet_1m_45_migration_complete: Option<bool>,
282 pub legacy_opus_migration_timestamp: Option<u64>,
283 pub sonnet_45_to_46_migration_timestamp: Option<u64>,
284 pub cached_statsig_gates: HashMap<String, bool>,
285 pub cached_dynamic_configs: Option<HashMap<String, serde_json::Value>>,
286 pub cached_growth_book_features: Option<HashMap<String, serde_json::Value>>,
287 pub growth_book_overrides: Option<HashMap<String, serde_json::Value>>,
288 pub last_shown_emergency_tip: Option<String>,
289 pub respect_gitignore: bool,
290 pub copy_full_response: bool,
291 pub copy_on_select: Option<bool>,
292 pub github_repo_paths: Option<HashMap<String, Vec<String>>>,
293 pub deep_link_terminal: Option<String>,
294 pub iterm2_it2_setup_complete: Option<bool>,
295 pub prefer_tmux_over_iterm2: Option<bool>,
296 pub skill_usage: Option<HashMap<String, SkillUsageEntry>>,
297 pub official_marketplace_auto_install_attempted: Option<bool>,
298 pub official_marketplace_auto_installed: Option<bool>,
299 pub official_marketplace_auto_install_fail_reason: Option<String>,
300 pub official_marketplace_auto_install_retry_count: Option<u32>,
301 pub official_marketplace_auto_install_last_attempt_time: Option<u64>,
302 pub official_marketplace_auto_install_next_retry_time: Option<u64>,
303 pub has_completed_claude_in_chrome_onboarding: Option<bool>,
304 pub claude_in_chrome_default_enabled: Option<bool>,
305 pub cached_chrome_extension_installed: Option<bool>,
306 pub chrome_extension: Option<ChromeExtensionState>,
307 pub lsp_recommendation_disabled: Option<bool>,
308 pub lsp_recommendation_never_plugins: Option<Vec<String>>,
309 pub lsp_recommendation_ignored_count: Option<u32>,
310 pub claude_code_hints: Option<ClaudeCodeHints>,
311 pub permission_explainer_enabled: Option<bool>,
312 pub teammate_mode: Option<String>,
313 pub teammate_default_model: Option<String>,
314 pub pr_status_footer_enabled: Option<bool>,
315 pub tungsten_panel_visible: Option<bool>,
316 pub penguin_mode_org_enabled: Option<bool>,
317 pub startup_prefetched_at: Option<u64>,
318 pub remote_control_at_startup: Option<bool>,
319 pub cached_extra_usage_disabled_reason: Option<String>,
320 pub auto_permissions_notification_count: Option<u32>,
321 pub speculation_enabled: Option<bool>,
322 pub client_data_cache: Option<serde_json::Value>,
323 pub additional_model_options_cache: Option<Vec<serde_json::Value>>,
324 pub metrics_status_cache: Option<MetricsStatusCache>,
325 pub migration_version: Option<u32>,
326}
327
328#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
330pub struct FeedbackSurveyState {
331 pub last_shown_time: Option<u64>,
332}
333
334#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
336pub struct S1mAccessCacheEntry {
337 pub has_access: bool,
338 pub has_access_not_as_default: Option<bool>,
339 pub timestamp: u64,
340}
341
342#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
344pub struct GroveConfigCacheEntry {
345 pub grove_enabled: bool,
346 pub timestamp: u64,
347}
348
349#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
351pub struct OverageCreditCacheEntry {
352 pub info: OverageCreditInfo,
353 pub timestamp: u64,
354}
355
356#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
358pub struct OverageCreditInfo {
359 pub available: bool,
360 pub eligible: bool,
361 pub granted: bool,
362 pub amount_minor_units: Option<i64>,
363 pub currency: Option<String>,
364}
365
366#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
368pub struct SkillUsageEntry {
369 pub usage_count: u32,
370 pub last_used_at: u64,
371}
372
373#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
375pub struct ChromeExtensionState {
376 pub paired_device_id: Option<String>,
377 pub paired_device_name: Option<String>,
378}
379
380#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
382pub struct ClaudeCodeHints {
383 pub plugin: Option<Vec<String>>,
384 pub disabled: Option<bool>,
385}
386
387#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
389pub struct MetricsStatusCache {
390 pub enabled: bool,
391 pub timestamp: u64,
392}
393
394#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
396pub struct CustomApiKeyResponses {
397 pub approved: Option<Vec<String>>,
398 pub rejected: Option<Vec<String>>,
399}
400
401impl Default for GlobalConfig {
402 fn default() -> Self {
403 GlobalConfig {
404 num_startups: 0,
405 install_method: None,
406 auto_updates: None,
407 theme: ThemeSetting::Dark,
408 preferred_notif_channel: NotificationChannel::Auto,
409 verbose: true,
410 editor_mode: Some(EditorMode::Normal),
411 auto_compact_enabled: true,
412 show_turn_duration: true,
413 queued_command_up_hint_count: Some(0),
414 diff_tool: Some(DiffTool::Auto),
415 custom_api_key_responses: Some(CustomApiKeyResponses {
416 approved: Some(vec![]),
417 rejected: Some(vec![]),
418 }),
419 env: HashMap::new(),
420 tips_history: HashMap::new(),
421 memory_usage_count: 0,
422 prompt_queue_use_count: 0,
423 btw_use_count: 0,
424 todo_feature_enabled: true,
425 show_expanded_todos: Some(false),
426 message_idle_notif_threshold_ms: 60000,
427 auto_connect_ide: Some(false),
428 auto_install_ide_extension: Some(true),
429 file_checkpointing_enabled: true,
430 terminal_progress_bar_enabled: true,
431 cached_statsig_gates: HashMap::new(),
432 cached_dynamic_configs: Some(HashMap::new()),
433 cached_growth_book_features: Some(HashMap::new()),
434 respect_gitignore: true,
435 copy_full_response: false,
436 api_key_helper: None,
438 projects: None,
439 auto_updates_protected_for_native: None,
440 doctor_shown_at_session: None,
441 user_id: None,
442 has_completed_onboarding: None,
443 last_onboarding_version: None,
444 last_release_notes_seen: None,
445 changelog_last_fetched: None,
446 cached_changelog: None,
447 mcp_servers: None,
448 claude_ai_mcp_ever_connected: None,
449 custom_notify_command: None,
450 primary_api_key: None,
451 has_acknowledged_cost_threshold: None,
452 has_seen_undercover_auto_notice: None,
453 has_seen_ultraplan_terms: None,
454 has_reset_auto_mode_opt_in_for_default_offer: None,
455 oauth_account: None,
456 iterm2_key_binding_installed: None,
457 bypass_permissions_mode_accepted: None,
458 has_used_backslash_return: None,
459 has_seen_tasks_hint: None,
460 has_used_stash: None,
461 has_used_background_task: None,
462 iterm2_setup_in_progress: None,
463 iterm2_backup_path: None,
464 apple_terminal_backup_path: None,
465 apple_terminal_setup_in_progress: None,
466 shift_enter_key_binding_installed: None,
467 option_as_meta_key_installed: None,
468 has_ide_onboarding_been_shown: None,
469 ide_hint_shown_count: None,
470 has_ide_auto_connect_dialog_been_shown: None,
471 companion: None,
472 companion_muted: None,
473 feedback_survey_state: None,
474 transcript_share_dismissed: None,
475 has_shown_s1m_welcome_v2: None,
476 s1m_access_cache: None,
477 s1m_non_subscriber_access_cache: None,
478 passes_eligibility_cache: None,
479 grove_config_cache: None,
480 passes_upsell_seen_count: None,
481 has_visited_passes: None,
482 passes_last_seen_remaining: None,
483 overage_credit_grant_cache: None,
484 overage_credit_upsell_seen_count: None,
485 has_visited_extra_usage: None,
486 voice_notice_seen_count: None,
487 voice_lang_hint_shown_count: None,
488 voice_lang_hint_last_language: None,
489 voice_footer_hint_seen_count: None,
490 opus_1m_merge_notice_seen_count: None,
491 experiment_notices_seen_count: None,
492 has_shown_opus_plan_welcome: None,
493 last_plan_mode_use: None,
494 subscription_notice_count: None,
495 has_available_subscription: None,
496 subscription_upsell_seen_count: None,
497 recommended_subscription: None,
498 show_spinner_tree: None,
499 first_start_time: None,
500 github_action_setup_count: None,
501 slack_app_install_count: None,
502 show_status_in_terminal_tab: None,
503 task_complete_notif_enabled: None,
504 input_needed_notif_enabled: None,
505 agent_push_notif_enabled: None,
506 claude_code_first_token_date: None,
507 model_switch_callout_dismissed: None,
508 model_switch_callout_last_shown: None,
509 model_switch_callout_version: None,
510 effort_callout_dismissed: None,
511 effort_callout_v2_dismissed: None,
512 remote_dialog_seen: None,
513 bridge_oauth_dead_expires_at: None,
514 bridge_oauth_dead_fail_count: None,
515 desktop_upsell_seen_count: None,
516 desktop_upsell_dismissed: None,
517 idle_return_dismissed: None,
518 opus_pro_migration_complete: None,
519 opus_pro_migration_timestamp: None,
520 sonnet_1m_45_migration_complete: None,
521 legacy_opus_migration_timestamp: None,
522 sonnet_45_to_46_migration_timestamp: None,
523 growth_book_overrides: None,
524 last_shown_emergency_tip: None,
525 copy_on_select: None,
526 github_repo_paths: None,
527 deep_link_terminal: None,
528 iterm2_it2_setup_complete: None,
529 prefer_tmux_over_iterm2: None,
530 skill_usage: None,
531 official_marketplace_auto_install_attempted: None,
532 official_marketplace_auto_installed: None,
533 official_marketplace_auto_install_fail_reason: None,
534 official_marketplace_auto_install_retry_count: None,
535 official_marketplace_auto_install_last_attempt_time: None,
536 official_marketplace_auto_install_next_retry_time: None,
537 has_completed_claude_in_chrome_onboarding: None,
538 claude_in_chrome_default_enabled: None,
539 cached_chrome_extension_installed: None,
540 chrome_extension: None,
541 lsp_recommendation_disabled: None,
542 lsp_recommendation_never_plugins: None,
543 lsp_recommendation_ignored_count: None,
544 claude_code_hints: None,
545 permission_explainer_enabled: None,
546 teammate_mode: None,
547 teammate_default_model: None,
548 pr_status_footer_enabled: None,
549 tungsten_panel_visible: None,
550 penguin_mode_org_enabled: None,
551 startup_prefetched_at: None,
552 remote_control_at_startup: None,
553 cached_extra_usage_disabled_reason: None,
554 auto_permissions_notification_count: None,
555 speculation_enabled: None,
556 client_data_cache: None,
557 additional_model_options_cache: None,
558 metrics_status_cache: None,
559 migration_version: None,
560 }
561 }
562}
563
564pub fn get_global_config_path() -> PathBuf {
566 let config_dir = std::env::var(ai::CONFIG_DIR)
568 .or_else(|_| std::env::var(ai::CLAUDE_CONFIG_DIR))
569 .unwrap_or_else(|_| {
570 dirs::home_dir()
571 .map(|h| h.join(".claude").to_string_lossy().to_string())
572 .unwrap_or_else(|| "~/.claude".to_string())
573 });
574
575 PathBuf::from(config_dir).join("settings.json")
576}
577
578pub fn get_global_config() -> GlobalConfig {
580 let path = get_global_config_path();
581
582 if !path.exists() {
583 return GlobalConfig::default();
584 }
585
586 match fs::read_to_string(&path) {
587 Ok(content) => match serde_json::from_str::<GlobalConfig>(&content) {
588 Ok(config) => {
589 let mut default_config = GlobalConfig::default();
591 merge_config(&mut default_config, config);
592 default_config
593 }
594 Err(e) => {
595 eprintln!("Failed to parse config: {}", e);
596 GlobalConfig::default()
597 }
598 },
599 Err(e) => {
600 eprintln!("Failed to read config file: {}", e);
601 GlobalConfig::default()
602 }
603 }
604}
605
606fn merge_config(default: &mut GlobalConfig, loaded: GlobalConfig) {
608 macro_rules! merge_option {
612 ($field:ident) => {
613 if let Some(v) = loaded.$field {
614 default.$field = Some(v);
615 }
616 };
617 }
618
619 macro_rules! merge_hashmap {
620 ($field:ident) => {
621 if let Some(v) = loaded.$field {
622 default.$field = v;
623 }
624 };
625 }
626
627 merge_option!(api_key_helper);
628 merge_option!(projects);
629 default.num_startups = loaded.num_startups;
630 merge_option!(install_method);
631 merge_option!(auto_updates);
632 merge_option!(auto_updates_protected_for_native);
633 merge_option!(doctor_shown_at_session);
634 merge_option!(user_id);
635 default.theme = loaded.theme;
636 merge_option!(has_completed_onboarding);
637 merge_option!(last_onboarding_version);
638 merge_option!(last_release_notes_seen);
639 merge_option!(changelog_last_fetched);
640 merge_option!(cached_changelog);
641 merge_option!(mcp_servers);
642 merge_option!(claude_ai_mcp_ever_connected);
643 default.preferred_notif_channel = loaded.preferred_notif_channel;
644 merge_option!(custom_notify_command);
645 default.verbose = loaded.verbose;
646 merge_option!(custom_api_key_responses);
647 merge_option!(primary_api_key);
648 merge_option!(has_acknowledged_cost_threshold);
649 merge_option!(has_seen_undercover_auto_notice);
650 merge_option!(has_seen_ultraplan_terms);
651 merge_option!(has_reset_auto_mode_opt_in_for_default_offer);
652 merge_option!(oauth_account);
653 merge_option!(iterm2_key_binding_installed);
654 merge_option!(editor_mode);
655 merge_option!(bypass_permissions_mode_accepted);
656 merge_option!(has_used_backslash_return);
657 default.auto_compact_enabled = loaded.auto_compact_enabled;
658 default.show_turn_duration = loaded.show_turn_duration;
659 default.env = loaded.env;
660 merge_option!(has_seen_tasks_hint);
661 merge_option!(has_used_stash);
662 merge_option!(has_used_background_task);
663 merge_option!(queued_command_up_hint_count);
664 merge_option!(diff_tool);
665 merge_option!(iterm2_setup_in_progress);
666 merge_option!(iterm2_backup_path);
667 merge_option!(apple_terminal_backup_path);
668 merge_option!(apple_terminal_setup_in_progress);
669 merge_option!(shift_enter_key_binding_installed);
670 merge_option!(option_as_meta_key_installed);
671 merge_option!(auto_connect_ide);
672 merge_option!(auto_install_ide_extension);
673 merge_option!(has_ide_onboarding_been_shown);
674 merge_option!(ide_hint_shown_count);
675 merge_option!(has_ide_auto_connect_dialog_been_shown);
676 default.tips_history = loaded.tips_history;
677 merge_option!(companion);
678 merge_option!(companion_muted);
679 merge_option!(feedback_survey_state);
680 merge_option!(transcript_share_dismissed);
681 default.memory_usage_count = loaded.memory_usage_count;
682 merge_option!(has_shown_s1m_welcome_v2);
683 merge_option!(s1m_access_cache);
684 merge_option!(s1m_non_subscriber_access_cache);
685 merge_option!(passes_eligibility_cache);
686 merge_option!(grove_config_cache);
687 merge_option!(passes_upsell_seen_count);
688 merge_option!(has_visited_passes);
689 merge_option!(passes_last_seen_remaining);
690 merge_option!(overage_credit_grant_cache);
691 merge_option!(overage_credit_upsell_seen_count);
692 merge_option!(has_visited_extra_usage);
693 merge_option!(voice_notice_seen_count);
694 merge_option!(voice_lang_hint_shown_count);
695 merge_option!(voice_lang_hint_last_language);
696 merge_option!(voice_footer_hint_seen_count);
697 merge_option!(opus_1m_merge_notice_seen_count);
698 merge_option!(experiment_notices_seen_count);
699 merge_option!(has_shown_opus_plan_welcome);
700 default.prompt_queue_use_count = loaded.prompt_queue_use_count;
701 default.btw_use_count = loaded.btw_use_count;
702 merge_option!(last_plan_mode_use);
703 merge_option!(subscription_notice_count);
704 merge_option!(has_available_subscription);
705 merge_option!(subscription_upsell_seen_count);
706 merge_option!(recommended_subscription);
707 default.todo_feature_enabled = loaded.todo_feature_enabled;
708 merge_option!(show_expanded_todos);
709 merge_option!(show_spinner_tree);
710 merge_option!(first_start_time);
711 default.message_idle_notif_threshold_ms = loaded.message_idle_notif_threshold_ms;
712 merge_option!(github_action_setup_count);
713 merge_option!(slack_app_install_count);
714 default.file_checkpointing_enabled = loaded.file_checkpointing_enabled;
715 default.terminal_progress_bar_enabled = loaded.terminal_progress_bar_enabled;
716 merge_option!(show_status_in_terminal_tab);
717 merge_option!(task_complete_notif_enabled);
718 merge_option!(input_needed_notif_enabled);
719 merge_option!(agent_push_notif_enabled);
720 merge_option!(claude_code_first_token_date);
721 merge_option!(model_switch_callout_dismissed);
722 merge_option!(model_switch_callout_last_shown);
723 merge_option!(model_switch_callout_version);
724 merge_option!(effort_callout_dismissed);
725 merge_option!(effort_callout_v2_dismissed);
726 merge_option!(remote_dialog_seen);
727 merge_option!(bridge_oauth_dead_expires_at);
728 merge_option!(bridge_oauth_dead_fail_count);
729 merge_option!(desktop_upsell_seen_count);
730 merge_option!(desktop_upsell_dismissed);
731 merge_option!(idle_return_dismissed);
732 merge_option!(opus_pro_migration_complete);
733 merge_option!(opus_pro_migration_timestamp);
734 merge_option!(sonnet_1m_45_migration_complete);
735 merge_option!(legacy_opus_migration_timestamp);
736 merge_option!(sonnet_45_to_46_migration_timestamp);
737 default.cached_statsig_gates = loaded.cached_statsig_gates;
738 merge_option!(cached_dynamic_configs);
739 merge_option!(cached_growth_book_features);
740 merge_option!(growth_book_overrides);
741 merge_option!(last_shown_emergency_tip);
742 default.respect_gitignore = loaded.respect_gitignore;
743 default.copy_full_response = loaded.copy_full_response;
744 merge_option!(copy_on_select);
745 merge_option!(github_repo_paths);
746 merge_option!(deep_link_terminal);
747 merge_option!(iterm2_it2_setup_complete);
748 merge_option!(prefer_tmux_over_iterm2);
749 merge_option!(skill_usage);
750 merge_option!(official_marketplace_auto_install_attempted);
751 merge_option!(official_marketplace_auto_installed);
752 merge_option!(official_marketplace_auto_install_fail_reason);
753 merge_option!(official_marketplace_auto_install_retry_count);
754 merge_option!(official_marketplace_auto_install_last_attempt_time);
755 merge_option!(official_marketplace_auto_install_next_retry_time);
756 merge_option!(has_completed_claude_in_chrome_onboarding);
757 merge_option!(claude_in_chrome_default_enabled);
758 merge_option!(cached_chrome_extension_installed);
759 merge_option!(chrome_extension);
760 merge_option!(lsp_recommendation_disabled);
761 merge_option!(lsp_recommendation_never_plugins);
762 merge_option!(lsp_recommendation_ignored_count);
763 merge_option!(claude_code_hints);
764 merge_option!(permission_explainer_enabled);
765 merge_option!(teammate_mode);
766 merge_option!(teammate_default_model);
767 merge_option!(pr_status_footer_enabled);
768 merge_option!(tungsten_panel_visible);
769 merge_option!(penguin_mode_org_enabled);
770 merge_option!(startup_prefetched_at);
771 merge_option!(remote_control_at_startup);
772 merge_option!(cached_extra_usage_disabled_reason);
773 merge_option!(auto_permissions_notification_count);
774 merge_option!(speculation_enabled);
775 merge_option!(client_data_cache);
776 merge_option!(additional_model_options_cache);
777 merge_option!(metrics_status_cache);
778 merge_option!(migration_version);
779}
780
781pub fn save_global_config(config: &GlobalConfig) -> Result<(), String> {
783 let path = get_global_config_path();
784
785 if let Some(parent) = path.parent() {
787 fs::create_dir_all(parent)
788 .map_err(|e| format!("Failed to create config directory: {}", e))?;
789 }
790
791 let json = serde_json::to_string_pretty(config)
792 .map_err(|e| format!("Failed to serialize config: {}", e))?;
793
794 fs::write(&path, json).map_err(|e| format!("Failed to write config file: {}", e))?;
795
796 if let Ok(mut cache) = GLOBAL_CONFIG_CACHE.lock() {
798 cache.config = Some(config.clone());
799 cache.mtime = std::time::SystemTime::now()
800 .duration_since(std::time::UNIX_EPOCH)
801 .map(|d| d.as_millis() as u64)
802 .unwrap_or(0);
803 }
804
805 Ok(())
806}
807
808pub fn get_current_project_config() -> ProjectConfig {
810 let global_config = get_global_config();
811
812 let project_path = std::env::var(ai::PROJECT_PATH)
814 .or_else(|_| std::env::var(ai::CLAUDE_PROJECT_PATH))
815 .unwrap_or_else(|_err| {
816 std::env::current_dir()
817 .map(|p| p.to_string_lossy().to_string())
818 .unwrap_or_default()
819 });
820
821 global_config
822 .projects
823 .and_then(|p| p.get(&project_path).cloned())
824 .unwrap_or_default()
825}
826
827pub fn save_current_project_config(config: ProjectConfig) -> Result<(), String> {
829 let project_path = std::env::var(ai::PROJECT_PATH)
830 .or_else(|_| std::env::var(ai::CLAUDE_PROJECT_PATH))
831 .unwrap_or_else(|_| {
832 std::env::current_dir()
833 .map(|p| p.to_string_lossy().to_string())
834 .unwrap_or_default()
835 });
836
837 let mut global_config = get_global_config();
838
839 let projects = global_config.projects.get_or_insert_with(HashMap::new);
840 projects.insert(project_path, config);
841
842 save_global_config(&global_config)
843}
844
845pub fn get_or_create_user_id() -> String {
847 let mut config = get_global_config();
848
849 if let Some(user_id) = &config.user_id {
850 return user_id.clone();
851 }
852
853 let user_id = uuid::Uuid::new_v4().to_string();
855 config.user_id = Some(user_id.clone());
856
857 let _ = save_global_config(&config);
858
859 user_id
860}
861
862#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
864#[serde(tag = "type", content = "envVar")]
865pub enum AutoUpdaterDisabledReason {
866 Development,
867 Env { env_var: String },
868 Config,
869}
870
871pub fn get_auto_updater_disabled_reason() -> Option<AutoUpdaterDisabledReason> {
873 if std::env::var(system::NODE_ENV)
875 .map(|v| v == "development")
876 .unwrap_or(false)
877 {
878 return Some(AutoUpdaterDisabledReason::Development);
879 }
880
881 if std::env::var(ai::DISABLE_AUTOUPDATER)
883 .or_else(|_| std::env::var(system::DISABLE_AUTOUPDATER))
884 .map(|v| !v.is_empty() && v != "false")
885 .unwrap_or(false)
886 {
887 return Some(AutoUpdaterDisabledReason::Env {
888 env_var: "DISABLE_AUTOUPDATER".to_string(),
889 });
890 }
891
892 let config = get_global_config();
894 if config.auto_updates == Some(false)
895 && (config.install_method != Some(InstallMethod::Native)
896 || config.auto_updates_protected_for_native != Some(true))
897 {
898 return Some(AutoUpdaterDisabledReason::Config);
899 }
900
901 None
902}
903
904pub fn is_auto_updater_disabled() -> bool {
906 get_auto_updater_disabled_reason().is_some()
907}
908
909pub fn get_custom_api_key_status(truncated_api_key: &str) -> &'static str {
911 let config = get_global_config();
912
913 if let Some(responses) = &config.custom_api_key_responses {
914 if let Some(approved) = &responses.approved {
915 if approved.contains(&truncated_api_key.to_string()) {
916 return "approved";
917 }
918 }
919 if let Some(rejected) = &responses.rejected {
920 if rejected.contains(&truncated_api_key.to_string()) {
921 return "rejected";
922 }
923 }
924 }
925
926 "new"
927}
928
929pub fn record_first_start_time() {
931 let mut config = get_global_config();
932
933 if config.first_start_time.is_none() {
934 config.first_start_time = Some(chrono::Utc::now().to_rfc3339());
935 let _ = save_global_config(&config);
936 }
937}
938
939#[cfg(test)]
940mod tests {
941 use super::*;
942
943 #[test]
944 fn test_default_config() {
945 let config = GlobalConfig::default();
946 assert_eq!(config.num_startups, 0);
947 assert_eq!(config.theme, ThemeSetting::Dark);
948 assert!(config.verbose);
949 assert!(config.auto_compact_enabled);
950 }
951
952 #[test]
953 fn test_get_global_config_path() {
954 let path = get_global_config_path();
955 assert!(path.to_string_lossy().contains(".claude"));
956 }
957
958 #[test]
959 fn test_is_auto_updater_disabled() {
960 let _ = is_auto_updater_disabled();
962 }
963
964 #[test]
965 fn test_get_custom_api_key_status() {
966 assert_eq!(get_custom_api_key_status("test-key"), "new");
967 }
968}