use crate::context::Context;
use crate::host::{Administered, Hive, Os};
use crate::provider::claude::paths as claude;
use serde_json::Value;
use std::path::PathBuf;
pub(crate) const OVERRIDING_ENV: [&str; 9] = [
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
"CLAUDE_CODE_OAUTH_TOKEN",
"CLAUDE_CODE_USE_BEDROCK",
"CLAUDE_CODE_USE_VERTEX",
"CLAUDE_CODE_USE_FOUNDRY",
"CLAUDE_CODE_USE_GATEWAY",
"CLAUDE_CODE_USE_ANTHROPIC_AWS",
"CLAUDE_CODE_USE_ANTHROPIC_GOOGLE_CLOUD",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Override {
pub(crate) layer: String,
pub(crate) key: String,
}
impl std::fmt::Display for Override {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} in {}", self.key, self.layer)
}
}
fn managed_dir(os: Os) -> PathBuf {
match os {
Os::MacOs => "/Library/Application Support/ClaudeCode".into(),
Os::Linux => "/etc/claude-code".into(),
Os::Windows => r"C:\Program Files\ClaudeCode".into(),
}
}
const POLICIES: &str = r"SOFTWARE\Policies\ClaudeCode";
const POLICY: &str = "Settings";
struct Layer {
name: String,
text: String,
}
fn managed_files(ctx: &Context) -> Vec<PathBuf> {
let dir = managed_dir(ctx.os);
let mut files = vec![dir.join("managed-settings.json")];
let mut drop_ins: Vec<PathBuf> = ctx
.host()
.administered_folder(&dir.join("managed-settings.d"))
.into_iter()
.filter(|p| p.extension().is_some_and(|e| e == "json"))
.collect();
drop_ins.sort();
files.extend(drop_ins);
files
}
fn managed(ctx: &Context) -> Vec<Layer> {
let host = ctx.host();
let mut there = false;
let mut layers: Vec<Layer> = managed_files(ctx)
.into_iter()
.filter_map(|path| match host.administered_file(&path) {
Administered::Set(text) => {
there |= holds_policy(&text);
Some(Layer {
name: path.display().to_string(),
text,
})
}
Administered::Unreadable(_) => {
there = true;
None
}
Administered::Unset => None,
})
.collect();
if ctx.os == Os::Windows {
for hive in [Hive::Machine, Hive::User] {
if hive == Hive::User && there {
break;
}
match host.policy(hive, POLICIES, POLICY) {
Administered::Set(text) => {
there |= holds_policy(&text);
layers.push(Layer {
name: format!(r"{}\{POLICIES}\{POLICY}", hive.name()),
text,
});
}
Administered::Unreadable(_) => there |= hive != Hive::Machine,
Administered::Unset => {}
}
}
}
layers
}
fn managed_taken(ctx: &Context) -> Result<Vec<Layer>, String> {
let host = ctx.host();
let mut folder = Vec::new();
let mut unread = None;
for path in managed_files(ctx) {
match host.administered_file(&path) {
Administered::Set(text) => folder.push(Layer {
name: path.display().to_string(),
text,
}),
Administered::Unreadable(why) => {
unread.get_or_insert(format!("{}: {why}", path.display()));
}
Administered::Unset => {}
}
}
let folder = || unread.map_or(Ok(folder), Err);
if ctx.os != Os::Windows {
return folder();
}
let policy = |hive: Hive| {
let name = format!(r"{}\{POLICIES}\{POLICY}", hive.name());
match host.policy(hive, POLICIES, POLICY) {
Administered::Set(text) if holds_policy(&text) => Ok(Some(Layer { name, text })),
Administered::Set(_) | Administered::Unset => Ok(None),
Administered::Unreadable(why) => Err(format!("{name}: {why}")),
}
};
if let Some(machine) = policy(Hive::Machine)? {
let merges = serde_json::from_str::<Value>(&machine.text)
.ok()
.and_then(|json| Some(json.get("managedSourcesBehavior")?.as_str()? == "merge"))
.unwrap_or(false);
let mut taken = vec![machine];
if merges {
taken.extend(folder()?);
}
return Ok(taken);
}
let folder = folder()?;
if folder.iter().any(|layer| holds_policy(&layer.text)) {
return Ok(folder);
}
Ok(policy(Hive::User)?.into_iter().collect())
}
fn holds_policy(text: &str) -> bool {
match serde_json::from_str::<Value>(text) {
Ok(Value::Object(keys)) => keys.keys().any(|key| {
!matches!(
key.as_str(),
"managedSourcesBehavior" | "wslInheritsWindowsSettings"
)
}),
Ok(_) | Err(_) => !text.trim().is_empty(),
}
}
fn is_set(value: &str) -> bool {
!matches!(value.trim(), "" | "0" | "false")
}
fn own(path: &std::path::Path) -> Option<Layer> {
Some(Layer {
name: path.display().to_string(),
text: std::fs::read_to_string(path).ok()?,
})
}
fn overrides_in(layer: &Layer) -> Vec<Override> {
let Ok(json) = serde_json::from_str::<Value>(&layer.text) else {
return Vec::new();
};
let layer = layer.name.clone();
let mut found = Vec::new();
if json
.get("apiKeyHelper")
.and_then(Value::as_str)
.is_some_and(|h| !h.trim().is_empty())
{
found.push(Override {
layer: layer.clone(),
key: "apiKeyHelper".into(),
});
}
if let Some(env) = json.get("env").and_then(Value::as_object) {
for name in OVERRIDING_ENV {
let set = match env.get(name) {
Some(Value::String(v)) => is_set(v),
Some(Value::Bool(b)) => *b,
Some(Value::Number(n)) => n.as_i64() != Some(0),
_ => false,
};
if set {
found.push(Override {
layer: layer.clone(),
key: format!("env.{name}"),
});
}
}
}
found
}
pub(crate) fn custom_oauth(ctx: &Context) -> bool {
if ctx.custom_oauth() {
return true;
}
managed(ctx)
.into_iter()
.chain(own(&claude::config_dir(ctx).join("settings.json")))
.any(|layer| {
serde_json::from_str::<Value>(&layer.text)
.ok()
.and_then(|json| {
json.get("env")?
.get("CLAUDE_CODE_CUSTOM_OAUTH_URL")?
.as_str()
.map(is_set)
})
.unwrap_or(false)
})
}
pub(crate) fn overrides(ctx: &Context) -> Vec<Override> {
let mut found: Vec<Override> = managed(ctx)
.into_iter()
.chain(own(&claude::config_dir(ctx).join("settings.json")))
.flat_map(|layer| overrides_in(&layer))
.collect();
found.extend(ctx.overriding_auth().iter().map(|key| Override {
layer: "the environment".into(),
key: key.clone(),
}));
found
}
pub(crate) struct EnvSetting {
pub(crate) layer: String,
pub(crate) value: Value,
pub(crate) managed: bool,
}
pub(crate) fn env_settings(ctx: &Context, name: &str) -> Result<Vec<EnvSetting>, String> {
let managed = managed_taken(ctx)?.into_iter().map(|layer| (layer, true));
let own = own(&claude::config_dir(ctx).join("settings.json")).map(|layer| (layer, false));
Ok(managed
.chain(own)
.filter_map(|(layer, managed)| {
let json = serde_json::from_str::<Value>(&layer.text).ok()?;
let value = json
.get("env")?
.as_object()?
.iter()
.find(|(key, _)| key.eq_ignore_ascii_case(name))?
.1
.clone();
Some(EnvSetting {
layer: layer.name,
value,
managed,
})
})
.collect())
}
pub(crate) fn status_line_command(ctx: &Context) -> Option<String> {
managed(ctx)
.into_iter()
.chain(own(&claude::config_dir(ctx).join("settings.json")))
.find_map(|layer| {
serde_json::from_str::<Value>(&layer.text)
.ok()?
.get("statusLine")?
.get("command")?
.as_str()
.map(str::to_owned)
})
}
pub(crate) fn overrides_of_a_run_in(ctx: &Context, config: &std::path::Path) -> Vec<Override> {
managed(ctx)
.into_iter()
.chain(own(&config.join("settings.json")))
.flat_map(|layer| overrides_in(&layer))
.collect()
}
#[cfg(test)]
#[allow(
clippy::disallowed_methods,
reason = "a test sets up its scratch files as a person or a tool would"
)]
pub(crate) mod tests_support {
use super::*;
pub(crate) fn home() -> (Context, PathBuf, tempfile::TempDir) {
let scratch = tempfile::tempdir().expect("a scratch home");
let config = scratch.path().join(".claude");
std::fs::create_dir_all(&config).expect("a config dir");
let ctx = Context::new(scratch.path().to_path_buf())
.with_claude_config_dir(config.to_string_lossy().into_owned());
(ctx, config, scratch)
}
pub(crate) fn write(path: &std::path::Path, value: Value) {
std::fs::write(path, value.to_string()).expect("written");
}
}
#[cfg(test)]
#[allow(
clippy::disallowed_methods,
reason = "a test sets up its scratch files as a person or a tool would"
)]
mod tests {
use super::tests_support::*;
use super::*;
use serde_json::json;
#[test]
fn the_status_line_command_is_read_from_the_persons_settings() {
let (ctx, config, _scratch) = home();
assert_eq!(status_line_command(&ctx), None);
write(
&config.join("settings.json"),
json!({"statusLine": {"type": "command", "command": "pitboard statusline"}}),
);
assert_eq!(
status_line_command(&ctx).as_deref(),
Some("pitboard statusline")
);
write(&config.join("settings.json"), json!({"statusLine": {}}));
assert_eq!(status_line_command(&ctx), None);
}
#[test]
fn an_ordinary_machine_has_nothing_in_the_way() {
let (ctx, config, _s) = home();
write(&config.join("settings.json"), json!({"theme": "dark"}));
assert_eq!(overrides(&ctx), Vec::new());
}
#[test]
fn a_settings_file_that_sets_a_key_is_found_where_the_environment_would_not_be() {
let (ctx, config, _s) = home();
write(
&config.join("settings.json"),
json!({"env": {"ANTHROPIC_API_KEY": "sk-ant-something"}}),
);
let found = overrides(&ctx);
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found[0].key, "env.ANTHROPIC_API_KEY");
assert!(found[0].layer.ends_with("settings.json"));
}
#[test]
fn a_helper_that_prints_a_key_is_an_override_whatever_it_prints() {
let (ctx, config, _s) = home();
write(
&config.join("settings.json"),
json!({"apiKeyHelper": "/usr/local/bin/get-key"}),
);
assert_eq!(overrides(&ctx)[0].key, "apiKeyHelper");
}
#[test]
fn a_third_party_provider_is_an_override_too() {
let (ctx, config, _s) = home();
write(
&config.join("settings.json"),
json!({"env": {"CLAUDE_CODE_USE_BEDROCK": "1"}}),
);
assert_eq!(overrides(&ctx)[0].key, "env.CLAUDE_CODE_USE_BEDROCK");
}
#[test]
fn a_key_that_is_present_but_empty_sets_nothing() {
let (ctx, config, _s) = home();
write(
&config.join("settings.json"),
json!({
"apiKeyHelper": " ",
"env": {
"ANTHROPIC_API_KEY": "",
"CLAUDE_CODE_USE_BEDROCK": "0",
"CLAUDE_CODE_USE_VERTEX": "false",
},
}),
);
assert_eq!(overrides(&ctx), Vec::new());
}
#[test]
fn a_settings_file_that_is_not_json_is_not_a_reason_to_refuse_anything() {
let (ctx, config, _s) = home();
std::fs::write(config.join("settings.json"), "{ not json").expect("written");
assert_eq!(overrides(&ctx), Vec::new());
}
}
#[cfg(test)]
mod custom_oauth_tests {
use super::tests_support::*;
use super::*;
use serde_json::json;
#[test]
fn a_custom_endpoint_in_a_settings_file_is_found_the_way_the_environment_one_is() {
let (ctx, config, _s) = home();
assert!(!custom_oauth(&ctx));
write(
&config.join("settings.json"),
json!({"env": {"CLAUDE_CODE_CUSTOM_OAUTH_URL": "https://oauth.example"}}),
);
assert!(custom_oauth(&ctx));
}
}
#[cfg(test)]
mod managed_tests {
use super::*;
use crate::context::Environment;
use crate::host::memory::MemoryHost;
use serde_json::json;
use std::sync::Arc;
fn on(os: Os, memory: &Arc<MemoryHost>) -> (Context, tempfile::TempDir) {
let scratch = tempfile::tempdir().expect("a scratch home");
let home = scratch.path().to_string_lossy().into_owned();
let config = scratch
.path()
.join(".claude")
.to_string_lossy()
.into_owned();
let env = Environment::on(
os,
[
("HOME", home.as_str()),
("USERPROFILE", home.as_str()),
("CLAUDE_CONFIG_DIR", config.as_str()),
],
);
let ctx = Context::for_command_line(&env).with_memory_stores(Arc::clone(memory));
(ctx, scratch)
}
fn said(found: &[Override]) -> Vec<(String, String)> {
found
.iter()
.map(|o| (o.layer.clone(), o.key.clone()))
.collect()
}
#[test]
fn managed_settings_are_read_where_claude_code_keeps_them_on_each_system() {
for (os, dir) in [
(Os::MacOs, "/Library/Application Support/ClaudeCode"),
(Os::Linux, "/etc/claude-code"),
(Os::Windows, r"C:\Program Files\ClaudeCode"),
] {
let memory = MemoryHost::new();
let dir = PathBuf::from(dir);
let (file, drop_ins) = (
dir.join("managed-settings.json"),
dir.join("managed-settings.d"),
);
memory.administers(
&file,
&json!({"env": {"ANTHROPIC_API_KEY": "sk-ant-x"}}).to_string(),
);
memory.administers(
drop_ins.join("20-helper.json"),
&json!({"apiKeyHelper": "/opt/key"}).to_string(),
);
memory.administers(
drop_ins.join("10-oauth.json"),
&json!({"env": {"CLAUDE_CODE_CUSTOM_OAUTH_URL": "https://oauth.example"}})
.to_string(),
);
memory.administers(
drop_ins.join("30-notes.txt"),
&json!({"apiKeyHelper": "/opt/other"}).to_string(),
);
let (ctx, scratch) = on(os, &memory);
let expected = [
(
file.display().to_string(),
"env.ANTHROPIC_API_KEY".to_string(),
),
(
drop_ins.join("20-helper.json").display().to_string(),
"apiKeyHelper".to_string(),
),
];
assert_eq!(said(&overrides(&ctx)), expected, "{os:?}");
assert!(custom_oauth(&ctx), "{os:?}");
let run = scratch.path().join("warm").join("config");
assert_eq!(said(&overrides_of_a_run_in(&ctx, &run)), expected, "{os:?}");
}
}
#[test]
fn windows_policies_are_read_as_claude_code_reads_them() {
let memory = MemoryHost::new();
let user = format!(r"HKCU\{POLICIES}\{POLICY}");
let machine = format!(r"HKLM\{POLICIES}\{POLICY}");
memory.sets_policy(
Hive::User,
POLICIES,
POLICY,
&json!({"env": {"CLAUDE_CODE_USE_BEDROCK": "1"}}).to_string(),
);
let (ctx, _scratch) = on(Os::Windows, &memory);
assert_eq!(
said(&overrides(&ctx)),
[(user.clone(), "env.CLAUDE_CODE_USE_BEDROCK".to_string())]
);
memory.sets_policy(
Hive::Machine,
POLICIES,
POLICY,
&json!({"managedSourcesBehavior": "merge"}).to_string(),
);
assert_eq!(
said(&overrides(&ctx)),
[(user, "env.CLAUDE_CODE_USE_BEDROCK".to_string())],
"one that says only how the sources compose keeps nothing out"
);
memory.sets_policy(
Hive::Machine,
POLICIES,
POLICY,
&json!({"apiKeyHelper": "C:\\key.cmd"}).to_string(),
);
assert_eq!(
said(&overrides(&ctx)),
[(machine, "apiKeyHelper".to_string())]
);
let (mac, _scratch) = on(Os::MacOs, &memory);
assert_eq!(overrides(&mac), Vec::new(), "macOS has no registry");
}
#[test]
fn an_unreadable_machine_policy_still_lets_hkcu_be_read() {
let memory = MemoryHost::new();
let user = format!(r"HKCU\{POLICIES}\{POLICY}");
memory.policy_unreadable(Hive::Machine, POLICIES, POLICY, "access denied");
memory.sets_policy(
Hive::User,
POLICIES,
POLICY,
&json!({"apiKeyHelper": "C:\\key.cmd"}).to_string(),
);
let (ctx, _scratch) = on(Os::Windows, &memory);
assert_eq!(said(&overrides(&ctx)), [(user, "apiKeyHelper".to_string())]);
}
#[test]
fn a_managed_file_keeps_the_persons_policy_out() {
let memory = MemoryHost::new();
memory.sets_policy(
Hive::User,
POLICIES,
POLICY,
&json!({"apiKeyHelper": "C:\\key.cmd"}).to_string(),
);
memory.administers(
PathBuf::from(r"C:\Program Files\ClaudeCode").join("managed-settings.json"),
&json!({"permissions": {"deny": ["WebFetch"]}}).to_string(),
);
let (ctx, _scratch) = on(Os::Windows, &memory);
assert_eq!(overrides(&ctx), Vec::new());
}
}