This guide covers how to use git-notes-memory as both a Python library and a Claude Code plugin.
- Concepts
- Installation
- Python Library Usage
- Claude Code Plugin
- Memory Namespaces
- Searching Memories
- Configuration
- Hooks Integration
- Debugging, Discovery & Memory Review
- Troubleshooting
Memories are structured pieces of context attached to git commits. They capture:
- Decisions made during development
- Learnings and technical insights
- Progress milestones
- Blockers and their resolutions
- Research findings
Memories are stored as git notes - a built-in git feature that attaches metadata to commits without modifying commit history. This means:
- Memories sync when you
git push/git pull - No impact on commit SHAs or history
- Separate from code changes
- Supports team collaboration
The library creates vector embeddings of your memories using sentence-transformers. This enables:
- Finding memories by meaning, not just keywords
- "What did we decide about caching?" finds memories about Redis, Memcached, etc.
- Similarity scoring for relevance ranking
- Python 3.11 or higher
- Git 2.25 or higher
- ~500MB disk space for the embedding model (downloaded on first use)
pip install git-notes-memoryuv add git-notes-memoryfrom git_notes_memory import get_capture_service, get_recall_service
print("Installation successful!")from git_notes_memory import get_capture_service
# Get the capture service (singleton)
capture = get_capture_service()
# Basic capture
result = capture.capture(
namespace="decisions",
summary="Chose PostgreSQL over SQLite",
content="""
## Context
Need persistent storage for user data.
## Decision
PostgreSQL for production, SQLite for testing.
## Rationale
- Better concurrent write support
- Rich query capabilities
- Team familiarity
"""
)
print(f"Captured memory: {result.memory_id}")
print(f"At commit: {result.commit}")result = capture.capture(
namespace="decisions",
summary="API authentication strategy",
content="Using JWT with refresh tokens...",
spec="AUTH-2024-001", # Link to a spec/project
tags=["security", "api"], # Searchable tags
phase="implementation", # Development phase
relates_to=["ARCH-001"], # Related memories/specs
)# Capture a blocker
capture.capture_blocker(
summary="CI pipeline timeout",
content="Tests exceed 30-minute limit...",
resolution="Split into parallel jobs"
)
# Capture a learning
capture.capture_learning(
summary="pytest-asyncio fixture scope",
content="Use session scope for expensive async fixtures..."
)
# Capture a decision (ADR style)
capture.capture_decision(
summary="Use pydantic for validation",
content="## Context\n...\n## Decision\n...",
spec="PROJ-001"
)from git_notes_memory import get_recall_service
recall = get_recall_service()
# Semantic search
results = recall.search("authentication strategy")
for mem in results:
print(f"[{mem.similarity:.2f}] {mem.memory.summary}")
# Filter by namespace
decisions = recall.search(
"database choice",
namespace="decisions",
k=5 # Return top 5 results
)
# Filter by spec
project_memories = recall.search(
"performance issues",
spec="PROJ-001"
)
# Set minimum similarity threshold
relevant = recall.search(
"caching strategy",
min_similarity=0.5 # Only results with >50% similarity
)# Full-text search (FTS5) instead of semantic
results = recall.search_text("PostgreSQL", limit=10)# Get by ID
memory = recall.get("decisions:abc1234:0")
# Get batch
memories = recall.get_batch(["decisions:abc1234:0", "learnings:def5678:1"])
# Get all in namespace
all_decisions = recall.get_by_namespace("decisions")
# Get for specific spec
spec_memories = recall.get_by_spec("PROJ-001")# Get relevant memories for current context
context = """
Working on adding Redis caching to the API layer.
Need to decide on cache invalidation strategy.
"""
suggestions = recall.proactive_recall(context, max_suggestions=5)
for mem in suggestions:
print(f"Relevant: {mem.summary}")Control how much data is loaded:
from git_notes_memory import HydrationLevel
# Summary only (fast, minimal)
light = recall.hydrate(memory_id, level=HydrationLevel.SUMMARY)
# Full content
full = recall.hydrate(memory_id, level=HydrationLevel.FULL)
# With file snapshots from commit
detailed = recall.hydrate(memory_id, level=HydrationLevel.FILES)from git_notes_memory import get_sync_service
sync = get_sync_service()
# Full reindex (rebuilds from scratch)
count = sync.reindex(full=True)
print(f"Indexed {count} memories")
# Incremental sync
count = sync.reindex(full=False)
# Verify consistency
result = sync.verify_consistency()
if not result.is_consistent:
print(f"Missing: {result.missing_in_index}")
print(f"Orphaned: {result.orphaned_in_index}")
# Auto-repair
sync.repair(result)When installed as a Claude Code plugin, these slash commands are available:
Capture a memory in the current conversation.
/memory:capture decisions Chose React over Vue for the frontend
Search for relevant memories.
/memory:recall database migration strategy
Advanced search with filtering options.
/memory:search PostgreSQL --type=text --namespace=decisions
Synchronize the index with git notes.
/memory:sync # Incremental
/memory:sync full # Full rebuild
/memory:sync verify # Check consistency
/memory:sync repair # Fix inconsistencies
Show index statistics and health.
/memory:status
/memory:status --verbose
Output:
Memory Index Status
===================
Total Memories: 47
By Namespace:
decisions: 12
learnings: 15
progress: 8
blockers: 5
research: 7
Index Size: 2.3 MB
Last Sync: 2024-01-15 10:30:00
The system supports 10 predefined namespaces, each for a specific purpose:
| Namespace | Purpose | Example |
|---|---|---|
inception |
Problem statements, scope, success criteria | "Building a CLI for ADR management" |
elicitation |
Requirements clarifications, constraints | "Must support Git 2.25+" |
research |
External findings, technology evaluations | "Compared sqlite-vec vs pgvector" |
decisions |
Architecture Decision Records | "Chose YAML over JSON for config" |
progress |
Task completions, milestones | "Completed Phase 1: Foundation" |
blockers |
Obstacles and resolutions | "CI timeout - fixed with parallelization" |
reviews |
Code review findings | "Security: validate all git refs" |
learnings |
Technical insights, patterns | "pytest fixtures need session scope" |
retrospective |
Post-mortems | "MVP delivered, 90% coverage achieved" |
patterns |
Cross-project generalizations | "Always use frozen dataclasses" |
Semantic search finds memories by meaning. Tips for better results:
- Use natural language: "How did we handle user authentication?" works better than "auth"
- Include context: "caching for API responses" beats just "caching"
- Combine with filters: Use
namespace=andspec=to narrow scope
# Namespace filter
recall.search("performance", namespace="learnings")
# Spec filter
recall.search("security", spec="AUTH-2024")
# Combined
recall.search("database", namespace="decisions", spec="PROJ-001")
# Similarity threshold
recall.search("api design", min_similarity=0.6)
# Result limit
recall.search("testing strategy", k=3)All environment variables are optional. Defaults are shown below.
| Variable | Description | Default |
|---|---|---|
MEMORY_PLUGIN_DATA_DIR |
Data directory path | ~/.local/share/memory-plugin/ |
MEMORY_PLUGIN_GIT_NAMESPACE |
Git notes ref | refs/notes/mem |
MEMORY_PLUGIN_EMBEDDING_MODEL |
Model name | all-MiniLM-L6-v2 |
MEMORY_PLUGIN_AUTO_CAPTURE |
Auto-capture hook | false |
| Variable | Description | Default |
|---|---|---|
HOOK_ENABLED |
Master switch for hooks | true |
HOOK_SESSION_START_ENABLED |
Enable SessionStart hook | true |
HOOK_USER_PROMPT_ENABLED |
Enable UserPromptSubmit hook | false |
HOOK_POST_TOOL_USE_ENABLED |
Enable PostToolUse hook | true |
HOOK_PRE_COMPACT_ENABLED |
Enable PreCompact hook | true |
HOOK_STOP_ENABLED |
Enable Stop hook | true |
HOOK_DEBUG |
Enable debug logging | false |
HOOK_TIMEOUT |
Hook timeout in seconds | 30 |
| Variable | Description | Default |
|---|---|---|
HOOK_SESSION_START_BUDGET_MODE |
Budget mode: adaptive, fixed, full, minimal | adaptive |
HOOK_SESSION_START_FIXED_BUDGET |
Fixed budget (when mode=fixed) | 1000 |
HOOK_SESSION_START_MAX_BUDGET |
Maximum budget cap | 3000 |
HOOK_SESSION_START_INCLUDE_GUIDANCE |
Include response guidance in context | true |
HOOK_SESSION_START_GUIDANCE_DETAIL |
Guidance detail: minimal, standard, detailed | standard |
| Variable | Description | Default |
|---|---|---|
HOOK_CAPTURE_DETECTION_MIN_CONFIDENCE |
Minimum confidence for suggestions | 0.7 |
HOOK_CAPTURE_DETECTION_AUTO_THRESHOLD |
Threshold for auto-capture | 0.95 |
HOOK_CAPTURE_DETECTION_NOVELTY_THRESHOLD |
Minimum novelty score | 0.3 |
export MEMORY_PLUGIN_DATA_DIR=/path/to/custom/data- Index database:
$DATA_DIR/index.db(SQLite + sqlite-vec) - Embedding model:
$DATA_DIR/models/(downloaded once) - Lock file:
$DATA_DIR/.capture.lock
You can create a .env file in your project root. See .env.example for all available options:
# Copy the example
cp .env.example .env
# Edit as needed
vim .envThe memory plugin includes hooks that integrate with Claude Code's hook system for automatic memory context injection and capture assistance.
Five hooks are available:
| Hook | Event | Purpose | Default |
|---|---|---|---|
| SessionStart | Session begins | Inject project memories and response guidance | Enabled |
| UserPromptSubmit | User sends prompt | Detect capture signals and inline markers | Disabled |
| PostToolUse | After Read/Write/Edit | Surface related memories for files | Enabled |
| PreCompact | Before context compaction | Auto-capture high-confidence content | Enabled |
| Stop | Session ends | Prompt for uncaptured content, sync index | Enabled |
Automatically injects relevant memories at the start of each Claude Code session.
What it does:
- Detects the current project (from git repo, package.json, pyproject.toml)
- Identifies the active spec (from CLAUDE.md or docs/spec/active/)
- Builds context within a token budget
- Injects XML-formatted memory context
Context includes:
- Working memory: pending actions, recent decisions, active blockers
- Semantic context: learnings and patterns relevant to the project
- Available commands: quick reference for memory operations
Configuration:
# Disable SessionStart hook
export HOOK_SESSION_START_ENABLED=false
# Budget modes: adaptive (default), fixed, full, minimal
export HOOK_SESSION_START_BUDGET_MODE=adaptive
# Fixed budget (when mode=fixed)
export HOOK_SESSION_START_FIXED_BUDGET=1000
# Maximum budget cap
export HOOK_SESSION_START_MAX_BUDGET=3000Analyzes user prompts for capture-worthy content and suggests or auto-captures memories.
What it detects:
- Decisions: "I decided to use...", "we're going with..."
- Learnings: "I learned that...", "TIL:", "turns out..."
- Blockers: "blocked by...", "stuck on...", "can't because..."
- Progress: "completed...", "finished...", "done with..."
Capture actions:
- AUTO: High-confidence signals (>=95%) are captured automatically
- SUGGEST: Medium-confidence signals (70-95%) show suggestions
- SKIP: Low-confidence or duplicate content is ignored
Configuration:
# Enable signal detection (disabled by default)
export HOOK_USER_PROMPT_ENABLED=true
# Minimum confidence for suggestions (0.0-1.0)
export HOOK_CAPTURE_DETECTION_MIN_CONFIDENCE=0.7
# Threshold for auto-capture (0.0-1.0)
export HOOK_CAPTURE_DETECTION_AUTO_THRESHOLD=0.95
# Novelty threshold (how different from existing memories)
export HOOK_CAPTURE_DETECTION_NOVELTY_THRESHOLD=0.3Surfaces related memories after file operations to provide contextual information.
What it does:
- Triggers after Read, Write, Edit, or MultiEdit operations
- Extracts domain terms from the file path (e.g.,
src/auth/jwt.py→ auth, jwt) - Searches for memories related to those domains
- Injects relevant memories as additional context
Example output:
When you read or edit src/auth/oauth_handler.py, the hook may inject:
<related_memories>
<memory namespace="decisions" confidence="0.82">
Chose OAuth 2.0 with PKCE for authentication flow
</memory>
<memory namespace="learnings" confidence="0.75">
Token refresh must happen before expiry to avoid race conditions
</memory>
</related_memories>Configuration:
# Disable PostToolUse hook
export HOOK_POST_TOOL_USE_ENABLED=false
# Minimum similarity threshold (0.0-1.0)
export HOOK_POST_TOOL_USE_MIN_SIMILARITY=0.6
# Maximum memories to inject
export HOOK_POST_TOOL_USE_MAX_RESULTS=3
# Timeout in seconds
export HOOK_POST_TOOL_USE_TIMEOUT=5Auto-captures high-confidence content before Claude Code compacts the context.
What it does:
- Triggers before context compaction (automatic or manual)
- Analyzes the conversation transcript for uncaptured signals
- Filters to high-confidence items (≥85% by default)
- Auto-captures up to 3 memories to prevent information loss
- Reports captures via stderr (visible in terminal)
Example stderr output:
Auto-captured 2 memories before compaction:
- [decisions] Chose PostgreSQL for JSONB support
- [learnings] pytest fixtures need session scope for DB connections
Configuration:
# Disable PreCompact hook
export HOOK_PRE_COMPACT_ENABLED=false
# Enable auto-capture (captures without prompting)
export HOOK_PRE_COMPACT_AUTO_CAPTURE=true
# Enable suggestion mode (prompts before capturing)
export HOOK_PRE_COMPACT_PROMPT_FIRST=false
# Minimum confidence for auto-capture (0.0-1.0)
export HOOK_PRE_COMPACT_MIN_CONFIDENCE=0.85
# Maximum memories to auto-capture
export HOOK_PRE_COMPACT_MAX_CAPTURES=3
# Timeout in seconds
export HOOK_PRE_COMPACT_TIMEOUT=15Performs session-end cleanup and memory assistance.
What it does:
- Analyzes session transcript for uncaptured memorable content
- Prompts user with suggestions if valuable content found
- Synchronizes the memory search index
Configuration:
# Disable Stop hook
export HOOK_STOP_ENABLED=false
# Disable uncaptured content prompts
export HOOK_STOP_PROMPT_UNCAPTURED=false
# Disable index sync on session end
export HOOK_STOP_SYNC_INDEX=false# Master switch - disable all hooks
export HOOK_ENABLED=false
# Enable debug logging to stderr
export HOOK_DEBUG=true
# Hook timeout in seconds (default: 30)
export HOOK_TIMEOUT=30The hooks are installed automatically when you configure the plugin in Claude Code. The hook scripts are in the hooks/ directory:
hooks/sessionstart.py- SessionStart event handlerhooks/userpromptsubmit.py- UserPromptSubmit event handlerhooks/posttooluse.py- PostToolUse event handlerhooks/precompact.py- PreCompact event handlerhooks/stop.py- Stop event handlerhooks/hooks.json- Hook registration configuration
You can capture memories inline in your prompts using special markers:
[remember] pytest fixtures with scope="module" persist across tests
[capture] We decided to use PostgreSQL for JSONB support
@memory The API rate limit is 1000 requests per minute
These markers are processed by the UserPromptSubmit hook when enabled.
Hook not triggering:
- Check if hooks are enabled:
echo $HOOK_ENABLED - Verify hook registration in
hooks/hooks.json - Enable debug mode:
export HOOK_DEBUG=true
Slow session start:
- Reduce budget:
export HOOK_SESSION_START_BUDGET_MODE=minimal - Check index size with
/memory:status - Run incremental reindex:
/memory:sync
Too many capture suggestions:
- Increase confidence threshold:
export HOOK_CAPTURE_DETECTION_MIN_CONFIDENCE=0.8 - Disable auto-capture:
export HOOK_CAPTURE_DETECTION_AUTO_THRESHOLD=1.0 - Disable hook entirely:
export HOOK_USER_PROMPT_ENABLED=false
This section covers how to explore, audit, and debug your memory system.
Quick status check:
# Plugin command
/memory:status --verbose
# Python
from git_notes_memory import get_sync_service
sync = get_sync_service()
stats = sync.get_stats()
print(f"Total memories: {stats.total_count}")
print(f"Namespaces: {stats.by_namespace}")List memories by namespace:
from git_notes_memory import get_recall_service
recall = get_recall_service()
# Get all decisions
decisions = recall.get_by_namespace("decisions")
for mem in decisions:
print(f"{mem.id}: {mem.summary}")
# Get all blockers (to check unresolved issues)
blockers = recall.get_by_namespace("blockers")
for mem in blockers:
status = "Resolved" if mem.status == "resolved" else "ACTIVE"
print(f"[{status}] {mem.summary}")Browse by spec/project:
# Get all memories for a specific spec
spec_memories = recall.get_by_spec("PROJ-001")
print(f"Found {len(spec_memories)} memories for PROJ-001")Semantic search (by meaning):
# Find decisions about databases without knowing exact keywords
results = recall.search("persistent storage choice")
# Find learnings about testing
results = recall.search("how to test async code", namespace="learnings")
# Find patterns applicable across projects
results = recall.search("error handling best practices", namespace="patterns")Text search (exact keywords):
# Find exact term matches
results = recall.search_text("PostgreSQL")
results = recall.search_text("pytest fixture")Combined strategies:
# First try semantic, then fall back to text
semantic = recall.search("database migration", namespace="decisions", k=5)
if not semantic:
text = recall.search_text("migration", limit=10)Check for duplicates:
from git_notes_memory import get_recall_service
recall = get_recall_service()
# Get all memories
all_memories = []
for ns in ["decisions", "learnings", "blockers", "patterns"]:
all_memories.extend(recall.get_by_namespace(ns))
# Check each for similar existing memories
for mem in all_memories:
similar = recall.search(mem.summary, k=3, min_similarity=0.8)
if len(similar) > 1: # More than self
print(f"Potential duplicates for: {mem.summary}")
for s in similar:
if s.memory.id != mem.id:
print(f" - [{s.similarity:.2f}] {s.memory.summary}")Audit memory content:
from git_notes_memory import HydrationLevel
# Full content review
memory = recall.get("decisions:abc1234:0")
hydrated = recall.hydrate(memory.id, level=HydrationLevel.FULL)
print(f"Summary: {hydrated.summary}")
print(f"Content:\n{hydrated.content}")
print(f"Tags: {hydrated.tags}")
print(f"Created: {hydrated.timestamp}")Check namespace distribution:
from collections import Counter
all_memories = []
for ns in recall.get_all_namespaces():
all_memories.extend(recall.get_by_namespace(ns))
distribution = Counter(m.namespace for m in all_memories)
print("Memory distribution:")
for ns, count in distribution.most_common():
print(f" {ns}: {count}")Enable debug logging:
# For hooks
export HOOK_DEBUG=true
# Check hook output in stderrVerify git notes storage:
# List all memory notes
git notes --ref=refs/notes/mem list
# View a specific note
git notes --ref=refs/notes/mem/decisions show HEAD
# Check notes refs exist
git for-each-ref refs/notes/memCheck index consistency:
from git_notes_memory import get_sync_service
sync = get_sync_service()
# Verify index matches git notes
result = sync.verify_consistency()
print(f"Consistent: {result.is_consistent}")
print(f"In git but not index: {len(result.missing_in_index)}")
print(f"In index but not git: {len(result.orphaned_in_index)}")
# Auto-repair if needed
if not result.is_consistent:
sync.repair(result)
print("Repaired!")Inspect raw git notes:
# See what's actually stored
git notes --ref=refs/notes/mem/decisions list | while read sha commit; do
echo "=== Note on $commit ==="
git notes --ref=refs/notes/mem/decisions show $commit
doneCheck embedding quality:
from git_notes_memory import get_embedding_service
embed = get_embedding_service()
# Test embedding generation
test_text = "PostgreSQL database migration"
embedding = embed.embed(test_text)
print(f"Embedding dimensions: {len(embedding)}")
print(f"First 5 values: {embedding[:5]}")
# Test similarity
text1 = "database schema migration"
text2 = "PostgreSQL table changes"
text3 = "completely unrelated topic"
e1, e2, e3 = embed.embed(text1), embed.embed(text2), embed.embed(text3)
def cosine_similarity(a, b):
import numpy as np
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))
print(f"Similarity (related): {cosine_similarity(e1, e2):.3f}")
print(f"Similarity (unrelated): {cosine_similarity(e1, e3):.3f}")Full reindex (rebuild search index):
from git_notes_memory import get_sync_service
sync = get_sync_service()
count = sync.reindex(full=True)
print(f"Reindexed {count} memories")Clean up orphaned index entries:
result = sync.verify_consistency()
if result.orphaned_in_index:
print(f"Removing {len(result.orphaned_in_index)} orphaned entries")
sync.repair(result)Export memories for backup:
import json
from git_notes_memory import get_recall_service, HydrationLevel
recall = get_recall_service()
export = []
for ns in ["decisions", "learnings", "blockers", "patterns", "progress"]:
for mem in recall.get_by_namespace(ns):
hydrated = recall.hydrate(mem.id, level=HydrationLevel.FULL)
export.append({
"id": mem.id,
"namespace": mem.namespace,
"summary": mem.summary,
"content": hydrated.content,
"tags": list(mem.tags),
"timestamp": mem.timestamp.isoformat(),
})
with open("memories_backup.json", "w") as f:
json.dump(export, f, indent=2)
print(f"Exported {len(export)} memories")Memory not appearing in search:
- Check if memory was captured:
git notes --ref=refs/notes/mem list - Verify it's indexed:
/memory:sync verify - Run reindex:
/memory:sync full - Check namespace filter in search
Duplicate memories:
- Check novelty threshold:
HOOK_CAPTURE_DETECTION_NOVELTY_THRESHOLD - Run duplicate detection script (above)
- Manually remove duplicates from git notes
Slow search performance:
- Check index size:
/memory:status - Reduce result count:
k=5instead ofk=20 - Add namespace filter to narrow scope
- Consider rebuilding index:
/memory:sync full
Hook not capturing:
- Verify hook enabled:
echo $HOOK_USER_PROMPT_ENABLED - Check confidence thresholds
- Enable debug:
export HOOK_DEBUG=true - Review debug output in stderr
- Check if memories are indexed:
sync.verify_consistency() - Run full reindex:
sync.reindex(full=True) - Verify git notes exist:
git notes --ref=mem list
The embedding model (~90MB) downloads on first use. After that, it's cached locally.
Another process may be capturing. Wait or delete .capture.lock manually.
Run a full reindex to rebuild:
from git_notes_memory import get_sync_service
sync = get_sync_service()
sync.reindex(full=True)Git notes require explicit push/pull:
git push origin refs/notes/mem
git fetch origin refs/notes/mem:refs/notes/mem- See Developer Guide for API reference
- Check CHANGELOG for version history