// 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
}