Created
July 26, 2026 09:28
-
-
Save cniska/26e5337b9b6609b29eef394daaa5b5f7 to your computer and use it in GitHub Desktop.
Claude Memory Audit Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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