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..6695ada 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,46 @@ 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. + +**Dependencies:** `python3` (for JSON parsing), `diff` (from diffutils — standard on any Linux/macOS install). + +--- + ## 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..b2f71b8 --- /dev/null +++ b/scripts/review-memory-write.sh @@ -0,0 +1,112 @@ +#!/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 a value from the JSON input by dot-separated key path. +# Requires python3 (standard on macOS and Linux). +parse() { + 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 '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" + } + ] + } + ] + } +}