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() }