Vectrum
Sign In
91 lines · 3.4 KB · Go
// 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
}