Install
$ agentstack add skill-arturseo-geo-claude-code-skills-memory-persistence ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Memory Persistence Skill
Memory Architecture Overview
Claude Code has no built-in cross-session memory — every new session starts blank. There are six patterns to solve this, ranging from simple to powerful:
| Pattern | Best for | Complexity | |---|---|---| | CLAUDE.md injection | Small, stable facts | Low | | Session summary files | Resuming complex work | Low | | Append-only JSONL | Evolving agent knowledge | Medium | | Graph-based memory | Relationship-rich knowledge | Medium | | Hooks (stop/pre/post) | Auto-saving and auto-loading | Medium | | MCP memory servers | Production multi-user agents | High |
Pattern 1: CLAUDE.md Injection (Always-On Facts)
CLAUDE.md is Claude Code's built-in memory layer. It loads automatically at session start, making it the simplest persistence mechanism. There are three levels:
| File | Scope | Loads when | |---|---|---| | ~/.claude/CLAUDE.md | All projects (global) | Every session | | CLAUDE.md (project root) | This project only | When in project dir | | .claude/CLAUDE.md | This project (hidden) | When in project dir |
All three merge at session start. Use the right level for each fact.
Example CLAUDE.md structure:
## Project Context
- Stack: Next.js 14, Supabase, Tailwind, Vercel
- Auth: Clerk (not NextAuth)
- Primary branch: main (never commit directly)
- Test command: pnpm test:unit
- Deployment: auto on push to main via Vercel
## Key Decisions
- We use Zod for all validation, not Yup
- All API routes live in /app/api, never /pages/api
- Error handling pattern: see /lib/errors.ts
CLAUDE.md Size Limits
Keep it under 150 lines — it loads every session, consuming tokens permanently. Move verbose details to reference files and @-import them only when needed.
| Content type | Put in CLAUDE.md? | Alternative | |---|---|---| | Stack, tools, test commands | Yes | — | | Architectural decisions ( list[dict]: memories = [] with open(path) as f: next(f) # skip schema line for line in f: m = json.loads(line) if types and m["type"] not in types: continue if m.get("importance", 1.0) float: """Score = importance recencydecay""" now = now or datetime.now(timezone.utc) ts = datetime.fromisoformat(memory["ts"].replace("Z", "+00:00")) agedays = (now - ts).totalseconds() / 86400 recency = math.exp(-0.01 agedays) # half-life ~69 days importance = memory.get("importance", 0.7) return importance * recency
**Retrieval strategy**: load all memories, score each, return top-N. This keeps
context lean while surfacing the most relevant knowledge.
---
## Pattern 4: Graph-Based Memory (Relationship-Rich Knowledge)
For complex projects where entities relate to each other — use a graph structure
stored as a JSON file. Faster context loading, fewer tokens, safer for refactoring.
```json
{
"nodes": {
"UserModel": {"type": "entity", "file": "lib/models/user.ts", "deps": ["AuthService", "DB"]},
"AuthService": {"type": "service", "file": "lib/auth.ts", "deps": ["Clerk", "UserModel"]},
"DB": {"type": "external", "url": "Supabase"}
},
"edges": [
{"from": "UserModel", "to": "AuthService", "rel": "used-by"},
{"from": "AuthService", "to": "Clerk", "rel": "delegates-auth-to"}
]
}
When to use: projects with 20+ modules, microservices, or complex dependency chains. Claude can query: "what depends on AuthService?" without loading all files.
Graph Queries
Common queries an agent can run against the graph:
| Query | How | |---|---| | What depends on X? | Filter edges where from == X | | What does X depend on? | Read nodes[X].deps | | Impact of changing X? | BFS from X along used-by edges | | Which services touch the DB? | Filter nodes where "DB" in deps |
Pattern 5: Hooks for Auto-Save and Auto-Load
Hooks let you automate memory operations so the user never has to remember to save.
Stop Hook — Auto-Save Session Learnings
A Stop hook runs at session end. It captures what Claude discovered and appends to memory automatically. Use Stop not UserPromptSubmit — runs once per session, not on every message, so no latency impact.
.claude/hooks/stop-memory.sh:
#!/bin/bash
# Runs at end of every session
# Asks Claude to extract learnings and append to JSONL
claude -p "Review this session. Extract any:
- Debugging patterns discovered
- Project-specific facts learned
- User preferences observed
- Decisions made
Format each as a JSON line:
{\"ts\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\", \"type\": \"[type]\", \"key\": \"[short_key]\", \"value\": \"[what to remember]\", \"context\": \"[brief context]\", \"importance\": [0.0-1.0]}
Append to .claude/memory.jsonl — do not overwrite."
Register in .claude/settings.json:
{
"hooks": {
"Stop": [{"command": "bash .claude/hooks/stop-memory.sh"}]
}
}
PreToolUse Hook — Auto-Load Relevant Memory
Load memory before specific tool calls to give Claude context exactly when needed:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"command": "bash .claude/hooks/load-memory.sh"
}
]
}
}
.claude/hooks/load-memory.sh:
#!/bin/bash
# Before editing files, surface relevant memories
# Read last 20 memories and output as context
tail -20 .claude/memory.jsonl 2>/dev/null || true
PostToolUse Hook — Capture Outcomes
After a tool runs, record what happened for future sessions:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"command": "bash .claude/hooks/record-outcome.sh"
}
]
}
}
Pattern 6: MCP Memory Servers
For production memory, connect a memory MCP server instead of flat files:
| Server | Storage | Best for | |---|---|---| | @modelcontextprotocol/memory | In-process key-value | Simple persistence | | Chroma MCP | Vector DB | Semantic search over memories | | SQLite MCP | Relational | Structured queries over memory |
Important: if a memory MCP is configured but no skill/hook actually writes to it, disable it — it consumes context window tokens with no benefit.
Memory Retrieval Strategies
When memory grows large, you need strategies to load only what matters:
| Strategy | How it works | Best for | |---|---|---| | Recency | Load last N entries | Fast-moving projects | | Type filter | Load only decision + pattern | Focused recall | | Importance threshold | Load entries with importance >= 0.7 | Mature memory stores | | Decay scoring | Score = importance * recency_decay | Balanced retrieval | | Semantic search | Vector similarity via MCP | Large memory stores (1000+) | | Key prefix | Filter by key prefix (auth_*) | Domain-specific queries |
Combining Strategies
For best results, combine multiple strategies:
def retrieve_relevant(path: str, query_types: list[str], top_n: int = 20) -> list[dict]:
all_memories = load_memories(path, types=query_types)
scored = [(m, score_memory(m)) for m in all_memories]
scored.sort(key=lambda x: x[1], reverse=True)
return [m for m, s in scored[:top_n]]
Quick Reference — Which Pattern to Use?
Need to remember forever? -> CLAUDE.md (if Session summary file
Agent that learns over time? -> Append-only JSONL + Stop hook
Complex codebase relationships? -> Graph-based JSON
Auto-save without user action? -> Stop hook + JSONL
Production multi-user agent? -> Memory MCP server
Surviving context compaction? -> Write to disk early and often
Pattern 7: Groq-Powered Memory Processing (Zero Claude Token Cost)
Use Groq's free tier to handle memory operations that would otherwise consume Claude tokens: scoring, compaction, summarisation, and relevance ranking.
Why offload memory ops to Groq?
Memory operations are repetitive, low-reasoning tasks — perfect for Llama 3.1 8B. Scoring 500 memories on Claude Sonnet costs ~$2. On Groq: $0.
Groq setup
pip install groq
export GROQ_API_KEY="gsk_..." # Free at console.groq.com
Memory scoring with Groq
Instead of loading all memories into Claude's context, score them on Groq first and feed only the top-N:
import os, json, time
from groq import Groq
groq_client = Groq(api_key=os.environ["GROQ_API_KEY"])
def score_memories_groq(memories: list[dict], context: str,
top_n: int = 20) -> list[dict]:
"""Score memory relevance using Groq free tier, return top-N."""
scored = []
batch_size = 15 # Send 15 per request to reduce RPM usage
for i in range(0, len(memories), batch_size):
batch = memories[i:i+batch_size]
batch_text = "\n".join([
f"{j+1}. [{m['type']}] {m['key']}: {m['value']}"
for j, m in enumerate(batch)
])
resp = groq_client.chat.completions.create(
model="llama-3.1-8b-instant",
messages=[{
"role": "user",
"content": f"""Rate each memory's relevance to this context (0.0-1.0).
Return ONLY a JSON array of numbers.
Context: {context}
Memories:
{batch_text}
Scores:"""
}],
temperature=0,
max_tokens=200
)
try:
scores = json.loads(resp.choices[0].message.content)
if isinstance(scores, dict):
scores = list(scores.values())[0]
for m, s in zip(batch, scores):
scored.append((m, float(s)))
except:
for m in batch:
scored.append((m, 0.5))
time.sleep(2) # Respect 30 RPM limit
scored.sort(key=lambda x: x[1], reverse=True)
return [m for m, s in scored[:top_n]]
# Usage: score 500 memories for free, feed top 20 to Claude
all_memories = load_memories(".claude/memory.jsonl")
relevant = score_memories_groq(all_memories, "debugging auth webhook")
# Now pass only 20 memories to Claude instead of 500
Memory summarisation with Groq
Compress old memories into summaries to reduce JSONL file size:
def summarise_old_memories_groq(memories: list[dict],
group_size: int = 20) -> list[str]:
"""Summarise groups of old memories into compact paragraphs."""
summaries = []
for i in range(0, len(memories), group_size):
group = memories[i:i+group_size]
group_text = "\n".join([
f"- [{m['type']}] {m['key']}: {m['value']}"
for m in group
])
resp = groq_client.chat.completions.create(
model="llama-3.1-8b-instant",
messages=[{
"role": "user",
"content": f"Summarise these memories into 3-5 bullet points, preserving the most important facts:\n\n{group_text}"
}],
temperature=0.1,
max_tokens=300
)
summaries.append(resp.choices[0].message.content.strip())
time.sleep(2)
return summaries
Memory deduplication with Groq
Find and merge duplicate memories without burning Claude tokens:
def find_duplicates_groq(memories: list[dict]) -> list[list[int]]:
"""Identify duplicate/near-duplicate memories using Groq."""
batch_text = "\n".join([
f"{i+1}. [{m['type']}] {m['key']}: {m['value']}"
for i, m in enumerate(memories)
])
resp = groq_client.chat.completions.create(
model="llama-3.1-8b-instant",
messages=[{
"role": "user",
"content": f"""Find groups of duplicate or near-duplicate memories.
Return JSON array of arrays, e.g. [[1,5], [3,7,12]].
If no duplicates, return [].
Memories:
{batch_text[:4000]}
Duplicate groups:"""
}],
temperature=0,
max_tokens=500
)
try:
return json.loads(resp.choices[0].message.content)
except:
return []
Haiku for real-time memory retrieval
For in-session memory operations that need to stay inside Claude Code, use Haiku subagents instead of the main model:
{
"env": {
"CLAUDE_CODE_SUBAGENT_MODEL": "haiku"
}
}
Then delegate memory tasks to subagents:
Use a subagent to: read .claude/memory.jsonl, find all memories
related to "authentication", and return the top 5 most relevant.
Haiku costs 4% of Opus — memory retrieval via subagent is nearly free.
Combined workflow: Groq + Haiku + Claude
| Memory operation | Route to | Cost | |---|---|---| | Score 500 memories for relevance | Groq | $0.00 | | Summarise old memories (bulk) | Groq | $0.00 | | Find and merge duplicates | Groq | $0.00 | | Load top-20 into session context | Haiku subagent | ~$0.001 | | Use memories for reasoning | Sonnet/Opus (main) | Normal | | Write new memories (Stop hook) | Haiku subagent | ~$0.001 |
Result: memory operations that cost $2-5/day on Claude now cost <$0.01/day.
See references/groq-memory.md for complete integration scripts.
Anti-Patterns to Avoid
| Anti-pattern | Why it fails | Fix | |---|---|---| | CLAUDE.md over 300 lines | Wastes tokens every session | Router pattern + reference files | | Overwriting memory files | Loses history, breaks diffs | Append-only JSONL | | Memory in chat only | Lost on compaction or new session | Write to disk | | No importance scoring | All memories treated equal | Add importance field | | Hook on every message | Latency on each prompt | Use Stop hook (once per session) | | MCP server with no writes | Token cost, zero benefit | Disable unused MCP tools |
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: arturseo-geo
- Source: arturseo-geo/claude-code-skills
- License: MIT
- Homepage: https://thegeolab.net
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.