From d7bc7626bb2046745bab280921f38ed813db0839 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Mar 2026 05:33:41 +0000 Subject: [PATCH 1/3] feat: add manual memory review hook (PreToolUse + PostToolUse) Adds scripts/review-memory-write.sh, a Claude Code hook that intercepts every write to MEMORY.md and shows a unified diff before the change becomes permanent. The user can approve, open the file in $EDITOR, or discard (restores from a backup taken by the PreToolUse phase). Also adds templates/settings.json with the hook registration block, updates README with setup instructions, marks CONTRIBUTING problem #1 as solved, and extends the lint workflow to syntax-check the new script. https://claude.ai/code/session_01PT5BQ1jNj5XVFLFMSvLkP9 --- .github/workflows/lint.yml | 2 +- CONTRIBUTING.md | 33 ++-------- README.md | 38 +++++++++++ scripts/review-memory-write.sh | 114 +++++++++++++++++++++++++++++++++ templates/settings.json | 26 ++++++++ 5 files changed, 184 insertions(+), 29 deletions(-) create mode 100644 scripts/review-memory-write.sh create mode 100644 templates/settings.json diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 41b23a7..d350594 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -34,4 +34,4 @@ jobs: - uses: actions/checkout@v4 - name: Check script syntax - run: bash -n scripts/ai-config-sync.sh && bash -n scripts/ai-memory-link.sh + run: bash -n scripts/ai-config-sync.sh && bash -n scripts/ai-memory-link.sh && bash -n scripts/review-memory-write.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f4035a..ab8e804 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,34 +6,11 @@ This project is in early, working state. Contributions that improve clarity, fix ## Open problems worth solving -### 1. Memory review UX before commit - -**The gap:** Claude Code's auto-memory writes MEMORY.md silently during a session. There's no moment where you review what was written before it becomes permanent. You might end up persisting wrong conclusions, misattributed bugs, or stale state. - -**The idea:** A `PostToolUse` hook that fires whenever MEMORY.md is written. The hook would show a diff of changes and prompt: approve, edit, or discard. - -```json -// .claude/settings.json -{ - "hooks": { - "PostToolUse": [{ - "matcher": "Write|Edit", - "hooks": [{ - "type": "command", - "command": "scripts/review-memory-write.sh" - }] - }] - } -} -``` - -The hook script would: -1. Check if the written file is `MEMORY.md` -2. Show `git diff` of the change -3. Prompt: approve / edit / discard -4. On discard: `git checkout -- MEMORY.md` - -This is genuinely unaddressed by any existing tool (as of early 2026). A clean implementation here would be a meaningful contribution. +### 1. Memory review UX before commit ✅ _solved_ + +**Implemented in:** `scripts/review-memory-write.sh` + `templates/settings.json` + +A `PreToolUse` + `PostToolUse` hook pair intercepts every write to MEMORY.md. Before the change becomes permanent the user sees a unified diff and chooses: approve, edit in `$EDITOR`, or discard (restores from backup made by the pre-hook). See the [README](README.md#review-memory-writesh--manual-review-before-changes-stick) for setup instructions. --- diff --git a/README.md b/README.md index 885c1e2..7aa2616 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,44 @@ Generated files are added to `.gitignore` automatically. See [`docs/CROSS_TOOL_S --- +## review-memory-write.sh — manual review before changes stick + +Claude Code can write to MEMORY.md silently during a session. If it saves wrong conclusions, misattributed bugs, or stale state you may not notice until the next session. + +`review-memory-write.sh` is a Claude Code hook that intercepts every write to MEMORY.md and shows you a diff before the change becomes permanent. + +``` + Claude writes MEMORY.md + │ + ▼ + PostToolUse hook fires + │ + ▼ + ┌─────────────────────────────────┐ + │ diff of what changed │ + │ │ + │ a Approve — keep as-is │ + │ e Edit — open in $EDITOR │ + │ d Discard — restore previous │ + └─────────────────────────────────┘ +``` + +### Setup + +```bash +# 1. Put the script on your PATH +cp scripts/review-memory-write.sh ~/.local/bin/ +chmod +x ~/.local/bin/review-memory-write.sh + +# 2. Install the hook config into your project +mkdir -p .claude +cp /path/to/ai-dev-context/templates/settings.json .claude/settings.json +``` + +The `templates/settings.json` file registers the hook for both `PreToolUse` (backup) and `PostToolUse` (review). If your project already has a `.claude/settings.json`, merge the `hooks` block in manually. + +--- + ## Complementary tools These tools solve adjacent problems and work well alongside this system: diff --git a/scripts/review-memory-write.sh b/scripts/review-memory-write.sh new file mode 100644 index 0000000..f969a6b --- /dev/null +++ b/scripts/review-memory-write.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# review-memory-write.sh — Prompt for manual review whenever MEMORY.md is written. +# +# Register as both a PreToolUse and PostToolUse hook in .claude/settings.json. +# See templates/settings.json in this repo for the configuration block. +# +# PreToolUse: backs up MEMORY.md before any write so a diff is possible. +# PostToolUse: shows the diff and prompts: (a)pprove / (e)dit / (d)iscard. +# +# For any file that is not a MEMORY.md the script exits immediately. +set -euo pipefail + +BACKUP_DIR="${TMPDIR:-/tmp}/ai-memory-review" + +# --- Read and parse JSON from stdin --- + +INPUT="$(cat)" + +parse() { + # Usage: parse 'dot.separated.key' + export PARSE_KEY="$1" + printf '%s\n' "$INPUT" | python3 -c " +import sys, json, os +try: + d = json.load(sys.stdin) + for k in os.environ.get('PARSE_KEY', '').split('.'): + d = d.get(k, '') if isinstance(d, dict) else '' + print(d if isinstance(d, str) else '') +except Exception: + print('') +" 2>/dev/null || true +} + +hook_event="$(parse 'hook_event_name')" +file_path="$(parse 'tool_input.file_path')" + +# --- Only act on MEMORY.md files --- + +if [[ "$(basename "$file_path")" != "MEMORY.md" ]]; then + exit 0 +fi + +mkdir -p "$BACKUP_DIR" +backup_path="${BACKUP_DIR}/$(printf '%s' "$file_path" | tr '/' '_').bak" + +# --- PreToolUse: save a backup before the write --- + +if [[ "$hook_event" == "PreToolUse" ]]; then + if [[ -f "$file_path" ]]; then + cp "$file_path" "$backup_path" + else + # New file — create empty backup so diff shows everything as added + : > "$backup_path" + fi + exit 0 # must exit 0 to allow the tool to proceed +fi + +# --- PostToolUse: show diff and prompt --- + +if [[ "$hook_event" != "PostToolUse" ]]; then + exit 0 +fi + +# Require a TTY for interactive prompting; auto-approve if none is available +if [[ ! -e /dev/tty ]]; then + exit 0 +fi + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " MEMORY.md was modified — review before it becomes permanent" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +if [[ -f "$backup_path" ]]; then + diff -u "$backup_path" "$file_path" && echo "(no changes detected)" || true +else + echo "(no backup found — showing full file)" + cat "$file_path" +fi + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " a Approve — keep changes as-is" +echo " e Edit — open in \$EDITOR before approving" +echo " d Discard — restore the previous version" +echo "" + +exec < /dev/tty +read -r -p "Choice [a/e/d, default a]: " choice + +case "${choice,,}" in + e|edit) + "${EDITOR:-vi}" "$file_path" < /dev/tty > /dev/tty + echo " ✓ Memory saved after editing." + rm -f "$backup_path" + ;; + d|discard) + if [[ -f "$backup_path" ]]; then + cp "$backup_path" "$file_path" + rm -f "$backup_path" + echo " ✗ Changes discarded — MEMORY.md restored to previous version." + else + echo " ! No backup available — cannot restore. File left as-is." + fi + ;; + *) + # Default: approve + echo " ✓ Changes approved." + rm -f "$backup_path" + ;; +esac + +echo "" diff --git a/templates/settings.json b/templates/settings.json new file mode 100644 index 0000000..fe3b58c --- /dev/null +++ b/templates/settings.json @@ -0,0 +1,26 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "review-memory-write.sh" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "review-memory-write.sh" + } + ] + } + ] + } +} From 152240e1e7aaaa3413a7ed0855503844cae22ca3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Mar 2026 06:12:21 +0000 Subject: [PATCH 2/3] refactor: replace python3 JSON parsing with grep+sed Drops the python3 dependency. The hook JSON from Claude Code is structured enough that two grep -o | head -1 | sed calls can extract hook_event_name and file_path reliably without any external interpreter. https://claude.ai/code/session_01PT5BQ1jNj5XVFLFMSvLkP9 --- scripts/review-memory-write.sh | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/scripts/review-memory-write.sh b/scripts/review-memory-write.sh index f969a6b..0930db9 100644 --- a/scripts/review-memory-write.sh +++ b/scripts/review-memory-write.sh @@ -16,23 +16,18 @@ BACKUP_DIR="${TMPDIR:-/tmp}/ai-memory-review" INPUT="$(cat)" +# Extract a JSON string value by key name using only grep and sed. +# Matches the first occurrence of "key": "value" anywhere in the JSON. +# Sufficient for simple string values (hook event names, file paths). parse() { - # Usage: parse 'dot.separated.key' - export PARSE_KEY="$1" - printf '%s\n' "$INPUT" | python3 -c " -import sys, json, os -try: - d = json.load(sys.stdin) - for k in os.environ.get('PARSE_KEY', '').split('.'): - d = d.get(k, '') if isinstance(d, dict) else '' - print(d if isinstance(d, str) else '') -except Exception: - print('') -" 2>/dev/null || true + printf '%s\n' "$INPUT" \ + | grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \ + | head -1 \ + | sed 's/.*:[[:space:]]*"\(.*\)"/\1/' } hook_event="$(parse 'hook_event_name')" -file_path="$(parse 'tool_input.file_path')" +file_path="$(parse 'file_path')" # --- Only act on MEMORY.md files --- From 0325dabb320cd63de3b0a6cd70ff3afcd06d5fad Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 2 Mar 2026 06:15:23 +0000 Subject: [PATCH 3/3] refactor: restore python3 JSON parsing for readability grep+sed is fragile for nested keys and harder to follow. python3 is standard on macOS and Linux so the dependency is acceptable. Also documents python3 and diff as explicit dependencies in the README. https://claude.ai/code/session_01PT5BQ1jNj5XVFLFMSvLkP9 --- README.md | 2 ++ scripts/review-memory-write.sh | 19 +++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7aa2616..6695ada 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,8 @@ cp /path/to/ai-dev-context/templates/settings.json .claude/settings.json The `templates/settings.json` file registers the hook for both `PreToolUse` (backup) and `PostToolUse` (review). If your project already has a `.claude/settings.json`, merge the `hooks` block in manually. +**Dependencies:** `python3` (for JSON parsing), `diff` (from diffutils — standard on any Linux/macOS install). + --- ## Complementary tools diff --git a/scripts/review-memory-write.sh b/scripts/review-memory-write.sh index 0930db9..b2f71b8 100644 --- a/scripts/review-memory-write.sh +++ b/scripts/review-memory-write.sh @@ -16,18 +16,21 @@ BACKUP_DIR="${TMPDIR:-/tmp}/ai-memory-review" INPUT="$(cat)" -# Extract a JSON string value by key name using only grep and sed. -# Matches the first occurrence of "key": "value" anywhere in the JSON. -# Sufficient for simple string values (hook event names, file paths). +# Parse a value from the JSON input by dot-separated key path. +# Requires python3 (standard on macOS and Linux). parse() { - printf '%s\n' "$INPUT" \ - | grep -o "\"$1\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" \ - | head -1 \ - | sed 's/.*:[[:space:]]*"\(.*\)"/\1/' + export PARSE_KEY="$1" + printf '%s\n' "$INPUT" | python3 -c " +import sys, json, os +d = json.load(sys.stdin) +for k in os.environ['PARSE_KEY'].split('.'): + d = d.get(k, '') if isinstance(d, dict) else '' +print(d if isinstance(d, str) else '') +" 2>/dev/null || true } hook_event="$(parse 'hook_event_name')" -file_path="$(parse 'file_path')" +file_path="$(parse 'tool_input.file_path')" # --- Only act on MEMORY.md files ---