claude-code: undo a home removal 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 delivered

A removal kept its copy but nothing could put it back without a shell on
the machine. claude_code_home_restore puts a kept copy back when nothing is
at its path and the copy matches the digests recorded at removal;
claude_code_home_removed lists what is kept.
This commit is contained in:
jochen
2026-10-07 19:44:07 +02:00
parent 03728728c3
commit a0deb90741
5 changed files with 304 additions and 23 deletions
+180 -21
View File
@@ -15,6 +15,7 @@ import (
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"time"
)
@@ -176,14 +177,22 @@ func ShowHome(p Paths, kind, name string) (map[string]any, error) {
// 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"`
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
@@ -229,7 +238,7 @@ func RemoveHome(p Paths, kind, name, why string, now time.Time) (map[string]any,
if !folder {
copyRoot = filepath.Dir(copyRoot)
}
names := make([]string, 0, len(files))
names := map[string]KeptFile{}
for in, content := range files {
to := filepath.Join(copyRoot, filepath.FromSlash(in))
if err := os.MkdirAll(filepath.Dir(to), 0o700); err != nil {
@@ -241,9 +250,9 @@ func RemoveHome(p Paths, kind, name, why string, now time.Time) (map[string]any,
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)
names[in] = KeptFile{Digest: digest(content), Mode: fmt.Sprintf("%04o", modes[in])}
}
r := Removal{At: now.Format(time.RFC3339), Node: p.Node, Kind: kind, Name: name, Path: full, Why: why,
r := Removal{Action: "removed", 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 {
@@ -269,16 +278,7 @@ func RemoveHome(p Paths, kind, name, why string, now time.Time) (map[string]any,
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
}
logged := logRemoval(p, r)
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()}
@@ -287,3 +287,162 @@ func RemoveHome(p Paths, kind, name, why string, now time.Time) (map[string]any,
}
return answer, nil
}
// logRemoval appends one line to the removed-items log, and answers whether it could.
func logRemoval(p Paths, r Removal) bool {
line, _ := json.Marshal(r)
f, err := os.OpenFile(p.removedLog(), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return false
}
_, werr := f.Write(append(line, '\n'))
return f.Close() == nil && werr == nil
}
// ---- putting a removal back ---------------------------------------------------------------------------
// Kept is one removal whose copy the module keeps, as the list of removals shows it.
type Kept struct {
Removal
Restored string `json:"restored,omitempty"`
}
// KeptRemovals lists every removal the module keeps a copy of, newest first, and whether it was put back.
func KeptRemovals(p Paths) ([]Kept, error) {
root := filepath.Join(p.State, RemovedDir)
notes, _ := filepath.Glob(filepath.Join(root, "*", "*", "removal.json"))
out := []Kept{}
for _, note := range notes {
var k Kept
if !readJSON(note, &k.Removal) {
continue
}
var back Removal
if readJSON(filepath.Join(filepath.Dir(note), "restored.json"), &back) {
k.Restored = back.At
}
out = append(out, k)
}
sort.Slice(out, func(i, j int) bool {
if out[i].At != out[j].At {
return out[i].At > out[j].At
}
return out[i].Kept > out[j].Kept
})
return out, nil
}
// removalFolder finds the folder of one removal from what a removal answered as keptAt (or the folder
// itself): <state>/removed-from-home/<date>/<removal>, and nothing outside it.
func removalFolder(p Paths, kept string) (string, error) {
root := filepath.Join(p.State, RemovedDir)
rel, err := filepath.Rel(root, filepath.Clean(kept))
parts := strings.Split(filepath.ToSlash(rel), "/")
if err != nil || kept == "" || len(parts) < 2 || parts[0] == ".." || parts[0] == "." {
return "", fmt.Errorf("%q is not a copy this module kept: give the keptAt a removal answered, as claude_code_home_removed lists it", kept)
}
return filepath.Join(root, parts[0], parts[1]), nil
}
// RestoreHome puts a removed item back where it was: only when nothing is at that path now, and only when the
// kept copy is exactly what was removed, file by file against the digests its note recorded. Logged as the
// removal was; the copy stays, marked as put back.
func RestoreHome(p Paths, kept string, now time.Time) (map[string]any, error) {
folder, err := removalFolder(p, kept)
if err != nil {
return nil, err
}
var r Removal
if !readJSON(filepath.Join(folder, "removal.json"), &r) {
return nil, fmt.Errorf("%s holds no readable removal.json: nothing to check the copy against, nothing restored", folder)
}
// Where it goes is worked out again from its kind and name, never taken from the note alone.
rel, isFolder, err := HomeItemPath(r.Kind, r.Name)
if err != nil {
return nil, err
}
dir := filepath.Join(p.Home, ".claude")
full := filepath.Join(dir, filepath.FromSlash(rel))
if r.Path != full || r.Kept != filepath.Join(folder, filepath.FromSlash(rel)) {
return nil, fmt.Errorf("%s/removal.json does not describe the copy beside it, nothing restored", folder)
}
// The copy's own integrity: the same files, each with the digest recorded when it was removed.
files, _, err := readItem(folder, rel, isFolder)
if err != nil {
return nil, fmt.Errorf("the kept copy cannot be read, nothing restored: %w", err)
}
if len(files) != len(r.Files) {
return nil, fmt.Errorf("the kept copy holds %d file(s), its note %d: nothing restored", len(files), len(r.Files))
}
for in, content := range files {
want, ok := r.Files[in]
if !ok || digest(content) != want.Digest {
return nil, fmt.Errorf("the kept copy's %s is not what was removed: nothing restored", in)
}
}
// Nothing may be in the way: whatever is there now is the person's, or the mesh's.
if why := linkedParent(dir, rel+"/x"); why != "" {
return nil, errors.New(why + ", nothing restored")
}
if _, err := os.Lstat(full); err == nil {
return nil, fmt.Errorf("%s exists now, nothing restored: look at it with claude_code_home_show first", full)
} else if !os.IsNotExist(err) {
return nil, err
}
base := full
if !isFolder {
base = filepath.Dir(full)
}
written := []string{}
undo := func() {
for _, w := range written {
_ = os.Remove(w)
}
if isFolder {
_ = os.RemoveAll(full)
}
}
for in, content := range files {
to := filepath.Join(base, filepath.FromSlash(in))
if !isFolder {
to = full
}
mode := os.FileMode(0o644)
var m uint32
if _, err := fmt.Sscanf(r.Files[in].Mode, "%o", &m); err == nil && m != 0 {
mode = os.FileMode(m).Perm()
}
if err := os.MkdirAll(filepath.Dir(to), 0o755); err != nil {
undo()
return nil, fmt.Errorf("%s could not be restored: %w", full, err)
}
f, err := os.OpenFile(to, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode)
if err != nil {
undo()
return nil, fmt.Errorf("%s could not be restored: %w", full, err)
}
_, werr := f.WriteString(content)
if cerr := f.Close(); werr != nil || cerr != nil {
undo()
return nil, fmt.Errorf("%s could not be restored", to)
}
_ = os.Chmod(to, mode) // the umask may have taken bits the file had
written = append(written, to)
}
back := Removal{Action: "restored", At: now.UTC().Format(time.RFC3339), Node: p.Node, Kind: r.Kind, Name: r.Name,
Path: full, Why: "undoes the removal of " + r.At + ": " + r.Why, Kept: r.Kept, Files: r.Files}
note, _ := indented(back)
_ = os.WriteFile(filepath.Join(folder, "restored.json"), note, 0o600)
logged := logRemoval(p, back)
say("restored %s from %s, undoing its removal of %s", full, r.Kept, r.At)
answer := map[string]any{"restored": full, "kind": r.Kind, "name": r.Name, "from": r.Kept, "removedAt": r.At,
"log": p.removedLog()}
if !logged {
answer["log"] = "the log could not be written; the restore is noted in " + filepath.Join(folder, "restored.json")
}
return answer, nil
}