pub mod error;
pub mod guard;
pub mod io;
pub mod merge;
pub mod paths;
pub mod permission;
pub mod types;
pub use error::{Result, SettingsError};
pub use guard::{MultiLevelGuard, SettingsGuard};
pub use io::SettingsIO;
pub use merge::{FigmentLoader, Merge, SettingsMerger};
pub use paths::PathResolver;
pub use permission::{Permission, PermissionPattern, PermissionRule, PermissionSet};
pub use types::{
Attribution, Hook, HookConfig, HookMatcher, Hooks, Permissions, Sandbox, Settings,
SettingsLevel,
};
use tracing::{Level, instrument};
#[derive(Debug, Clone)]
pub struct ClaudeSettings {
io: SettingsIO,
}
impl Default for ClaudeSettings {
fn default() -> Self {
Self::new()
}
}
impl ClaudeSettings {
#[instrument(level = Level::TRACE)]
pub fn new() -> Self {
Self {
io: SettingsIO::new(),
}
}
#[instrument(level = Level::TRACE)]
pub fn with_resolver(resolver: PathResolver) -> Self {
Self {
io: SettingsIO::with_resolver(resolver),
}
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn io(&self) -> &SettingsIO {
&self.io
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn resolver(&self) -> &PathResolver {
self.io.resolver()
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn read(&self, level: SettingsLevel) -> Result<Option<Settings>> {
self.io.read_optional(level)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn read_or_default(&self, level: SettingsLevel) -> Result<Settings> {
Ok(self.io.read_optional(level)?.unwrap_or_default())
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn write(&self, level: SettingsLevel, settings: &Settings) -> Result<()> {
self.io.write(level, settings)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn exists(&self, level: SettingsLevel) -> Result<bool> {
self.io.exists(level)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn delete(&self, level: SettingsLevel) -> Result<()> {
self.io.delete(level)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn effective(&self) -> Result<Settings> {
let all = self.io.read_all()?;
Ok(merge::merge_all(&all))
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn list_all(&self) -> Result<Vec<(SettingsLevel, std::path::PathBuf, Settings)>> {
let mut results = Vec::new();
for level in SettingsLevel::all_by_priority() {
let path = self.resolver().settings_path(*level)?;
if let Some(settings) = self.read(*level)? {
results.push((*level, path, settings));
}
}
Ok(results)
}
#[instrument(level = Level::TRACE, skip(self, f))]
pub fn update<F>(&self, level: SettingsLevel, f: F) -> Result<()>
where
F: FnOnce(&mut Settings),
{
let mut settings = self.read_or_default(level)?;
f(&mut settings);
self.write(level, &settings)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn add_allow_permission(&self, level: SettingsLevel, pattern: &str) -> Result<()> {
self.update(level, |settings| {
settings.permissions.insert_allow(pattern);
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn add_deny_permission(&self, level: SettingsLevel, pattern: &str) -> Result<()> {
self.update(level, |settings| {
settings.permissions.insert_deny(pattern);
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn set_model(&self, level: SettingsLevel, model: &str) -> Result<()> {
self.update(level, |settings| {
settings.model = Some(model.to_string());
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn set_env(&self, level: SettingsLevel, key: &str, value: &str) -> Result<()> {
self.update(level, |settings| {
let env = settings.env.get_or_insert_with(Default::default);
env.insert(key.to_string(), value.to_string());
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn remove_env(&self, level: SettingsLevel, key: &str) -> Result<()> {
self.update(level, |settings| {
if let Some(ref mut env) = settings.env {
env.remove(key);
}
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn add_ask_permission(&self, level: SettingsLevel, pattern: &str) -> Result<()> {
self.update(level, |settings| {
settings.permissions.insert_ask(pattern);
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn remove_permission(&self, level: SettingsLevel, pattern: &str) -> Result<()> {
self.update(level, |settings| {
settings.permissions.remove(&pattern.into());
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn effective_permissions(&self) -> Result<PermissionSet> {
let settings = self.effective()?;
Ok(settings.permissions.clone())
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn permissions_at(&self, level: SettingsLevel) -> Result<Option<PermissionSet>> {
match self.read(level)? {
Some(settings) => Ok(Some(settings.permissions.clone())),
None => Ok(None),
}
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn set_permissions(&self, level: SettingsLevel, perms: &PermissionSet) -> Result<()> {
self.update(level, |settings| {
settings.permissions = perms.clone();
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn clear_model(&self, level: SettingsLevel) -> Result<()> {
self.update(level, |settings| {
settings.model = None;
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn clear_permissions(&self, level: SettingsLevel) -> Result<()> {
self.update(level, |settings| {
settings.permissions.clear();
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn clear_env(&self, level: SettingsLevel) -> Result<()> {
self.update(level, |settings| {
settings.env = None;
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn set_language(&self, level: SettingsLevel, language: &str) -> Result<()> {
self.update(level, |settings| {
settings.language = Some(language.to_string());
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn set_bypass_permissions(&self, level: SettingsLevel, enabled: bool) -> Result<()> {
self.update(level, |settings| {
settings.bypass_permissions = Some(enabled);
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn set_default_permission_mode(&self, level: SettingsLevel, mode: &str) -> Result<()> {
self.update(level, |settings| {
settings.permissions.set_default_mode(mode);
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn set_sandbox_enabled(&self, level: SettingsLevel, enabled: bool) -> Result<()> {
self.update(level, |settings| {
let sandbox = settings.sandbox.get_or_insert_with(Sandbox::default);
sandbox.enabled = Some(enabled);
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn set_cleanup_period(&self, level: SettingsLevel, days: u32) -> Result<()> {
self.update(level, |settings| {
settings.cleanup_period_days = Some(days);
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn set_plugin_enabled(
&self,
level: SettingsLevel,
plugin: &str,
enabled: bool,
) -> Result<()> {
self.update(level, |settings| {
let plugins = settings
.enabled_plugins
.get_or_insert_with(Default::default);
plugins.insert(plugin.to_string(), enabled);
})
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn backup(&self, level: SettingsLevel, suffix: &str) -> Result<Option<std::path::PathBuf>> {
self.io.backup(level, suffix)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn write_with_backup(
&self,
level: SettingsLevel,
settings: &Settings,
backup_suffix: &str,
) -> Result<Option<std::path::PathBuf>> {
self.io.write_with_backup(level, settings, backup_suffix)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn restore_from_backup(&self, level: SettingsLevel, suffix: &str) -> Result<bool> {
self.io.restore_from_backup(level, suffix)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn backup_exists(&self, level: SettingsLevel, suffix: &str) -> Result<bool> {
self.io.backup_exists(level, suffix)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn scoped(&self, level: SettingsLevel) -> Result<SettingsGuard<'_>> {
SettingsGuard::new(self, level)
}
#[instrument(level = Level::TRACE, skip(self))]
pub fn scoped_multi(&self, levels: &[SettingsLevel]) -> Result<MultiLevelGuard<'_>> {
MultiLevelGuard::new(self, levels)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn setup_test_manager() -> (TempDir, ClaudeSettings) {
let temp = TempDir::new().unwrap();
let resolver = PathResolver::new()
.with_home(temp.path().join("home"))
.with_project(temp.path().join("project"));
fs::create_dir_all(temp.path().join("home/.claude")).unwrap();
fs::create_dir_all(temp.path().join("project/.claude")).unwrap();
(temp, ClaudeSettings::with_resolver(resolver))
}
#[test]
fn test_read_write_cycle() {
let (_temp, manager) = setup_test_manager();
let settings = Settings::new()
.with_model("test-model")
.with_permissions(PermissionSet::new().allow("Bash(git:*)"));
manager.write(SettingsLevel::User, &settings).unwrap();
let read = manager.read(SettingsLevel::User).unwrap().unwrap();
assert_eq!(read.model.unwrap(), "test-model");
}
#[test]
fn test_effective_settings() {
let (_temp, manager) = setup_test_manager();
let user = Settings::new()
.with_model("user-model")
.with_permissions(PermissionSet::new().allow("Bash(git:*)"));
manager.write(SettingsLevel::User, &user).unwrap();
let project = Settings::new()
.with_model("project-model")
.with_permissions(PermissionSet::new().deny("Read(.env)"));
manager.write(SettingsLevel::Project, &project).unwrap();
let effective = manager.effective().unwrap();
assert_eq!(effective.model.unwrap(), "project-model");
let perms = &effective.permissions;
assert!(perms.is_allowed("Bash", Some("git status")));
assert!(perms.is_denied("Read", Some(".env")));
}
#[test]
fn test_update() {
let (_temp, manager) = setup_test_manager();
manager
.update(SettingsLevel::User, |s| {
s.model = Some("updated-model".to_string());
})
.unwrap();
let read = manager.read(SettingsLevel::User).unwrap().unwrap();
assert_eq!(read.model.unwrap(), "updated-model");
}
#[test]
fn test_add_permissions() {
let (_temp, manager) = setup_test_manager();
manager
.add_allow_permission(SettingsLevel::User, "Bash(git:*)")
.unwrap();
manager
.add_deny_permission(SettingsLevel::User, "Read(.env)")
.unwrap();
let read = manager.read(SettingsLevel::User).unwrap().unwrap();
let perms = &read.permissions;
assert!(perms.is_allowed("Bash", Some("git status")));
assert!(perms.is_denied("Read", Some(".env")));
}
#[test]
fn test_set_and_remove_env() {
let (_temp, manager) = setup_test_manager();
manager
.set_env(SettingsLevel::User, "MY_VAR", "my_value")
.unwrap();
let read = manager.read(SettingsLevel::User).unwrap().unwrap();
assert_eq!(
read.env.as_ref().unwrap().get("MY_VAR").unwrap(),
"my_value"
);
manager.remove_env(SettingsLevel::User, "MY_VAR").unwrap();
let read = manager.read(SettingsLevel::User).unwrap().unwrap();
assert!(!read.env.as_ref().unwrap().contains_key("MY_VAR"));
}
#[test]
fn test_list_all() {
let (_temp, manager) = setup_test_manager();
manager
.write(SettingsLevel::User, &Settings::new().with_model("user"))
.unwrap();
manager
.write(
SettingsLevel::Project,
&Settings::new().with_model("project"),
)
.unwrap();
let all = manager.list_all().unwrap();
assert_eq!(all.len(), 2);
}
#[test]
fn test_set_bypass_permissions() {
let (_temp, manager) = setup_test_manager();
manager
.set_bypass_permissions(SettingsLevel::User, true)
.unwrap();
let read = manager.read(SettingsLevel::User).unwrap().unwrap();
assert_eq!(read.bypass_permissions, Some(true));
}
#[test]
fn test_set_default_permission_mode() {
let (_temp, manager) = setup_test_manager();
manager
.set_default_permission_mode(SettingsLevel::User, "bypassPermissions")
.unwrap();
let read = manager.read(SettingsLevel::User).unwrap().unwrap();
assert_eq!(read.permissions.default_mode(), Some("bypassPermissions"));
}
}