Skip to content

Instantly share code, notes, and snippets.

@a5r0n
Created March 9, 2026 20:23
Show Gist options
  • Select an option

  • Save a5r0n/14544654f0469edab17e0d0157938157 to your computer and use it in GitHub Desktop.

Select an option

Save a5r0n/14544654f0469edab17e0d0157938157 to your computer and use it in GitHub Desktop.
Claude code session memory hook
#!/bin/bash
# Auto-save session learnings to memory on exit
LOG="$HOME/.claude/hooks/session-end-memory.log"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG"; }
# Loop prevention — child claude -p session also triggers SessionEnd
LOCK="/tmp/claude-memory-save.lock"
if [ -f "$LOCK" ]; then
PID=$(cat "$LOCK")
if kill -0 "$PID" 2>/dev/null; then
log "Skipping — memory save already running (PID $PID)"
exit 0
else
log "Removing stale lock (PID $PID no longer running)"
rm -f "$LOCK"
fi
fi
echo $$ > "$LOCK"
trap "rm -f $LOCK" EXIT
# Only fire on explicit /exit
HOOK_INPUT=$(cat)
REASON=$(jq -r '.reason' <<< "$HOOK_INPUT")
if [ "$REASON" != "prompt_input_exit" ]; then
log "Skipping — reason was '$REASON', not prompt_input_exit"
exit 0
fi
TRANSCRIPT=$(jq -r '.transcript_path' <<< "$HOOK_INPUT")
# Derive memory dir from git root — mirrors Claude's path escaping (/ → -)
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
ESCAPED=$(echo "$PROJECT_ROOT" | sed 's|/|-|g')
MEMORY_DIR="$HOME/.claude/projects/$ESCAPED/memory"
if [ ! -d "$MEMORY_DIR" ]; then
log "Skipping — no memory dir at $MEMORY_DIR"
exit 0
fi
log "Running memory save for $MEMORY_DIR (transcript: $TRANSCRIPT)"
# Pre-extract text content from transcript to avoid token overload when reading raw JSONL.
# Pulls assistant text + user text, truncates to ~8000 chars, saves to a temp summary file.
SUMMARY_FILE=$(mktemp /tmp/claude-session-summary.XXXXXX)
python3 - "$TRANSCRIPT" "$SUMMARY_FILE" <<'PYEOF'
import sys, json
transcript_path = sys.argv[1]
output_path = sys.argv[2]
MAX_CHARS = 8000
chunks = []
with open(transcript_path) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
obj = json.loads(line)
except json.JSONDecodeError:
continue
role = obj.get("role", "")
if role not in ("user", "assistant"):
continue
content = obj.get("content", "")
texts = []
if isinstance(content, list):
for block in content:
if isinstance(block, dict) and block.get("type") == "text":
texts.append(block["text"])
elif isinstance(content, str):
texts.append(content)
for text in texts:
text = text.strip()
if text:
chunks.append(f"[{role}]: {text}")
full = "\n---\n".join(chunks)
if len(full) > MAX_CHARS:
full = full[:MAX_CHARS] + "\n...[truncated]"
with open(output_path, "w") as f:
f.write(full)
PYEOF
trap "rm -f $LOCK $SUMMARY_FILE" EXIT
claude -p "Review the following session summary and save any useful patterns, tool learnings, architectural insights, or user preferences to $MEMORY_DIR. Update MEMORY.md if needed and create or update topic files for detailed notes. Be concise and avoid duplicating existing entries.
<session_summary>
$(cat "$SUMMARY_FILE")
</session_summary>" \
--allowedTools "Read,Write,Edit,Glob"
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"SessionEnd": [
{
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/session-end-memory.sh",
"statusMessage": "Saving session memories..."
}
]
}
]
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment