Skip to content

Instantly share code, notes, and snippets.

@cniska
Created July 26, 2026 09:28
Show Gist options
  • Select an option

  • Save cniska/26e5337b9b6609b29eef394daaa5b5f7 to your computer and use it in GitHub Desktop.

Select an option

Save cniska/26e5337b9b6609b29eef394daaa5b5f7 to your computer and use it in GitHub Desktop.
Claude Memory Audit Script
#!/usr/bin/env bash
# Mechanical integrity scan for the file-based memory store. Prints PASS / WARN /
# FAIL per check: malformed name: slugs, missing descriptions,
# dead cross-links, orphaned files (present but not in the MEMORY.md index, so they
# never load), index entries pointing at a missing file, and past dates worth
# re-checking. Exits non-zero only when an index reference is genuinely broken;
# WARNs do not fail the run. The judgment calls (stale state, contradictions) still
# need an agent pass — this only covers what a script can prove.
# No color: the consumer is an agent reading the output, not a terminal.
set -uo pipefail
# Deterministic collation and output regardless of the caller's locale, so sort +
# comm stay consistent and the script behaves identically on macOS and Linux.
export LC_ALL=C
# An empty store must yield vanished globs, not a literal "*.md" processed as a file.
shopt -s nullglob
FAILED=0
pass() { printf ' PASS %s\n' "$1"; }
warn() { printf ' WARN %s\n' "$1"; }
fail() { printf ' FAIL %s\n' "$1"; FAILED=1; }
item() { printf ' %s\n' "$1"; }
count() { printf '%s\n' "$1" | grep -c .; }
# Canonical id form: dash-separated, so _ vs - drift never false-positives.
norm() { printf '%s' "$1" | tr '_' '-'; }
DIR="${1:-$HOME/.claude/memory}"
# Without this guard a bad DIR silently scans the caller's cwd and reports on the wrong files.
cd "$DIR" || { echo "memory-audit: cannot enter $DIR" >&2; exit 1; }
index="MEMORY.md"
today="$(date +%Y-%m-%d)"
echo "Memory audit ($DIR)"
# name: slugs declared by each memory file, normalised to dash form (the canonical id)
names="$(grep -h '^name:' -- *.md 2>/dev/null | sed -E 's/^name:[[:space:]]*//; s/^"//; s/"$//' | tr '_' '-' | sort -u)"
# every memory file except the index, one basename per line
files="$(for f in *.md; do [ "$f" = "$index" ] && continue; printf '%s\n' "$f"; done | sort -u)"
# basenames the index links to, with any ./ or directory prefix stripped
indexed="$(grep -oE '\]\([^)]+\.md\)' "$index" 2>/dev/null | sed -E 's/^\]\(//; s/\)$//; s#.*/##' | sort -u)"
# name: hygiene — a slug that is malformed or does not match its filename is the
# root cause of dead links; catch it directly, not only as a downstream symptom.
badnames=""
for f in $files; do
nm="$(grep -m1 '^name:' "$f" | sed -E 's/^name:[[:space:]]*//; s/^"//; s/"$//')"
expect="$(norm "${f%.md}")"
if [ -z "$nm" ]; then
badnames="$badnames$f: no name: field"$'\n'
elif ! printf '%s' "$nm" | grep -qE '^[A-Za-z0-9_-]+$'; then
badnames="$badnames$f: name '$nm' is not a kebab slug"$'\n'
elif [ "$(norm "$nm")" != "$expect" ]; then
badnames="$badnames$f: name '$nm' does not match filename slug '$expect'"$'\n'
fi
done
badnames="$(printf '%s' "$badnames" | grep -v '^$' || true)"
if [ -z "$badnames" ]; then
pass "every name: is a slug matching its filename"
else
warn "$(count "$badnames") file(s) with a malformed or mismatched name:"
printf '%s\n' "$badnames" | while read -r l; do [ -n "$l" ] && item "$l"; done
fi
# Missing description: recall relevance is judged from it, so a blank one is a near-dead memory.
nodesc=""
for f in $files; do
grep -qE '^description:[[:space:]]*\S' "$f" || nodesc="$nodesc$f"$'\n'
done
nodesc="$(printf '%s' "$nodesc" | grep -v '^$' || true)"
if [ -z "$nodesc" ]; then
pass "every file has a description:"
else
warn "$(count "$nodesc") file(s) missing a description:"
printf '%s\n' "$nodesc" | while read -r f; do [ -n "$f" ] && item "$f"; done
fi
# Dead cross-links: a [[target]] with no file whose name: matches. Reported with
# its source file so each finding is actionable on its own.
dead=""
while IFS= read -r hit; do
[ -z "$hit" ] && continue
src="${hit%%:*}"
tgt="${hit#*:}"; tgt="${tgt#*[[}"; tgt="${tgt%]]*}"
grep -qxF -- "$(norm "$tgt")" <<<"$names" || dead="$dead$src: [[$tgt]]"$'\n'
done < <(grep -oHE '\[\[[^]]+\]\]' -- *.md 2>/dev/null)
dead="$(printf '%s' "$dead" | grep -v '^$' || true)"
if [ -z "$dead" ]; then
pass "no dead cross-links"
else
warn "$(count "$dead") dead cross-link(s), target has no matching file"
printf '%s\n' "$dead" | while read -r l; do [ -n "$l" ] && item "$l"; done
fi
# Orphans: a memory file not linked from the index, so it never loads. Compared on
# basename so ./ or directory prefixes in the index links never false-positive.
orphans="$(comm -23 <(printf '%s\n' "$files") <(printf '%s\n' "$indexed") | grep -v '^$' || true)"
if [ -z "$orphans" ]; then
pass "no orphans, every file is linked from $index"
else
warn "$(count "$orphans") orphan(s) not linked from $index"
printf '%s\n' "$orphans" | while read -r f; do [ -n "$f" ] && item "$f"; done
fi
# Index points at a missing file: a dangling reference in the index itself.
missing=""
while read -r f; do
base="${f##*/}"
[ "$base" = "$index" ] && continue
[ -f "$base" ] || missing="$missing$base"$'\n'
done < <(grep -oE '\]\([^)]+\.md\)' "$index" | sed -E 's/^\]\(//; s/\)$//')
missing="$(printf '%s' "$missing" | grep -v '^$' || true)"
if [ -z "$missing" ]; then
pass "every $index entry resolves to a file"
else
fail "$(count "$missing") index entr(y/ies) point at a missing file"
printf '%s\n' "$missing" | while read -r f; do [ -n "$f" ] && item "$f — remove the line from $index or restore the file"; done
fi
# Past dates worth re-checking (YYYY-MM-DD before today, excludes the index's own dates).
dates="$(grep -onE '20[0-9]{2}-[0-9]{2}-[0-9]{2}' -- *.md | awk -F: -v t="$today" '$3 < t && $1 != "MEMORY.md" {print $1": "$3}' | sort -u)"
if [ -z "$dates" ]; then
pass "no past dates to re-check"
else
warn "$(count "$dates") past date(s) worth re-checking"
printf '%s\n' "$dates" | while read -r d; do [ -n "$d" ] && item "$d"; done
fi
echo " summary: $(count "$files") files, $(grep -coE '\]\([^)]+\.md\)' "$index" 2>/dev/null || echo 0) index links"
[ "$FAILED" -eq 0 ] || exit 1
#!/usr/bin/env bash
# Test for memory-audit.sh. Builds throwaway fixture stores and asserts the audit
# reports exactly the seeded defects and nothing else. The false-negative case (a
# real orphan/dead-link/drift going unreported) is the one that matters, so the
# defect fixture mirrors the real index's ./-prefixed links and each file isolates
# a single defect. A second clean fixture guards the green path (all PASS, exit 0).
set -uo pipefail
SCRIPT="$(dirname "$0")/memory-audit.sh"
fails=0
check() { # desc, actual, expected
[ "$2" = "$3" ] || { echo "FAIL: $1 (got '$2', want '$3')"; fails=$((fails+1)); }
}
# --- defect fixture: one defect per file, ./-prefixed index links --------------
fix="$(mktemp -d)"
mk() { printf '%s\n' "$2" > "$fix/$1"; }
# clean indexed pair that must NOT be flagged; good_a links good_b in both dash and
# underscore form to prove _→- normalisation does not false-positive
mk good_a.md '---
name: good-a
description: a clean note
---
links [[good-b]] and [[good_b]].'
mk good_b.md '---
name: good-b
description: another clean note
---
back to [[good-a]].'
# one defect each, everything else about the file valid
mk deadlink.md '---
name: deadlink
description: sources a dead link
---
points at [[does-not-exist]].'
mk orphan.md '---
name: orphan
description: not indexed anywhere
---
nobody indexes me.'
mk dated.md '---
name: dated
description: holds a past date
---
decided 2020-01-01, long past.'
mk badname.md '---
name: Not A Slug
description: name is free text, not a slug
---
body.'
mk nodesc.md '---
name: nodesc
---
body with no description field.'
# index uses ./-prefixed links like the real MEMORY.md; omits orphan.md; points at missing.md
mk MEMORY.md '- [a](./good_a.md)
- [b](./good_b.md)
- [d](./deadlink.md)
- [t](./dated.md)
- [n](./badname.md)
- [x](./nodesc.md)
- [gone](./missing.md)'
out="$("$SCRIPT" "$fix" 2>&1)"; rc=$?
rm -rf "$fix"
want() { grep -qF -- "$1" <<<"$out" || { echo "FAIL: expected '$1'"; fails=$((fails+1)); }; }
wantnot() { grep -qF -- "$1" <<<"$out" && { echo "FAIL: should NOT have flagged '$1'"; fails=$((fails+1)); } || true; }
want "deadlink.md: [[does-not-exist]]" # dead link caught, with its source file
want "orphan.md" # orphan caught despite ./-prefixed index links
want "missing.md" # dangling index target caught
want "dated.md: 2020-01-01" # past date caught
want "badname.md: name 'Not A Slug'" # malformed slug caught
want "nodesc.md" # missing description caught
wantnot "good_a.md" # clean indexed file never flagged an orphan
wantnot "good_b.md" # clean indexed file never flagged an orphan
check "defect fixture exits 1 (broken index ref)" "$rc" "1"
# --- clean fixture: green path, no findings, exit 0 ----------------------------
clean="$(mktemp -d)"
ck() { printf '%s\n' "$2" > "$clean/$1"; }
ck alpha.md '---
name: alpha
description: clean
---
see [[beta]].'
ck beta.md '---
name: beta
description: clean
---
see [[alpha]].'
ck MEMORY.md '- [a](./alpha.md)
- [b](./beta.md)'
cout="$("$SCRIPT" "$clean" 2>&1)"; crc=$?
rm -rf "$clean"
check "clean fixture exits 0" "$crc" "0"
if grep -qE ' (WARN|FAIL) ' <<<"$cout"; then
echo "FAIL: clean fixture produced WARN/FAIL:"; printf '%s\n' "$cout"; fails=$((fails+1))
fi
if [ "$fails" -eq 0 ]; then echo "PASS: all audit checks fired correctly"; else echo "$fails check(s) failed"; exit 1; fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment