claude-code: read and remove the person's own home items through the mesh
mesh/merge-gate pass: builds claude-code → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer head of the same pull request

Hand-written items in an account's ~/.claude could only be removed over a
shell on the machine. claude_code_home_show reads one in full (memory,
~/.claude/CLAUDE.md, included); claude_code_home_remove removes one on the
person's word, with a required reason, refusing what the mesh placed and
symbolic links, keeping a dated copy in the module's state and logging it.
This commit is contained in:
jochen
2026-10-07 19:39:27 +02:00
parent c74f407abd
commit 03728728c3
6 changed files with 544 additions and 4 deletions
+9 -1
View File
@@ -70,7 +70,15 @@ operator account's own `~/.claude` on those nodes):
- `claude_code_config_list`, `_show` (one registration in full), `_status` (what applies here, the plugin
as written, and the home's own items — which the mesh placed, which share a name with a mesh item, which
call a tool server not loaded here) and `_import` (an item of this node's home, registered at a scope;
the original stays).
the original stays);
- `claude_code_home_show` (`kind`, `name`): one item of this node's home in full — a skill, subagent,
command, output style, rule file (`instructions`), or the account's own memory `~/.claude/CLAUDE.md`
(`memory`) — and whether the mesh placed it;
- `claude_code_home_remove` (`kind`, `name`, `why`): removes one item the person made, on their word —
removing it is the person's act, and this tool is that act made explicit. `why` is required; what the
mesh placed is refused (its `_unregister` owns it), and so is a symbolic link. A copy is kept first in
the module's state, `removed-from-home/<date>/<time>-<kind>-<name>/`, with a `removal.json` note, and
the removal and its reason are appended to `removed-from-home/removed.log`; the answer names the copy.
A new session takes a change; a running one at `/reload-plugins`.
@@ -665,7 +665,7 @@ type HomeItem struct {
var mcpToolRef = regexp.MustCompile(`mcp__([A-Za-z0-9_-]+)__[A-Za-z0-9_-]+`)
// HomeItems lists the home's skills, subagents, commands, output styles and rule files, says which the mesh
// HomeItems lists the home's skills, subagents, commands, output styles, rule files and its own memory (CLAUDE.md), says which the mesh
// placed, and notes those that share a name with a mesh item or call a tool server that is not loaded here.
func HomeItems(p Paths, c Config, loaded Servers) []HomeItem {
dir := filepath.Join(p.Home, ".claude")
@@ -718,6 +718,9 @@ func HomeItems(p Paths, c Config, loaded Servers) []HomeItem {
add(kind, strings.TrimSuffix(e.Name(), ".md"), sub+"/"+e.Name(), string(body))
}
}
if body, err := os.ReadFile(filepath.Join(dir, "CLAUDE.md")); err == nil {
add(KindMemory, memoryName, "CLAUDE.md", string(body))
}
sort.Slice(out, func(i, j int) bool {
if out[i].Kind != out[j].Kind {
return out[i].Kind < out[j].Kind
@@ -11,6 +11,7 @@ import (
"os"
"path/filepath"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
)
@@ -247,7 +248,7 @@ func configTools(p Paths, state ConfigState, view *ConfigView) []stdio.Tool {
Input: map[string]any{"key": str("the key")},
Run: func(a map[string]any) (any, error) { return Show(state, strArg(a, "key")) }},
stdio.Tool{Name: "claude_code_config_status",
Description: "Claude Code's configuration on this node: what was registered and applies here (mesh, node, home), the plugin as written, and the home's own skills, subagents, commands, output styles and rule files — which the mesh placed, which share a name with a mesh item, and which call tools of a tool server not loaded here (stale).",
Description: "Claude Code's configuration on this node: what was registered and applies here (mesh, node, home), the plugin as written, and the home's own skills, subagents, commands, output styles, rule files and memory (CLAUDE.md) — which the mesh placed, which share a name with a mesh item, and which call tools of a tool server not loaded here (stale).",
Run: func(map[string]any) (any, error) {
c := ConfigOf(view.Items())
names := func(items []Item) []string {
@@ -301,6 +302,33 @@ func configTools(p Paths, state ConfigState, view *ConfigView) []stdio.Tool {
}
return Register(p, it, nodes, false, state, view, writeManaged)
}},
stdio.Tool{Name: "claude_code_home_show",
Description: "One item of this node's operator account's own ~/.claude in full, as the status tool lists it — a skill (every file in its folder), subagent, command, output style or rule file — or the account's own memory, ~/.claude/CLAUDE.md: where it is, whether the mesh placed it, and its content.",
Input: map[string]any{
"kind": str("skill, agent, command, output-style, instructions (a rule file) or memory (~/.claude/CLAUDE.md)"),
"name": str("its name in the home: the skill's folder, or the file without .md; absent for memory"),
},
Run: func(a map[string]any) (any, error) { return ShowHome(p, strArg(a, "kind"), strArg(a, "name")) }},
stdio.Tool{Name: "claude_code_home_remove",
Description: "Remove one item the person made in this node's operator account's own ~/.claude — a skill, subagent, command, output style, rule file, or the memory ~/.claude/CLAUDE.md — on the person's word: removing it is the person's act, and this tool is that act made explicit. Called only when the person asked for that item to go, never on the agent's own judgement. `why` is required. An item the mesh placed is refused (its unregister owns it). A copy is kept first in the module's state, under removed-from-home/<date>/, and the removal and its reason are logged there; the answer says where the copy is, so it can be put back.",
Input: map[string]any{
"kind": str("skill, agent, command, output-style, instructions (a rule file) or memory (~/.claude/CLAUDE.md)"),
"name": str("its name in the home: the skill's folder, or the file without .md; absent for memory"),
"why": str("the person's reason for removing it, kept in the log"),
},
Run: func(a map[string]any) (any, error) {
answer, err := RemoveHome(p, strArg(a, "kind"), strArg(a, "name"), strArg(a, "why"), time.Now())
if err != nil {
return nil, err
}
// A home registration of the same name was held back by the person's file; it is placed now.
for _, it := range ConfigOf(view.Items()).Home {
if it.Kind == answer["kind"] && it.Name == answer["name"] {
answer["then"] = "the mesh registers a " + it.Kind + " of this name for this home; it is placed at the next render"
}
}
return answer, nil
}},
)
return out
}
+289
View File
@@ -0,0 +1,289 @@
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"
"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 {
At string `json:"at"`
Node string `json:"node"`
Kind string `json:"kind"`
Name string `json:"name"`
Path string `json:"path"`
Why string `json:"why"`
Kept string `json:"keptAt"`
Files []string `json:"files"`
}
// 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: <state>/removed-from-home/<date>/<time>-<kind>-<name>/<path as it was in the home>.
now = now.UTC()
day := filepath.Join(p.State, RemovedDir, now.Format("2006-01-02"))
kept := filepath.Join(day, now.Format("150405")+"-"+kind+"-"+name)
for n := 2; ; n++ {
if _, err := os.Lstat(kept); os.IsNotExist(err) {
break
}
kept = filepath.Join(day, fmt.Sprintf("%s-%s-%s-%d", now.Format("150405"), kind, name, n))
}
copyRoot := filepath.Join(kept, filepath.FromSlash(rel))
if !folder {
copyRoot = filepath.Dir(copyRoot)
}
names := make([]string, 0, len(files))
for in, content := range files {
to := filepath.Join(copyRoot, filepath.FromSlash(in))
if err := os.MkdirAll(filepath.Dir(to), 0o700); err != nil {
return nil, fmt.Errorf("the copy could not be made, nothing removed: %w", err)
}
if err := os.WriteFile(to, []byte(content), modes[in]|0o600); err != nil {
return nil, fmt.Errorf("the copy could not be made, nothing removed: %w", err)
}
if back, err := os.ReadFile(to); err != nil || !bytes.Equal(back, []byte(content)) {
return nil, fmt.Errorf("the copy of %s does not read back the same, nothing removed", in)
}
names = append(names, in)
}
r := Removal{At: now.Format(time.RFC3339), Node: p.Node, Kind: kind, Name: name, Path: full, Why: why,
Kept: filepath.Join(kept, filepath.FromSlash(rel)), Files: names}
note, _ := indented(r)
if err := os.WriteFile(filepath.Join(kept, "removal.json"), note, 0o600); err != nil {
return nil, fmt.Errorf("the copy's note could not be written, nothing removed: %w", err)
}
// The item may have changed while it was copied: only what was copied is removed.
again, _, err := readItem(dir, rel, folder)
if err != nil || len(again) != len(files) {
return nil, fmt.Errorf("%s changed while it was copied, nothing removed; the copy is at %s", full, kept)
}
for in, content := range again {
if files[in] != content {
return nil, fmt.Errorf("%s changed while it was copied, nothing removed; the copy is at %s", full, kept)
}
}
if folder {
err = os.RemoveAll(full)
} else {
err = os.Remove(full)
}
if err != nil {
return nil, fmt.Errorf("%s could not be removed (the copy is at %s): %w", full, kept, err)
}
line, _ := json.Marshal(r)
logged := true
if f, err := os.OpenFile(p.removedLog(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600); err == nil {
_, werr := f.Write(append(line, '\n'))
if cerr := f.Close(); werr != nil || cerr != nil {
logged = false
}
} else {
logged = false
}
say("removed %s from the home on the person's word, kept at %s: %s", full, r.Kept, why)
answer := map[string]any{"removed": full, "kind": kind, "name": name, "why": why, "keptAt": r.Kept,
"undo": "copy " + r.Kept + " back to " + full, "log": p.removedLog()}
if !logged {
answer["log"] = "the log could not be written; the removal is noted in " + filepath.Join(kept, "removal.json")
}
return answer, nil
}
@@ -0,0 +1,210 @@
package main
// The person's own items in the home, read and removed on their word (novox/hq ADR 0216 rule 6: removing the
// original is the person's act — the remove tool is that act made explicit).
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
var at = time.Date(2026, 10, 7, 9, 30, 0, 0, time.UTC)
func TestEveryKindIsShownInFull(t *testing.T) {
p, _ := node(t, "laptop")
dir := filepath.Join(p.Home, ".claude")
_ = os.MkdirAll(filepath.Join(dir, "rules"), 0o755)
_ = os.MkdirAll(filepath.Join(dir, "skills", "old", "scripts"), 0o755)
writeFile(t, filepath.Join(dir, "rules", "hal-era.md"), "# old rule\n")
writeFile(t, filepath.Join(dir, "skills", "old", "SKILL.md"), "---\nname: old\n---\n")
writeFile(t, filepath.Join(dir, "skills", "old", "scripts", "a.sh"), "#!/bin/sh\n")
writeFile(t, filepath.Join(dir, "CLAUDE.md"), "# my memory\n")
rule, err := ShowHome(p, "instructions", "hal-era")
if err != nil || rule["content"] != "# old rule\n" || rule["placedByTheMesh"] != false {
t.Fatalf("%v %v", rule, err)
}
if again, err := ShowHome(p, "rule", "hal-era.md"); err != nil || again["content"] != "# old rule\n" {
t.Fatalf("a rule named as a person writes it: %v %v", again, err)
}
skill, err := ShowHome(p, KindSkill, "old")
if files, _ := skill["files"].(map[string]string); err != nil || len(files) != 2 || files["scripts/a.sh"] != "#!/bin/sh\n" {
t.Fatalf("%v %v", skill, err)
}
memory, err := ShowHome(p, KindMemory, "")
if err != nil || memory["content"] != "# my memory\n" {
t.Fatalf("%v %v", memory, err)
}
if _, err := ShowHome(p, KindAgent, "absent"); err == nil {
t.Fatal("an absent item was shown")
}
for _, name := range []string{"../../etc/passwd", "..", ".credentials", "a/b"} {
if _, err := ShowHome(p, KindInstructions, name); err == nil {
t.Errorf("%q reached outside its kind's folder", name)
}
}
if _, err := ShowHome(p, KindHook, "x"); err == nil {
t.Fatal("a kind the home does not hold was taken")
}
// The status tool lists the memory beside the rest.
found := false
for _, it := range HomeItems(p, Config{}, Servers{}) {
found = found || (it.Kind == KindMemory && it.Name == memoryName && !it.Placed)
}
if !found {
t.Fatal("the status does not list the home's memory")
}
}
func TestARemovalNeedsItsReason(t *testing.T) {
p, _ := node(t, "laptop")
dir := filepath.Join(p.Home, ".claude")
_ = os.MkdirAll(filepath.Join(dir, "rules"), 0o755)
writeFile(t, filepath.Join(dir, "rules", "keep.md"), "x")
if _, err := RemoveHome(p, KindInstructions, "keep", " ", at); err == nil {
t.Fatal("removed without a reason")
}
if _, err := os.Stat(filepath.Join(dir, "rules", "keep.md")); err != nil {
t.Fatal("the file went although the removal was refused")
}
}
func TestARemovalKeepsACopyAndLogsWhy(t *testing.T) {
p, _ := node(t, "laptop")
dir := filepath.Join(p.Home, ".claude")
_ = os.MkdirAll(filepath.Join(dir, "rules"), 0o755)
_ = os.MkdirAll(filepath.Join(dir, "skills", "old", "scripts"), 0o755)
writeFile(t, filepath.Join(dir, "rules", "hal-era.md"), "# old rule\n")
writeFile(t, filepath.Join(dir, "skills", "old", "SKILL.md"), "---\nname: old\n---\n")
writeFile(t, filepath.Join(dir, "skills", "old", "scripts", "a.sh"), "#!/bin/sh\n")
_ = os.Chmod(filepath.Join(dir, "skills", "old", "scripts", "a.sh"), 0o755)
answer, err := RemoveHome(p, KindInstructions, "hal-era", "HAL is retired", at)
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(dir, "rules", "hal-era.md")); !os.IsNotExist(err) {
t.Fatal("the rule file is still there")
}
if _, err := os.Stat(filepath.Join(dir, "rules")); err != nil {
t.Fatal("the kind's own folder went with it")
}
kept, _ := answer["keptAt"].(string)
want := filepath.Join(p.State, RemovedDir, "2026-10-07", "093000-instructions-hal-era", "rules", "hal-era.md")
if kept != want {
t.Fatalf("kept at %s, not %s", kept, want)
}
if !strings.HasPrefix(kept, p.State) || strings.HasPrefix(kept, dir) {
t.Fatal("the copy is inside the agent directory")
}
if raw, _ := os.ReadFile(kept); string(raw) != "# old rule\n" {
t.Fatalf("the copy holds %q", raw)
}
// A second removal in the same second does not overwrite the first's copy.
writeFile(t, filepath.Join(dir, "rules", "hal-era.md"), "# made again\n")
second, err := RemoveHome(p, KindInstructions, "hal-era", "again", at)
if err != nil || second["keptAt"] == kept {
t.Fatalf("%v %v", second, err)
}
if raw, _ := os.ReadFile(kept); string(raw) != "# old rule\n" {
t.Fatal("the first copy was overwritten")
}
// A skill goes as its whole folder, kept with its modes.
skill, err := RemoveHome(p, KindSkill, "old", "superseded by the plugin's", at)
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(dir, "skills", "old")); !os.IsNotExist(err) {
t.Fatal("the skill's folder is still there")
}
script := filepath.Join(skill["keptAt"].(string), "scripts", "a.sh")
if info, err := os.Stat(script); err != nil || info.Mode().Perm()&0o100 == 0 {
t.Fatalf("the script's copy lost its executable bit: %v", err)
}
// The log holds every removal, each with its reason.
raw, err := os.ReadFile(p.removedLog())
if err != nil {
t.Fatal(err)
}
lines := strings.Split(strings.TrimSpace(string(raw)), "\n")
if len(lines) != 3 {
t.Fatalf("%d lines logged", len(lines))
}
var r Removal
if err := json.Unmarshal([]byte(lines[0]), &r); err != nil || r.Why != "HAL is retired" || r.Node != "laptop" || r.Kept != kept {
t.Fatalf("%+v %v", r, err)
}
}
func TestTheMemoryIsRemovedAndKept(t *testing.T) {
p, _ := node(t, "laptop")
dir := filepath.Join(p.Home, ".claude")
writeFile(t, filepath.Join(dir, "CLAUDE.md"), "# HAL era\n")
answer, err := RemoveHome(p, KindMemory, "", "the managed file says it now", at)
if err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(dir, "CLAUDE.md")); !os.IsNotExist(err) {
t.Fatal("the memory is still there")
}
if raw, _ := os.ReadFile(answer["keptAt"].(string)); string(raw) != "# HAL era\n" {
t.Fatal("the memory was not kept")
}
}
func TestWhatTheMeshPlacedIsRefused(t *testing.T) {
p, w := node(t, "laptop")
state, view := memConfig{}, NewConfigView(p)
if _, err := Register(p, Item{Kind: KindAgent, Name: "placed", Scope: ScopeHome, Files: one("placed", "v1")}, nil, false, state, view, writer(w)); err != nil {
t.Fatal(err)
}
full := filepath.Join(p.Home, ".claude", "agents", "placed.md")
if _, err := os.Stat(full); err != nil {
t.Fatal("the home item was not placed")
}
shown, err := ShowHome(p, KindAgent, "placed")
if err != nil || shown["placedByTheMesh"] != true {
t.Fatalf("%v %v", shown, err)
}
if _, err := RemoveHome(p, KindAgent, "placed", "tidy", at); err == nil || !strings.Contains(err.Error(), "claude_code_agent_unregister") {
t.Fatalf("what the mesh placed was not refused: %v", err)
}
if _, err := os.Stat(full); err != nil {
t.Fatal("the mesh's item was removed")
}
if _, err := os.Stat(filepath.Join(p.State, RemovedDir)); !os.IsNotExist(err) {
t.Fatal("a refused removal left a copy")
}
}
func TestASymbolicLinkIsLeftAlone(t *testing.T) {
p, _ := node(t, "laptop")
dir := filepath.Join(p.Home, ".claude")
elsewhere := t.TempDir()
writeFile(t, filepath.Join(elsewhere, "target.md"), "not the home's")
_ = os.MkdirAll(filepath.Join(dir, "rules"), 0o755)
if err := os.Symlink(filepath.Join(elsewhere, "target.md"), filepath.Join(dir, "rules", "linked.md")); err != nil {
t.Skip(err)
}
if _, err := RemoveHome(p, KindInstructions, "linked", "x", at); err == nil {
t.Fatal("a symbolic link was removed")
}
if err := os.Symlink(elsewhere, filepath.Join(dir, "skills")); err != nil {
t.Skip(err)
}
_ = os.MkdirAll(filepath.Join(elsewhere, "s"), 0o755)
writeFile(t, filepath.Join(elsewhere, "s", "SKILL.md"), "x")
if _, err := RemoveHome(p, KindSkill, "s", "x", at); err == nil {
t.Fatal("a skill was removed through a linked folder")
}
if _, err := os.Stat(filepath.Join(elsewhere, "s", "SKILL.md")); err != nil {
t.Fatal("what the link points at was removed")
}
}
+3 -1
View File
@@ -54,7 +54,9 @@
"claude_code_config_list",
"claude_code_config_show",
"claude_code_config_status",
"claude_code_config_import"
"claude_code_config_import",
"claude_code_home_show",
"claude_code_home_remove"
],
"data": {
"own": [