// 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 MCP servers it knows about, the // permission rules in its settings files, and the CLAUDE.md files it loads // into every conversation. // // Reading is the easy half. The half that needs care is what is honoured, and // it turns on who wrote each file. ~/.claude.json and ~/.claude/settings.json // are the user's own, written by the user or by `claude` on their behalf, and // they carry the same weight as Codey's own config. A project's .mcp.json and // .claude/settings.json are committed to the repository, which means they were // written by whoever wrote the repository: starting a server listed there is // running a command a stranger chose, and an allow rule there is a stranger // deciding what runs without asking. Those two are honoured only once the user // has said, for this checkout and these exact definitions, 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, // and the top-level servers in ~/.claude.json. ScopeUser Scope = "user" // ScopeProject is committed to the repository: .mcp.json and // .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, and the // per-project servers in ~/.claude.json, which `claude mcp add` writes. ScopeLocal Scope = "local" ) // File names, relative to the home directory or the working root. const ( ClaudeJSON = ".claude.json" UserSettings = ".claude/settings.json" ProjectSettings = ".claude/settings.json" LocalSettings = ".claude/settings.local.json" ProjectMCP = ".mcp.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 ".mcp.json" is one a person reads; one that says // /home/someone/src/thing/.mcp.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 }