pitboard_core/
settings.rs1use crate::context::Context;
23use crate::provider::claude::paths as claude;
24use serde_json::Value;
25use std::path::PathBuf;
26
27pub(crate) const OVERRIDING_ENV: [&str; 9] = [
30 "ANTHROPIC_API_KEY",
31 "ANTHROPIC_AUTH_TOKEN",
32 "CLAUDE_CODE_OAUTH_TOKEN",
33 "CLAUDE_CODE_USE_BEDROCK",
34 "CLAUDE_CODE_USE_VERTEX",
35 "CLAUDE_CODE_USE_FOUNDRY",
36 "CLAUDE_CODE_USE_GATEWAY",
37 "CLAUDE_CODE_USE_ANTHROPIC_AWS",
38 "CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD",
39];
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct Override {
44 pub layer: String,
46 pub key: String,
48}
49
50impl std::fmt::Display for Override {
51 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 write!(f, "{} in {}", self.key, self.layer)
53 }
54}
55
56fn managed_dir() -> PathBuf {
58 if cfg!(target_os = "macos") {
59 PathBuf::from("/Library/Application Support/ClaudeCode")
60 } else {
61 PathBuf::from("/etc/claude-code")
62 }
63}
64
65fn managed_files() -> Vec<PathBuf> {
66 let dir = managed_dir();
67 let mut files = vec![dir.join("managed-settings.json")];
68 if let Ok(entries) = std::fs::read_dir(dir.join("managed-settings.d")) {
69 let mut drop_ins: Vec<PathBuf> = entries
70 .flatten()
71 .map(|e| e.path())
72 .filter(|p| p.extension().is_some_and(|e| e == "json"))
73 .collect();
74 drop_ins.sort();
75 files.extend(drop_ins);
76 }
77 files
78}
79
80fn is_set(value: &str) -> bool {
82 !matches!(value.trim(), "" | "0" | "false")
83}
84
85fn overrides_in(path: &std::path::Path) -> Vec<Override> {
86 let Ok(raw) = std::fs::read_to_string(path) else {
87 return Vec::new();
88 };
89 let Ok(json) = serde_json::from_str::<Value>(&raw) else {
90 return Vec::new();
91 };
92 let layer = path.display().to_string();
93 let mut found = Vec::new();
94 if json
96 .get("apiKeyHelper")
97 .and_then(Value::as_str)
98 .is_some_and(|h| !h.trim().is_empty())
99 {
100 found.push(Override {
101 layer: layer.clone(),
102 key: "apiKeyHelper".into(),
103 });
104 }
105 if let Some(env) = json.get("env").and_then(Value::as_object) {
106 for name in OVERRIDING_ENV {
107 let set = match env.get(name) {
108 Some(Value::String(v)) => is_set(v),
109 Some(Value::Bool(b)) => *b,
110 Some(Value::Number(n)) => n.as_i64() != Some(0),
111 _ => false,
112 };
113 if set {
114 found.push(Override {
115 layer: layer.clone(),
116 key: format!("env.{name}"),
117 });
118 }
119 }
120 }
121 found
122}
123
124pub fn custom_oauth(ctx: &Context) -> bool {
131 if ctx.custom_oauth() {
132 return true;
133 }
134 managed_files()
135 .iter()
136 .chain(std::iter::once(
137 &claude::config_dir(ctx).join("settings.json"),
138 ))
139 .any(|path| {
140 std::fs::read_to_string(path)
141 .ok()
142 .and_then(|raw| serde_json::from_str::<Value>(&raw).ok())
143 .and_then(|json| {
144 json.get("env")?
145 .get("CLAUDE_CODE_CUSTOM_OAUTH_URL")?
146 .as_str()
147 .map(is_set)
148 })
149 .unwrap_or(false)
150 })
151}
152
153pub fn overrides(ctx: &Context) -> Vec<Override> {
157 let mut found: Vec<Override> = managed_files()
158 .iter()
159 .chain(std::iter::once(
160 &claude::config_dir(ctx).join("settings.json"),
161 ))
162 .flat_map(|path| overrides_in(path))
163 .collect();
164 found.extend(ctx.overriding_auth().iter().map(|key| Override {
166 layer: "the environment".into(),
167 key: key.clone(),
168 }));
169 found
170}
171
172#[cfg(test)]
173pub(crate) mod tests_support {
174 use super::*;
175
176 pub(crate) struct Scratch(pub(crate) PathBuf);
177 impl Drop for Scratch {
178 fn drop(&mut self) {
179 let _ = std::fs::remove_dir_all(&self.0);
180 }
181 }
182
183 pub(crate) fn home(name: &str) -> (Context, PathBuf, Scratch) {
184 let root = std::env::temp_dir().join(format!(
185 "pitboard-settings-{name}-{}-{:?}",
186 std::process::id(),
187 std::thread::current().id()
188 ));
189 let _ = std::fs::remove_dir_all(&root);
190 let config = root.join(".claude");
191 std::fs::create_dir_all(&config).expect("a config dir");
192 let ctx = Context::new(root.clone())
193 .with_claude_config_dir(config.to_string_lossy().into_owned());
194 (ctx, config, Scratch(root))
195 }
196
197 pub(crate) fn write(path: &std::path::Path, value: Value) {
198 std::fs::write(path, value.to_string()).expect("written");
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::tests_support::*;
205 use super::*;
206 use serde_json::json;
207
208 #[test]
209 fn an_ordinary_machine_has_nothing_in_the_way() {
210 let (ctx, config, _s) = home("clean");
211 write(&config.join("settings.json"), json!({"theme": "dark"}));
212 assert_eq!(overrides(&ctx), Vec::new());
213 }
214
215 #[test]
216 fn a_settings_file_that_sets_a_key_is_found_where_the_environment_would_not_be() {
217 let (ctx, config, _s) = home("env-block");
218 write(
219 &config.join("settings.json"),
220 json!({"env": {"ANTHROPIC_API_KEY": "sk-ant-something"}}),
221 );
222 let found = overrides(&ctx);
223 assert_eq!(found.len(), 1, "{found:?}");
224 assert_eq!(found[0].key, "env.ANTHROPIC_API_KEY");
225 assert!(found[0].layer.ends_with("settings.json"));
226 }
227
228 #[test]
229 fn a_helper_that_prints_a_key_is_an_override_whatever_it_prints() {
230 let (ctx, config, _s) = home("helper");
231 write(
232 &config.join("settings.json"),
233 json!({"apiKeyHelper": "/usr/local/bin/get-key"}),
234 );
235 assert_eq!(overrides(&ctx)[0].key, "apiKeyHelper");
236 }
237
238 #[test]
239 fn a_third_party_provider_is_an_override_too() {
240 let (ctx, config, _s) = home("bedrock");
241 write(
242 &config.join("settings.json"),
243 json!({"env": {"CLAUDE_CODE_USE_BEDROCK": "1"}}),
244 );
245 assert_eq!(overrides(&ctx)[0].key, "env.CLAUDE_CODE_USE_BEDROCK");
246 }
247
248 #[test]
250 fn a_key_that_is_present_but_empty_sets_nothing() {
251 let (ctx, config, _s) = home("empty");
252 write(
253 &config.join("settings.json"),
254 json!({
255 "apiKeyHelper": " ",
256 "env": {
257 "ANTHROPIC_API_KEY": "",
258 "CLAUDE_CODE_USE_BEDROCK": "0",
259 "CLAUDE_CODE_USE_VERTEX": "false",
260 },
261 }),
262 );
263 assert_eq!(overrides(&ctx), Vec::new());
264 }
265
266 #[test]
267 fn a_settings_file_that_is_not_json_is_not_a_reason_to_refuse_anything() {
268 let (ctx, config, _s) = home("broken");
269 std::fs::write(config.join("settings.json"), "{ not json").expect("written");
270 assert_eq!(overrides(&ctx), Vec::new());
271 }
272}
273
274#[cfg(test)]
275mod custom_oauth_tests {
276 use super::tests_support::*;
277 use super::*;
278 use serde_json::json;
279
280 #[test]
281 fn a_custom_endpoint_in_a_settings_file_is_found_the_way_the_environment_one_is() {
282 let (ctx, config, _s) = home("custom-oauth");
283 assert!(!custom_oauth(&ctx));
284 write(
285 &config.join("settings.json"),
286 json!({"env": {"CLAUDE_CODE_CUSTOM_OAUTH_URL": "https://oauth.example"}}),
287 );
288 assert!(custom_oauth(&ctx));
289 }
290}