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