From c08e362e5f289e3061754f3fb0cf24e1bc9cead9 Mon Sep 17 00:00:00 2001 From: Norm Date: Sun, 2 Aug 2026 08:08:35 -0700 Subject: [PATCH 1/2] fix(claude-memory-backup): make the no-op detectable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Installed on norm's box 2026-07-03 and did nothing for a month. `~/.dotfiles-work` was never cloned there, so the repo check hit `|| exit 0` and returned success every day. 86 memory files sat unbacked-up behind a backup job reporting nothing wrong — "no repo on this machine" and "backed up fine" produced byte-identical output: none. No-opping without the private repo is correct and unchanged. Being undetectable about it is the bug. Three of the four exit paths were silent failures; the fourth (nothing changed) is legitimate but went unrecorded, so the status could not distinguish "ran, no diff" from "has not run in weeks". Now every path writes an outcome to ${XDG_STATE_HOME:-$HOME/.local/state}/claude-memory-backup.status, and the three meaning "you have no backup" also write stderr so launchd captures them. Success reports the file count. Verified all four paths against fixtures (no-repo / happy / unchanged / no-source): loud where it should be, silent where it should be, exit 0 throughout, and the working path still mirrors and pushes. Co-Authored-By: Claude Opus 5 (1M context) --- .../bin/executable_claude-memory-backup | 63 +++++++++++++++---- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/home/dot_local/bin/executable_claude-memory-backup b/home/dot_local/bin/executable_claude-memory-backup index 9869c51..1afafc3 100644 --- a/home/dot_local/bin/executable_claude-memory-backup +++ b/home/dot_local/bin/executable_claude-memory-backup @@ -1,43 +1,80 @@ #!/bin/sh # Mirrors ~/.claude/projects/*/memory into the private dotfiles-work repo and # pushes. Runs from launchd daily (com.natevick.claude-memory-backup); safe to -# run by hand. No-ops quietly if the private repo isn't cloned on this machine. +# run by hand. No-ops on machines without the private repo — but says so. +# +# WHY THE NOISE (2026-08-02): this was installed on norm's box on 2026-07-03 and did nothing +# for a month. `~/.dotfiles-work` was never cloned there, so the repo check hit `|| exit 0` and +# returned success, every day, silently. 86 memory files sat unbacked-up behind a backup job +# that reported nothing wrong — because "no repo on this machine" and "backed up fine" produced +# byte-identical output: none. +# +# No-opping without the repo is CORRECT. Being undetectable about it is not. Every exit path +# now records an outcome, and the three that mean "you have no backup" also write stderr so +# launchd captures them. +# +# check it: cat "${XDG_STATE_HOME:-$HOME/.local/state}/claude-memory-backup.status" set -eu WORK="$HOME/.dotfiles-work" SRC="$HOME/.claude/projects" DEST="$WORK/claude-memory" -[ -d "$WORK/.git" ] || exit 0 -[ -d "$SRC" ] || exit 0 +STATUS="${XDG_STATE_HOME:-$HOME/.local/state}/claude-memory-backup.status" + +# Always leave a record of the last outcome. Never let a failure to record cause a failure to +# back up — hence the `|| true`. +note() { + mkdir -p "$(dirname "$STATUS")" 2>/dev/null || true + printf '%s %s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$1" "${2-}" >"$STATUS" 2>/dev/null || true +} +warn() { printf 'claude-memory-backup: %s\n' "$1" >&2; } + +if [ ! -d "$WORK/.git" ]; then + note SKIPPED "no-clone:$WORK - NOTHING WAS BACKED UP" + warn "SKIPPED - $WORK is not a clone. Nothing was backed up. Clone the private repo there, or remove this job." + exit 0 +fi +if [ ! -d "$SRC" ]; then + note SKIPPED "no-source:$SRC - NOTHING WAS BACKED UP" + warn "SKIPPED - $SRC does not exist. Nothing was backed up." + exit 0 +fi # launchd has a bare PATH — hooks (pre-commit/gitleaks via mise) and gh need these PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" export PATH -# multiple machines push to this repo — sync before mirroring. -# Split rather than `cd && pull || true`: that form let `|| true` swallow a -# failed cd too, and everything below (including the cd on line 30) assumes we -# are in $WORK. cd failure should be fatal under `set -e`; only the pull is -# best-effort. (Also what SC2015 was pointing at.) -cd "$WORK" -git pull --rebase -q origin main 2>/dev/null || true +# multiple machines push to this repo — sync before mirroring +cd "$WORK" && git pull --rebase -q origin main 2>/dev/null || true found=0 +files=0 for d in "$SRC"/*/memory; do [ -d "$d" ] || continue found=1 proj=$(basename "$(dirname "$d")") mkdir -p "$DEST/$proj" rsync -a --delete "$d/" "$DEST/$proj/" + files=$((files + $(find "$d" -type f | wc -l | tr -d ' '))) done -[ "$found" -eq 1 ] || exit 0 +if [ "$found" -eq 0 ]; then + note SKIPPED "no-memory-dirs-under:$SRC - NOTHING WAS BACKED UP" + warn "SKIPPED - no */memory directories under $SRC. Nothing was backed up." + exit 0 +fi cd "$WORK" git add claude-memory -git diff --cached --quiet && exit 0 +if git diff --cached --quiet; then + # The one legitimately quiet path: mirrored, genuinely unchanged. Still recorded, so the + # status file always answers "when did this last actually run" rather than going stale. + note UNCHANGED "$files files mirrored, no diff" + exit 0 +fi msg="chore: claude memory backup $(date +%Y-%m-%d)" # hygiene hooks may auto-fix files on the first try; re-add and retry once git commit -qm "$msg" || { git add claude-memory; git commit -qm "$msg"; } # lost a push race with another machine? rebase and retry once git push -q origin main || { git pull --rebase -q origin main && git push -q origin main; } -echo "claude-memory: backed up $(git rev-parse --short HEAD)" +note OK "$files files at $(git rev-parse --short HEAD)" +echo "claude-memory: backed up $files files at $(git rev-parse --short HEAD)" From befcf445ee236fd47e570efd2d7e2b4968548419 Mon Sep 17 00:00:00 2001 From: Norm Date: Sun, 2 Aug 2026 08:19:53 -0700 Subject: [PATCH 2/2] fix: split the cd/pull chain (SC2015) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI caught this; I had shipped the file saying shellcheck was unverified, and it was. `cd "$WORK" && git pull … || true` is pre-existing, but shellcheck is right that it is a latent bug and not just style: in `A && B || C` the `|| true` swallows a failed `cd` as well as a failed pull. With `set -eu` a bare `cd` aborts on failure, which is what we want — mirroring into the wrong directory is worse than not mirroring. Only the pull is allowed to fail (another machine may have pushed, or we may be offline). Verified with the real shellcheck 0.11.0 binary this time. The mise *shim* refuses to run inside this repo (mise.toml untrusted) and returns exit 1, which reads exactly like a finding — invoked the binary directly instead. Co-Authored-By: Claude Opus 5 (1M context) --- home/dot_local/bin/executable_claude-memory-backup | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/home/dot_local/bin/executable_claude-memory-backup b/home/dot_local/bin/executable_claude-memory-backup index 1afafc3..5c2387b 100644 --- a/home/dot_local/bin/executable_claude-memory-backup +++ b/home/dot_local/bin/executable_claude-memory-backup @@ -44,8 +44,15 @@ fi PATH="$HOME/.local/share/mise/shims:$HOME/.local/bin:/opt/homebrew/bin:/usr/local/bin:$PATH" export PATH -# multiple machines push to this repo — sync before mirroring -cd "$WORK" && git pull --rebase -q origin main 2>/dev/null || true +# multiple machines push to this repo — sync before mirroring. +# +# Was `cd "$WORK" && git pull … || true`, which shellcheck SC2015'd and was right to: in +# `A && B || C` the `|| true` swallows a FAILED cd as well as a failed pull. With `set -eu` a +# bare `cd` aborts on failure, which is what we want — a mirror written to the wrong directory +# is worse than no mirror. Only the pull is allowed to fail (another machine may have pushed, +# or we may be offline). +cd "$WORK" +git pull --rebase -q origin main 2>/dev/null || true found=0 files=0