claude_code_cli_acp/config/
settings.rs1use std::{
2 collections::BTreeMap,
3 path::{Path, PathBuf},
4};
5
6use anyhow::Context;
7use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct ClaudeSettings {
12 #[serde(default)]
13 pub permissions: PermissionSettings,
14 #[serde(default)]
15 pub env: BTreeMap<String, String>,
16 pub model: Option<String>,
17 #[serde(rename = "effortLevel")]
18 pub effort_level: Option<String>,
19 pub available_models: Option<Vec<String>>,
20}
21
22#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "camelCase")]
24pub struct PermissionSettings {
25 pub default_mode: Option<String>,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct SettingsPaths {
30 pub user: PathBuf,
31 pub project: PathBuf,
32 pub local: PathBuf,
33 pub managed: PathBuf,
34}
35
36impl SettingsPaths {
37 pub fn for_cwd(cwd: impl AsRef<Path>) -> anyhow::Result<Self> {
38 let home = dirs::home_dir().context("home directory unavailable")?;
39 Ok(Self::for_cwd_and_home(cwd, home))
40 }
41
42 pub fn for_cwd_and_home(cwd: impl AsRef<Path>, home: impl AsRef<Path>) -> Self {
43 let config_dir = std::env::var_os("CLAUDE_CONFIG_DIR")
44 .map(PathBuf::from)
45 .unwrap_or_else(|| home.as_ref().join(".claude"));
46 Self {
47 user: config_dir.join("settings.json"),
48 project: cwd.as_ref().join(".claude/settings.json"),
49 local: cwd.as_ref().join(".claude/settings.local.json"),
50 managed: managed_settings_path(),
51 }
52 }
53
54 pub fn with_managed(mut self, managed: impl Into<PathBuf>) -> Self {
55 self.managed = managed.into();
56 self
57 }
58}
59
60#[derive(Clone, Debug, Default, PartialEq, Eq)]
61pub struct SettingsLoad {
62 pub settings: ClaudeSettings,
63 pub warnings: Vec<String>,
64}
65
66pub fn load_merged_settings(paths: &SettingsPaths) -> SettingsLoad {
67 let mut load = SettingsLoad::default();
68 for path in [&paths.user, &paths.project, &paths.local, &paths.managed] {
69 match load_settings_file(path) {
70 Ok(Some(settings)) => merge_settings(&mut load.settings, settings),
71 Ok(None) => {}
72 Err(err) => load.warnings.push(format!(
73 "failed to load settings from {}: {err}",
74 path.display()
75 )),
76 }
77 }
78 load
79}
80
81fn load_settings_file(path: &Path) -> anyhow::Result<Option<ClaudeSettings>> {
82 match std::fs::read_to_string(path) {
83 Ok(contents) => serde_json::from_str(&contents)
84 .with_context(|| format!("parse settings {}", path.display()))
85 .map(Some),
86 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
87 Err(err) => Err(err).with_context(|| format!("read settings {}", path.display())),
88 }
89}
90
91fn merge_settings(merged: &mut ClaudeSettings, settings: ClaudeSettings) {
92 merged.env.extend(settings.env);
93 if let Some(model) = settings.model {
94 merged.model = Some(model);
95 }
96 if let Some(effort_level) = settings.effort_level {
97 merged.effort_level = Some(effort_level);
98 }
99 if let Some(default_mode) = settings.permissions.default_mode {
100 merged.permissions.default_mode = Some(default_mode);
101 }
102 if let Some(models) = settings.available_models {
103 let existing = merged.available_models.get_or_insert_with(Vec::new);
104 for model in models {
105 if !existing.contains(&model) {
106 existing.push(model);
107 }
108 }
109 }
110}
111
112fn managed_settings_path() -> PathBuf {
113 if let Some(path) = std::env::var_os("CLAUDE_CODE_ACP_MANAGED_SETTINGS") {
114 return path.into();
115 }
116
117 #[cfg(target_os = "macos")]
118 {
119 PathBuf::from("/Library/Application Support/ClaudeCode/managed-settings.json")
120 }
121 #[cfg(target_os = "windows")]
122 {
123 PathBuf::from(r"C:\Program Files\ClaudeCode\managed-settings.json")
124 }
125 #[cfg(all(not(target_os = "macos"), not(target_os = "windows")))]
126 {
127 PathBuf::from("/etc/claude-code/managed-settings.json")
128 }
129}