// Package ccsettings reads the configuration Claude Code keeps for a user and // for a project, so that someone who already set Claude Code up does not have // to set Codey up a second time: the permission rules in its settings files, // and the CLAUDE.md files it loads into every conversation. Its MCP servers // are not read: Codey starts only the servers in its own config. // // Reading is the easy half. The half that needs care is what is honoured, and // it turns on who wrote each file. ~/.claude/settings.json is the user's own, // written by the user or by `claude` on their behalf, and it carries the same // weight as Codey's own config. A project's .claude/settings.json is committed // to the repository, which means it was written by whoever wrote the // repository: an allow rule there is a stranger deciding what runs without // asking. Those rules are honoured only once the user has said, for this // checkout and these exact rules, that they trust them — the same bargain // Claude Code's own trust dialog strikes. Anything that can only take power // away — a deny rule, an ask rule — is honoured from every file, because // nobody is harmed by an agent that asks more often. // // Nothing here writes to Claude Code's files. The one file this package // writes is Codey's own record of those trust decisions. package ccsettings import ( "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "strings" ) // Scope is who a setting belongs to, which is what decides whether it is // honoured without asking. type Scope string const ( // ScopeUser is the user's own, for every project: ~/.claude/settings.json. ScopeUser Scope = "user" // ScopeProject is committed to the repository: .claude/settings.json at // the working root. Written by whoever wrote the repository, so what it // grants waits for trust. ScopeProject Scope = "project" // ScopeLocal is the user's own, for this project only: // .claude/settings.local.json, which Claude Code keeps out of git. ScopeLocal Scope = "local" ) // File names, relative to the home directory or the working root. const ( UserSettings = ".claude/settings.json" ProjectSettings = ".claude/settings.json" LocalSettings = ".claude/settings.local.json" UserInstructions = ".claude/CLAUDE.md" instructionsName = "CLAUDE.md" localInstructions = "CLAUDE.local.md" ) // readJSON decodes a file into v. A missing file is (false, nil): every file // here is optional, and most machines have none of them. func readJSON(path string, v any) (bool, error) { raw, err := os.ReadFile(path) if errors.Is(err, fs.ErrNotExist) { return false, nil } if err != nil { return false, err } if err := json.Unmarshal(raw, v); err != nil { return false, fmt.Errorf("%s is not valid JSON: %w", path, err) } return true, nil } // shown is how a path is written in a message: under ~ when it is in the home // directory, relative when it is in the working root, absolute otherwise. A // prompt that says ".claude/settings.json" is one a person reads; one that // says /home/someone/src/thing/.claude/settings.json is one they skim. func shown(home, root, path string) string { if root != "" { if rel, err := filepath.Rel(root, path); err == nil && !strings.HasPrefix(rel, "..") { return filepath.ToSlash(rel) } } if home != "" { if rel, err := filepath.Rel(home, path); err == nil && !strings.HasPrefix(rel, "..") { return "~/" + filepath.ToSlash(rel) } } return path }