Vectrum
Sign In
247 lines · 7.8 KB · Go
package ccsettings

import (
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strings"
)

// Instruction is one CLAUDE.md-style file, read.
type Instruction struct {
	Path string
	// Shown names the file in the prompt, and Role says whose it is.
	Shown string
	Role  string
	Body  string
}

// Roles, as the prompt describes them. Saying whose a file is matters to the
// model as much as to a person: the user's own preferences and a repository's
// conventions are different kinds of instruction.
const (
	roleUser     = "the user's own instructions, for every project"
	roleProject  = "this project's instructions, checked in"
	roleLocal    = "the user's private instructions for this project"
	importMaxHop = 5
	// maxInstructionBytes bounds one file. These go into every request, and
	// a generated file or a stray log someone named CLAUDE.md should not be
	// able to fill the window.
	maxInstructionBytes = 64 << 10
)

// LoadInstructions reads the files Claude Code loads into a conversation
// started in root, in the order it loads them, least specific first:
//
//   - ~/.claude/CLAUDE.md, the user's own;
//   - then for each directory from the top of the project down to root —
//     the top being the nearest directory with a .git in it, or the home
//     directory if there is none on the way — CLAUDE.md, .claude/CLAUDE.md
//     and CLAUDE.local.md.
//
// Each file's @path imports follow it, up to five hops deep. An import is
// relative to the file it appears in, and may not leave the tree its file
// belongs to: a project's files may import from the project, the user's from
// the home directory. A repository's CLAUDE.md that could import
// @~/.aws/credentials would be a repository reading your credentials into
// the conversation, which is then sent to the model.
//
// Claude Code also loads a subdirectory's CLAUDE.md when it first reads a
// file there, and .claude/rules/; neither is done here.
func LoadInstructions(home, root string) ([]Instruction, []error) {
	l := &instructionLoader{home: home, root: root, seen: map[string]bool{}}
	if home != "" {
		l.load(filepath.Join(home, UserInstructions), roleUser, home, "", 0)
	}
	if root == "" {
		return l.out, l.errs
	}
	dirs := projectDirs(home, root)
	top := dirs[0]
	for _, dir := range dirs {
		for _, name := range []string{instructionsName, filepath.Join(".claude", instructionsName), localInstructions} {
			path := filepath.Join(dir, name)
			role, bound := roleProject, top
			if name == localInstructions {
				role = roleLocal
				// A CLAUDE.local.md is the user's, and may reach into their
				// home directory — Claude Code's own docs suggest importing a
				// personal file from there — unless git is tracking it, which
				// makes it the repository's after all.
				if _, err := os.Lstat(path); err != nil {
					continue
				}
				if home != "" && !gitTracks(dir, localInstructions) {
					bound = home
				}
			}
			l.load(path, role, bound, "", 0)
		}
	}
	return l.out, l.errs
}

// projectDirs is root and the directories above it that belong to the same
// project, outermost first. The walk stops at a directory holding .git, or at
// the home directory. A root that is neither in a repository nor under home
// is a project of one directory: walking up to / would read whatever CLAUDE.md
// happened to be lying in /tmp.
func projectDirs(home, root string) []string {
	root = filepath.Clean(root)
	var dirs []string
	for d := root; ; {
		dirs = append(dirs, d)
		if _, err := os.Lstat(filepath.Join(d, ".git")); err == nil {
			break
		}
		if home != "" && d == filepath.Clean(home) {
			break
		}
		parent := filepath.Dir(d)
		if parent == d {
			return []string{root}
		}
		d = parent
	}
	for i, j := 0, len(dirs)-1; i < j; i, j = i+1, j-1 {
		dirs[i], dirs[j] = dirs[j], dirs[i]
	}
	return dirs
}

type instructionLoader struct {
	home string
	root string
	seen map[string]bool
	out  []Instruction
	errs []error
}

// load reads one file and then its imports. bound is the directory the file
// and everything it imports must stay inside; importer is the file that
// imported this one, empty for a file found by name.
func (l *instructionLoader) load(path, role, bound, importer string, hop int) {
	path = filepath.Clean(path)
	real, err := filepath.EvalSymlinks(path)
	if err != nil {
		return // not there, which is the usual case
	}
	if l.seen[real] {
		return
	}
	// The user's own file is wherever they keep it — often a symlink into a
	// dotfiles repository. Everything else, including what it imports, stays
	// inside its bound, and that is checked on the resolved path: a symlink
	// committed to a repository can point anywhere.
	ownFile := importer == "" && role == roleUser
	if !ownFile && !within(bound, real) {
		if importer != "" {
			where := "the project"
			if bound == l.home {
				where = "the home directory"
			}
			l.errs = append(l.errs, fmt.Errorf("CLAUDE.md: %s imports %s, which is outside %s — skipped", l.shown(importer), l.shown(path), where))
		}
		return
	}
	info, err := os.Stat(real)
	if err != nil || !info.Mode().IsRegular() {
		return
	}
	raw, err := os.ReadFile(real)
	if err != nil {
		l.errs = append(l.errs, fmt.Errorf("CLAUDE.md: %w", err))
		return
	}
	l.seen[real] = true
	body := string(raw)
	if len(body) > maxInstructionBytes {
		l.errs = append(l.errs, fmt.Errorf("CLAUDE.md: %s is over %d KB — only the start of it is used", l.shown(path), maxInstructionBytes>>10))
		body = body[:maxInstructionBytes]
	}
	in := Instruction{Path: path, Shown: l.shown(path), Role: role, Body: strings.TrimSpace(body)}
	if importer != "" {
		in.Role = "imported by " + l.shown(importer)
	}
	if in.Body != "" {
		l.out = append(l.out, in)
	}
	if hop >= importMaxHop {
		return
	}
	for _, ref := range imports(body) {
		target := ref
		switch {
		case strings.HasPrefix(ref, "~/"):
			if l.home == "" {
				continue
			}
			target = filepath.Join(l.home, ref[2:])
		case !filepath.IsAbs(ref):
			target = filepath.Join(filepath.Dir(path), ref)
		}
		l.load(target, role, bound, path, hop+1)
	}
}

func (l *instructionLoader) shown(path string) string {
	return shown(l.home, l.root, path)
}

func within(dir, path string) bool {
	if real, err := filepath.EvalSymlinks(dir); err == nil {
		dir = real
	}
	rel, err := filepath.Rel(dir, path)
	return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) && !filepath.IsAbs(rel)
}

var (
	importRef  = regexp.MustCompile("(?:^|[\\s(])@([^\\s`]+)")
	inlineCode = regexp.MustCompile("`[^`]*`")
)

// imports finds the @path references in a file, skipping code: an @ in a
// code block or span is an example, or a decorator, not an import.
func imports(body string) []string {
	var out []string
	fenced := false
	for _, line := range strings.Split(body, "\n") {
		if strings.HasPrefix(strings.TrimSpace(line), "```") {
			fenced = !fenced
			continue
		}
		if fenced {
			continue
		}
		line = inlineCode.ReplaceAllString(line, "")
		for _, m := range importRef.FindAllStringSubmatch(line, -1) {
			ref := strings.TrimRight(m[1], ".,;:!?)]'\"")
			if ref != "" {
				out = append(out, ref)
			}
		}
	}
	return out
}

// FormatInstructions is the system prompt section the files make. It is built
// once, at startup, and so is constant for the session: the cached prefix
// still matches request to request.
func FormatInstructions(files []Instruction) string {
	if len(files) == 0 {
		return ""
	}
	var b strings.Builder
	b.WriteString("\n\n# Instructions from CLAUDE.md\n\n" +
		"These are the instruction files Claude Code would load for this repository,\n" +
		"read at startup and given here in full, so there is no need to read them\n" +
		"again. Follow them: on anything about this codebase they outrank the\n" +
		"defaults above. Later files are more specific to where you are working, and\n" +
		"win where two disagree.")
	for _, f := range files {
		fmt.Fprintf(&b, "\n\n## %s (%s)\n\n%s", f.Shown, f.Role, f.Body)
	}
	return b.String()
}