package main
// What a Claude Code user's existing configuration contributes to a run: the
// permission rules in its settings files, and its CLAUDE.md files. Its MCP
// servers are not among them; Codey starts only the ones in its own config. The reading and the rules about what is honoured live in
// internal/ccsettings; this file is the wiring — when to ask about trust, how,
// and what to say when the answer is no.
import (
"fmt"
"io"
"strings"
"sync"
"github.com/johannSo/codey/internal/approval"
"github.com/johannSo/codey/internal/ccsettings"
)
// trustFlag is spelled once, because three messages name it.
const trustFlag = "-trust-project"
// ccOptions is what loading Claude Code's configuration needs from main.
type ccOptions struct {
Home, Root string
// Trust is -trust-project: use the project's allow rules this run,
// whatever was decided before, without asking.
Trust bool
// Ask puts the trust question to a person and reports the answer, and
// whether there was one. Nil where nobody can answer: -p, agent stdio,
// a piped stdin.
Ask func(root string, lines []string) (trusted, answered bool)
Store ccsettings.TrustStore
// Notice reports something to the user, wherever this run's notices go.
Notice func(format string, args ...any)
}
// ccSetup is what came of it.
type ccSetup struct {
// Rules is nil when no settings file had any rules, so the policy is left
// exactly as it would be on a machine without Claude Code.
Rules approval.RuleSet
// Trusted is the trust decision for the root, for reuse by rulesFor.
Trusted bool
}
// loadClaudeCode reads Claude Code's files for the startup root, settles
// whether the project's own files are trusted — asking, if there is someone
// to ask and nothing has been decided about these exact rules — and returns
// what this run should use.
func loadClaudeCode(o ccOptions) ccSetup {
perms, errs := ccsettings.LoadPermissions(o.Home, o.Root)
for _, err := range errs {
o.Notice("%v — its deny rules are not in force", err)
}
trusted := settleTrust(o, ccsettings.NewGrant(perms))
out := ccSetup{Trusted: trusted}
rules, notes := ccsettings.NewRuleset(o.Home, o.Root, perms, trusted)
for _, n := range notes {
o.Notice("%s", n)
}
if !rules.Empty() {
out.Rules = rules
}
return out
}
// settleTrust decides whether what the project's committed files grant is
// used this run. The flag wins; then a decision already recorded for these
// exact definitions; then the person at the terminal, if there is one. With
// nobody to ask, the answer is no, and the notice says how to say yes.
func settleTrust(o ccOptions, grant ccsettings.Grant) bool {
if grant.Empty() {
return false
}
if o.Trust {
return true
}
fp := grant.Fingerprint()
decided, trusted, err := o.Store.Lookup(o.Root, fp)
if err != nil {
o.Notice("%v — treating this project as not trusted", err)
return false
}
if decided {
if !trusted {
o.Notice("%s — you declined to trust them for this checkout. %s uses them for one run; a change to them asks again", untrustedWhat(grant), trustFlag)
}
return trusted
}
if o.Ask == nil {
o.Notice("%s — they come from the repository, and nobody has said this checkout is trusted. Run once interactively to be asked, or pass %s", untrustedWhat(grant), trustFlag)
return false
}
trusted, answered := o.Ask(o.Root, grant.Describe())
if !answered {
// Interrupted, or stdin closed: no for now, and the question stays
// open for the next start.
return false
}
if err := o.Store.Record(o.Root, fp, trusted); err != nil {
o.Notice("%v — the answer holds for this run only", err)
}
if !trusted {
o.Notice("%s — you can change your mind with %s", untrustedWhat(grant), trustFlag)
}
return trusted
}
// untrustedWhat names what is being left out, for a notice.
func untrustedWhat(g ccsettings.Grant) string {
return fmt.Sprintf("the allow rules in %s are ignored", strings.Join(g.AllowFrom, " and "))
}
// askTrustOnTerminal is the trust question, asked on the terminal before the
// TUI takes it over — the same moment Claude Code asks its own. It goes to
// stderr and reads stdin a byte at a time, so nothing typed after the answer
// is swallowed into a buffer the TUI never sees.
func askTrustOnTerminal(in io.Reader, out io.Writer) func(string, []string) (bool, bool) {
return func(root string, lines []string) (bool, bool) {
fmt.Fprintf(out, "\nClaude Code's configuration for %s would:\n\n", root)
for _, l := range lines {
fmt.Fprintln(out, " "+l)
}
fmt.Fprint(out, "\nThese files come from the repository, not from you: trusting them lets it\n"+
"choose commands this machine runs. Your answer is remembered until they change.\n"+
"Trust them for this checkout? [y/N] ")
line, err := readLine(in)
if err != nil {
fmt.Fprintln(out)
return false, false
}
switch strings.ToLower(strings.TrimSpace(line)) {
case "y", "yes":
return true, true
default:
return false, true
}
}
}
// readLine reads up to a newline without reading past it.
func readLine(r io.Reader) (string, error) {
var b strings.Builder
buf := make([]byte, 1)
for {
n, err := r.Read(buf)
if n == 1 {
if buf[0] == '\n' {
return b.String(), nil
}
b.WriteByte(buf[0])
}
if err != nil {
if err == io.EOF && b.Len() > 0 {
return b.String(), nil
}
return "", err
}
}
}
// rulesFor builds the rule set for a root other than the one Codey started
// in — an ACP client hands each thread a directory of its own. Nobody can be
// asked from there, so the project's allow rules count only if trust was
// already recorded for them, or -trust-project was given. Each root is read
// once; a model switch rebuilds the agent, and the same notices twice would
// be noise.
func rulesFor(o ccOptions, startRoot string, start ccSetup) func(root string) approval.RuleSet {
var mu sync.Mutex
cache := map[string]approval.RuleSet{startRoot: start.Rules}
return func(root string) approval.RuleSet {
mu.Lock()
defer mu.Unlock()
if rs, ok := cache[root]; ok {
return rs
}
ro := o
ro.Root, ro.Ask = root, nil
perms, errs := ccsettings.LoadPermissions(o.Home, root)
for _, err := range errs {
o.Notice("%v — its deny rules are not in force", err)
}
trusted := settleTrust(ro, ccsettings.NewGrant(perms))
rules, notes := ccsettings.NewRuleset(o.Home, root, perms, trusted)
for _, n := range notes {
o.Notice("%s", n)
}
var rs approval.RuleSet
if !rules.Empty() {
rs = rules
}
cache[root] = rs
return rs
}
}
// instructionsFor reads the CLAUDE.md files for a root, formatted for the
// system prompt, reporting what could not be read.
func instructionsFor(home string, notice func(string, ...any)) func(root string) string {
var mu sync.Mutex
cache := map[string]string{}
return func(root string) string {
mu.Lock()
defer mu.Unlock()
if s, ok := cache[root]; ok {
return s
}
files, errs := ccsettings.LoadInstructions(home, root)
for _, err := range errs {
notice("%v", err)
}
s := ccsettings.FormatInstructions(files)
cache[root] = s
return s
}
}