package main // The person's own items in the operator account's agent directory — the ones the mesh did not place (novox/hq // ADR 0216 rule 6) — read and removed through the mesh rather than over a shell on the machine. // // Removing one stays the person's act: the remove tool is the act made explicit. It is called on the person's // word, takes their reason, refuses anything the mesh placed (unregister owns those), keeps a copy outside the // agent directory before it deletes, and logs what it removed and why. Nothing here removes on its own. import ( "bytes" "encoding/json" "errors" "fmt" "io/fs" "os" "path/filepath" "sort" "strings" "time" ) // KindMemory is the account's own instruction file, ~/.claude/CLAUDE.md: never placed by the mesh, read by // every session of the account. const KindMemory = "memory" // memoryName is the one name the memory kind has. const memoryName = "CLAUDE" // RemovedDir is where, in the module's state, a removed item is kept: one dated folder per day, one folder // per removal inside it. const RemovedDir = "removed-from-home" // homeKinds are the kinds the home tools take: those the status tool lists, and the memory. var homeKinds = map[string]string{KindSkill: "skills", KindAgent: "agents", KindCommand: "commands", KindOutputStyle: "output-styles", KindInstructions: "rules", KindMemory: ""} func (p Paths) removedLog() string { return filepath.Join(p.State, RemovedDir, "removed.log") } // homeKindOf reads a kind as a person may write it: output_style for output-style, rule for instructions. func homeKindOf(kind string) string { switch k := strings.ToLower(strings.TrimSpace(kind)); k { case "output_style": return KindOutputStyle case "rule", "rules": return KindInstructions case "claude.md": return KindMemory default: return k } } // HomeItemPath is where one item of the home is, relative to the agent directory: a skill is its folder, // every other kind one file. func HomeItemPath(kind, name string) (rel string, folder bool, err error) { kind = homeKindOf(kind) sub, ok := homeKinds[kind] if !ok { return "", false, fmt.Errorf("kind is skill, agent, command, output-style, instructions (a rule file) or memory (~/.claude/CLAUDE.md), not %q", kind) } if kind == KindMemory { if name != "" && name != memoryName && name != memoryName+".md" { return "", false, fmt.Errorf("the memory is one file, CLAUDE.md; its name is %s or absent", memoryName) } return "CLAUDE.md", false, nil } name = strings.TrimSuffix(name, ".md") if name == "" || name == "." || name == ".." || strings.HasPrefix(name, ".") || strings.ContainsAny(name, `/\`) || strings.ContainsRune(name, 0) { return "", false, fmt.Errorf("%q is not an item's name: its folder, or its file without .md, as the status tool lists it", name) } if kind == KindSkill { return sub + "/" + name, true, nil } return sub + "/" + name + ".md", false, nil } // placedUnder says whether the mesh placed the path, or anything inside it, and still holds it as its own: a // path it placed that the person deleted and then made again is theirs (PlaceHome leaves it alone). func placedUnder(p Paths, rel string) string { var placed Placed _ = readJSON(p.placed(), &placed) for at, ours := range placed { if ours == deletedByHand { continue } if at == rel || strings.HasPrefix(at, rel+"/") { return at } } return "" } // readItem reads one item's files by path inside it, refusing a symbolic link anywhere on the way or in it: // what it points at is not the home's. func readItem(dir, rel string, folder bool) (map[string]string, map[string]fs.FileMode, error) { if why := linkedParent(dir, rel+"/x"); why != "" { return nil, nil, errors.New(why) } full := filepath.Join(dir, filepath.FromSlash(rel)) info, err := os.Lstat(full) if err != nil { if os.IsNotExist(err) { return nil, nil, fmt.Errorf("%s is not in this home", full) } return nil, nil, err } if info.Mode()&os.ModeSymlink != 0 { return nil, nil, fmt.Errorf("%s is a symbolic link", full) } files, modes := map[string]string{}, map[string]fs.FileMode{} if !folder { if !info.Mode().IsRegular() { return nil, nil, fmt.Errorf("%s is not a file", full) } raw, err := os.ReadFile(full) if err != nil { return nil, nil, err } files[filepath.Base(full)], modes[filepath.Base(full)] = string(raw), info.Mode().Perm() return files, modes, nil } if !info.IsDir() { return nil, nil, fmt.Errorf("%s is not a folder", full) } err = filepath.WalkDir(full, func(at string, d fs.DirEntry, err error) error { if err != nil { return err } if d.Type()&fs.ModeSymlink != 0 { return fmt.Errorf("%s is a symbolic link", at) } if d.IsDir() { return nil } if !d.Type().IsRegular() { return fmt.Errorf("%s is not a file", at) } in, _ := filepath.Rel(full, at) raw, err := os.ReadFile(at) if err != nil { return err } fi, err := d.Info() if err != nil { return err } files[filepath.ToSlash(in)], modes[filepath.ToSlash(in)] = string(raw), fi.Mode().Perm() return nil }) if err != nil { return nil, nil, err } return files, modes, nil } // ShowHome answers one item of the home in full: where it is, whether the mesh placed it, and its files. func ShowHome(p Paths, kind, name string) (map[string]any, error) { rel, folder, err := HomeItemPath(kind, name) if err != nil { return nil, err } dir := filepath.Join(p.Home, ".claude") files, _, err := readItem(dir, rel, folder) if err != nil { return nil, err } answer := map[string]any{"kind": homeKindOf(kind), "path": filepath.Join(dir, filepath.FromSlash(rel)), "placedByTheMesh": placedUnder(p, rel) != "", "files": files} if !folder { for _, content := range files { answer["content"] = content } } return answer, nil } // Removal is what the removed-items log keeps of one removal, and the copy's own note. type Removal struct { Action string `json:"action"` At string `json:"at"` Node string `json:"node"` Kind string `json:"kind"` Name string `json:"name"` Path string `json:"path"` Why string `json:"why,omitempty"` Kept string `json:"keptAt"` Files map[string]KeptFile `json:"files"` } // KeptFile is one file of a kept copy as it was in the home: its digest, checked before it is put back, and // its mode. type KeptFile struct { Digest string `json:"sha256"` Mode string `json:"mode"` } // RemoveHome removes one item the person made in the home, on their word and for the reason given: it keeps // a copy in the module's state first, under a dated folder, checks the copy, then deletes and logs. An item // the mesh placed is refused — unregistering owns it. Answers where the copy is, so it can be put back. func RemoveHome(p Paths, kind, name, why string, now time.Time) (map[string]any, error) { why = strings.TrimSpace(why) if why == "" { return nil, errors.New("why is required: the person's reason for removing it, kept in the log") } rel, folder, err := HomeItemPath(kind, name) if err != nil { return nil, err } kind = homeKindOf(kind) if kind == KindMemory { name = memoryName } else { name = strings.TrimSuffix(name, ".md") } if at := placedUnder(p, rel); at != "" { return nil, fmt.Errorf("the mesh placed %s: remove it with claude_code_%s_unregister at the home scope", at, strings.ReplaceAll(kind, "-", "_")) } dir := filepath.Join(p.Home, ".claude") files, modes, err := readItem(dir, rel, folder) if err != nil { return nil, err } full := filepath.Join(dir, filepath.FromSlash(rel)) // The copy: /removed-from-home//