Skip to main content

claude_code_cli_acp/config/
settings.rs

1use 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}