EverMem Plugin for Claude Code
April 13, 2026 ยท View on GitHub
Persistent memory for Claude Code. Automatically saves and recalls context from past coding sessions.

Features
- Automatic Memory Save - Conversations are saved when Claude finishes responding
- Automatic Memory Retrieval - Relevant memories are retrieved when you submit a prompt
- Session Context - Recent work summary loaded on session start
- Memory Search - Manually search your memory history
- Memory Hub - Visual dashboard to explore and manage memories
Quick Install
curl -fsSL https://raw.githubusercontent.com/EverMind-AI/evermem-claude-code/main/install.sh | bash
This will:
- Prompt for your EverMem API key
- Save it to your shell profile
- Install the plugin via Claude Code's plugin system
Get your API key: console.evermind.ai
Manual Installation
1. Get Your API Key
Visit console.evermind.ai to create an account and get your API key.
2. Configure Environment Variable
Add to your shell profile (~/.zshrc or ~/.bashrc):
export EVERMEM_API_KEY="your-api-key-here"
Reload your shell:
source ~/.zshrc # or source ~/.bashrc
3. Install the Plugin
# Add marketplace from GitHub (tracks updates automatically)
claude plugin marketplace add https://github.com/EverMind-AI/evermem-claude-code
# Install the plugin
claude plugin install evermem@evermem --scope user
To update the plugin later:
claude plugin marketplace update evermem
claude plugin update evermem@evermem
4. Verify Installation
Run /evermem:help to check if the plugin is configured correctly.
Usage
Commands
| Command | Description |
|---|---|
/evermem:help | Show setup status and available commands |
/evermem:search <query> | Search your memories for specific topics |
/evermem:ask <question> | Ask about past work (combines memory + context) |
/evermem:hub | Open the Memory Hub dashboard |
/evermem:debug | View debug logs for troubleshooting |
/evermem:projects | View your Claude Code projects table |
Automatic Behavior
The plugin works automatically in the background:
On Session Start:
๐ก EverMem: Last session (2h ago): "Implementing JWT authentication..." | 3 memories
Recent memories and last session summary are loaded to provide context.
On Prompt Submit:
You: "How should I handle authentication?"
โ
๐ Memory Retrieved (2):
โข [0.85] (2 days ago) Discussion about JWT token implementation
โข [0.72] (1 week ago) Auth middleware setup decisions
โ
Claude receives the relevant context and responds accordingly
On Response Complete:
๐พ EverMem: Memory saved (4 messages)
Memory Hub
The Memory Hub provides a visual interface to explore your memories:
- Activity heatmap (GitHub-style, 6 months)
- Memory statistics (Total, Projects, Active Days, Avg/Day, Avg/Project)
- Last 7 Days growth chart
- Project-based memory grouping with expandable cards
- Timeline view within each project (grouped by date)
- Load more pagination for large projects
To use the hub, run /evermem:hub and follow the instructions.
Configuration
Environment Variables
| Variable | Description | Required |
|---|---|---|
EVERMEM_API_KEY | Your EverMem API key | Yes |
Project-Specific Settings
Create .claude/evermem.local.md in your project root for per-project configuration:
---
group_id: "my-project"
---
Project-specific notes here.
Troubleshooting
API Key Not Configured
# Check if the key is set
echo $EVERMEM_API_KEY
# If empty, add to your shell profile and reload
export EVERMEM_API_KEY="your-key-here"
source ~/.zshrc
No Memories Found
- Memories are only recalled after you've had previous conversations
- Short prompts (less than 3 words) are skipped
- Check that your API key is valid at console.evermind.ai
API Errors
- 403 Forbidden: Invalid or expired API key
- 502 Bad Gateway: Server temporarily unavailable, try again
Debug Mode
Enable debug logging to troubleshoot issues:
# Set environment variable
export EVERMEM_DEBUG=1
# View logs in real-time
tail -f /tmp/evermem-debug.log
# Clear logs
> /tmp/evermem-debug.log
Run /evermem:debug to view recent debug logs directly.
Links
- Console: console.evermind.ai
- API Documentation: docs.evermind.ai
- Issues: GitHub Issues
License
MIT
Technical Details
The following sections explain how EverMem works internally. This is useful for developers who want to understand the implementation or contribute to the project.
How It Works
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Session Start โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SessionStart Hook โ
โ โข Fetches recent memories from EverMem Cloud โ
โ โข Loads last session summary from local storage โ
โ โข Injects session context into Claude's prompt โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Your Prompt โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ UserPromptSubmit Hook โ
โ โข Searches EverMem Cloud for relevant memories โ
โ โข Displays memory summary to user โ
โ โข Injects context into Claude's prompt โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Claude Response โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Stop Hook โ
โ โข Extracts conversation from transcript โ
โ โข Sends to EverMem Cloud for storage โ
โ โข Server generates summary and stores memory โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Session End โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SessionEnd Hook โ
โ โข Parses transcript to extract first user prompt โ
โ โข Saves session summary to local storage โ
โ โข No AI calls - pure local data extraction โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Claude Code Hooks Mechanism
Reference: Claude Code Hooks Documentation
Claude Code provides a hooks system that allows plugins to execute custom scripts at specific lifecycle events. Hooks are event-driven - they don't run continuously but are triggered by Claude Code at specific moments.
How Hooks Work
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Claude Code (Main Process) โ
โ โ
โ 1. Event occurs (e.g., user sends message, Claude responds) โ
โ 2. Claude Code reads hooks.json โ
โ 3. Finds matching hooks for the event โ
โ 4. Spawns child process: node <script.js> โ
โ 5. Sends JSON data via stdin pipe โโโโโโโโโโโโโโ โ
โ 6. Reads response from stdout โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Hook Script (Child Process) โ
โ โ
โ // Read JSON from stdin (sent by Claude Code) โ
โ let input = ''; โ
โ for await (const chunk of process.stdin) { โ
โ input += chunk; โ
โ } โ
โ const hookInput = JSON.parse(input); โ
โ โ
โ // Process and return result via stdout โ
โ console.log(JSON.stringify({ ... })); โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Hook Events
| Event | Trigger | Use Case |
|---|---|---|
SessionStart | Claude Code starts | Load context, setup environment |
UserPromptSubmit | User sends a message | Validate prompt, inject context |
PreToolUse | Before tool execution | Approve/deny/modify tool calls |
PostToolUse | After tool execution | Validate results, run linters |
Stop | Claude finishes responding | Save conversation, cleanup |
Notification | System notification | Custom alerts |
Plugin hooks.json Configuration
{
"hooks": {
"EventName": [
{
"matcher": "*", // Pattern to match (for tool events)
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/my-hook.js",
"timeout": 30 // Timeout in seconds
}
]
}
]
}
}
Environment Variables:
${CLAUDE_PLUGIN_ROOT}- Plugin directory path (for plugins)${CLAUDE_PROJECT_DIR}- Project root directory
EverMem Plugin Hooks
{
"hooks": {
"SessionStart": [...], // Load session context + track groups locally
"UserPromptSubmit": [...], // Search & inject memories
"Stop": [...], // Save conversation to cloud
"SessionEnd": [...] // Save session summary locally
}
}
SessionStart Hook
The SessionStart hook runs when Claude Code starts a new session. It loads recent memories from the cloud and last session summary from local storage.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Claude Code Session Start โ
โ โ
โ 1. Claude Code spawns: session-context-wrapper.sh โ
โ 2. Wrapper checks npm dependencies โ
โ 3. Wrapper executes: node session-context.js โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ session-context.js โ
โ โ
โ 1. Read hook input from stdin (contains cwd) โ
โ 2. Save group to local storage (groups.jsonl) โ
โ 3. Fetch recent memories from EverMem API (limit: 100) โ
โ 4. Take the 5 most recent memories โ
โ 5. Get last session summary from sessions.jsonl โ
โ 6. Output systemMessage + systemPrompt via stdout โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Claude Code Receives โ
โ โ
โ โข systemMessage: "๐ก EverMem: Last session (2h ago): \"...\" | 5 memories"โ
โ โข systemPrompt: <session-context>...</session-context> โ
โ โ
โ The systemPrompt is injected into Claude's context window โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Hook Input (stdin)
{
"session_id": "<session-uuid>",
"cwd": "/path/to/your/project",
"permission_mode": "default",
"hook_event_name": "SessionStart"
}
Hook Output (stdout)
{
"continue": true,
"systemMessage": "๐ก EverMem: Last session (2h ago): \"Implementing JWT authentication...\" | 5 memories",
"systemPrompt": "<session-context>\nLast session (2h ago, 5 turns): Implementing JWT authentication for the API\n\nRecent memories (5):\n\n[1] (2/9/2026) JWT token implementation\n...\n</session-context>"
}
Output Fields
| Field | Description |
|---|---|
continue | Always true - never block session start |
systemMessage | Displayed to user in terminal |
systemPrompt | Injected into Claude's context (invisible to user) |
Data Sources
The hook combines two data sources:
- Cloud Memories - Recent memories from EverMem API (5 most recent)
- Local Session Summary - Last session from
data/sessions.jsonl(saved by SessionEnd hook)
No AI summarization is used - pure local data extraction for zero latency and no additional API costs.
Error Handling
| Error Type | User Message |
|---|---|
| Network error | "Cannot reach EverMem server. Check your internet connection." |
| Timeout | "EverMem server is slow or unreachable." |
| 401/Unauthorized | "Authentication failed. Check your EVERMEM_API_KEY." |
| 404 | "API endpoint not found. Check EVERMEM_BASE_URL." |
| Module not found | "Missing dependency. Run: npm install" |
All errors return continue: true to ensure session starts normally.
Node.js Version Check
The hook requires Node.js 18+ for ES modules support. If an older version is detected:
{
"continue": true,
"systemMessage": "โ ๏ธ EverMem: Node.js 16.x is too old. Please upgrade to Node.js 18+."
}
SessionEnd Hook
The SessionEnd hook runs when a Claude Code session ends. It saves a session summary to local storage for use by the SessionStart hook.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Claude Code Session End โ
โ โ
โ Triggers: /exit, closing terminal, idle timeout โ
โ Claude Code spawns: node session-summary.js โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ session-summary.js โ
โ โ
โ 1. Read hook input from stdin (contains transcript_path) โ
โ 2. Check if session already summarized (skip if yes) โ
โ 3. Parse transcript JSONL file โ
โ 4. Extract: first user prompt, turn count, timestamps โ
โ 5. Save to data/sessions.jsonl โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Local Storage โ
โ โ
โ data/sessions.jsonl: โ
โ {"sessionId":"abc","groupId":"...","summary":"First user โ
โ prompt truncated to 200 chars","turnCount":5,...} โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Hook Input (stdin)
{
"session_id": "<session-uuid>",
"transcript_path": "~/.claude/projects/<hash>/<session-uuid>.jsonl",
"cwd": "/path/to/your/project",
"reason": "user_exit",
"hook_event_name": "SessionEnd"
}
Hook Output (stdout)
{
"systemMessage": "๐ Session saved (5 turns): Implementing JWT authentication for the..."
}
Session Summary Format
Each session is saved as a single line in data/sessions.jsonl:
{
"sessionId": "<session-uuid>",
"groupId": "claude-code:/path/to/project",
"summary": "First user prompt truncated to 200 characters",
"turnCount": 5,
"reason": "user_exit",
"startTime": "2026-02-09T10:00:00.000Z",
"endTime": "2026-02-09T10:30:00.000Z",
"timestamp": "2026-02-09T10:30:05.000Z"
}
Fields
| Field | Description |
|---|---|
sessionId | Unique session identifier (from Claude Code) |
groupId | Project identifier (based on working directory) |
summary | First user prompt (truncated to 200 chars) |
turnCount | Number of conversation turns |
reason | Why session ended (user_exit, idle_timeout, etc.) |
startTime | First message timestamp |
endTime | Last message timestamp |
timestamp | When summary was saved |
Deduplication
Each session is only saved once. Before saving, the hook checks if the sessionId already exists in sessions.jsonl.
No AI Summarization
The SessionEnd hook uses a simple approach: the first user prompt becomes the session summary. This provides:
- Zero latency - No API calls needed
- Zero cost - No Haiku or other model usage
- Reliability - Works offline, no external dependencies
The first user prompt typically describes what the user wanted to accomplish, making it a natural summary of the session's purpose.
Design Philosophy: Deferred Display Pattern
The SessionEnd and SessionStart hooks work together using a "save now, display later" pattern:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Session A (ending) โ
โ โ
โ SessionEnd Hook: โ
โ โข Extracts first user prompt, turn count, duration โ
โ โข Saves to sessions.jsonl โ
โ โข Output NOT displayed (session already closed) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โ sessions.jsonl (local storage)
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Session B (starting) โ
โ โ
โ SessionStart Hook: โ
โ โข Reads last session from sessions.jsonl โ
โ โข Displays: "Last (2h ago, 5 turns): Your question..." โ
โ โข Provides continuity across sessions โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Why this design?
-
SessionEnd can't display messages - When a session ends (
/exit,Ctrl+D), the terminal is closing. AnysystemMessageoutput would be lost or not visible to the user. -
SessionStart is the right moment - The next time the user opens Claude Code, they see what they were working on. This creates a natural "welcome back" experience.
-
Local-first architecture - Session summaries are stored locally in
sessions.jsonl, not in the cloud. This ensures:- Instant access (no API latency)
- Works offline
- No additional API costs
- Privacy (session data stays on your machine)
-
Graceful degradation - If SessionEnd fails to run (e.g.,
Ctrl+Cforce quit), the next SessionStart still works with cloud memories. No single point of failure.
Data Flow Summary:
| Event | Action | Storage | Display |
|---|---|---|---|
| SessionEnd | Save summary | Local (sessions.jsonl) | None |
| SessionStart | Read summary | Local + Cloud | Yes |
Local Groups Tracking
The SessionStart hook automatically records project groups to data/groups.jsonl (JSONL format):
{"keyId":"9a823d2f8ea5","groupId":"claude-code:/path/to/project-a","name":"project-a","path":"/path/to/project-a","timestamp":"2026-02-09T06:00:00Z"}
{"keyId":"9a823d2f8ea5","groupId":"claude-code:/path/to/api-server","name":"api-server","path":"/path/to/api-server","timestamp":"2026-02-09T08:00:00Z"}
Fields:
keyId: SHA-256 hash (first 12 chars) of the API key - associates groups with accountsgroupId: Unique identifier based on working directory, format:claude-code:{path}name: Project folder namepath: Full path to the projecttimestamp: When the group was first recorded
Deduplication: Each keyId + groupId combination is stored only once (no duplicates).
View tracked projects with /evermem:projects command.
Stop Hook: Conversation Flow
Claude Code stores all conversations locally in JSONL (JSON Lines) format. The EverMem plugin reads this transcript and uploads the latest Q&A pair to the cloud.
Hook Input
When Claude finishes responding, the Stop hook receives input like this:
{
"session_id": "<session-uuid>",
"transcript_path": "~/.claude/projects/<project-hash>/<session-uuid>.jsonl",
"cwd": "/path/to/your/project",
"permission_mode": "default",
"hook_event_name": "Stop",
"stop_hook_active": false
}
Transcript File Format
The transcript file (*.jsonl) contains one JSON object per line, recording every message and event in the session. Important: A single Claude response may span multiple lines with different content types.
Common Fields:
| Field | Description |
|---|---|
type | Line type: user, assistant, progress, system, file-history-snapshot |
uuid | Unique message ID |
parentUuid | Parent message ID (for threading) |
timestamp | ISO 8601 timestamp |
sessionId | Session UUID |
message.role | user or assistant |
message.content | String or array of content blocks |
Content Block Types (in message.content array):
| Type | Description |
|---|---|
text | Final text response to user |
thinking | Claude's internal reasoning (extended thinking) |
tool_use | Tool invocation (Read, Write, Bash, etc.) |
tool_result | Result returned from tool execution |
Complete Conversation Example:
A single Q&A turn generates multiple JSONL lines:
// 1. User message
{"type":"user","message":{"role":"user","content":"debug.js ๅฆไฝไฝฟ็จ"},"uuid":"696034a3-...","timestamp":"2026-02-09T02:20:16.540Z"}
// 2. Assistant thinking (extended thinking mode)
{"type":"assistant","message":{"role":"assistant","content":[{"type":"thinking","thinking":"็จๆทๅธๆไบ่งฃ debug.js ็ไฝฟ็จๆนๆณ...","signature":"EuAC..."}]},"uuid":"b375ff09-...","timestamp":"2026-02-09T02:20:26.866Z"}
// 3. Assistant tool use (e.g., Read file)
{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_01Qur8BnkKD9t53JSSorDLbm","name":"Read","input":{"file_path":"/path/to/README.md"}}]},"uuid":"f01ec15c-..."}
// 4. Progress event (hook execution)
{"type":"progress","data":{"type":"hook_progress","hookEvent":"PostToolUse","hookName":"PostToolUse:Read"},"uuid":"f4219b83-..."}
// 5. Tool result (returned as user message)
{"type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Qur8BnkKD9t53JSSorDLbm","type":"tool_result","content":"file contents here..."}]},"uuid":"f5c5f7c6-..."}
// 6. Assistant final text response
{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"ๅฎๆ๏ผREADME ๅทฒๆดๆฐ..."}]},"uuid":"cae1b79c-..."}
// 7. System events (stop hook, timing)
{"type":"system","subtype":"stop_hook_summary","hookCount":1,"hasOutput":true,"uuid":"25a25edf-..."}
{"type":"system","subtype":"turn_duration","durationMs":81371,"uuid":"55418b2c-..."}
Simplified View:
User Input
โ
[thinking] โ [tool_use] โ [tool_result] โ [tool_use] โ ... โ [text]
โ
System Events (hooks, timing)
Turn Boundary & Segmentation
Session Level: One JSONL file = One Session (filename is session ID)
Turn Level: A "Turn" = User sends message โ Claude fully responds
Turn boundary marker (ONLY this one):
{"type":"system","subtype":"turn_duration","durationMs":30692}
Note:
file-history-snapshotis NOT a turn boundary. It's a session-level marker that can appear anywhere in the file.
JSONL Structure:
Line 1: file-history-snapshot โ Session marker (NOT turn boundary)
Line 2-21: Turn 1
Line 22: turn_duration โ Turn 1 end โ
Line 23: file-history-snapshot โ Can appear mid-session (NOT turn boundary)
Line 24-43: Turn 2
Line 44: turn_duration โ Turn 2 end โ
...
Message Chain (parentUuid):
user (uuid: aaa, parent: None) โ Turn start
โ
assistant/thinking (parent: aaa)
โ
assistant/tool_use (parent: ...)
โ
user/tool_result (parent: ...) โ NOT user input, skip!
โ
assistant/text (parent: ...) โ Final response
โ
system/turn_duration (parent: ...) โ Turn end
Memory Extraction
The store-memories.js hook extracts the last complete Turn:
- Wait for completion - Retry reading file until
turn_durationmarker appears (indicates turn is complete) - Find turn boundaries - Start after last
turn_duration, end at currentturn_duration- ONLY
turn_durationis used as boundary (NOTfile-history-snapshot)
- ONLY
- Collect user text - Original input only (skip
tool_result) - Collect assistant text - All
textblocks (skipthinking,tool_use) - Merge content - Join scattered text blocks with
\n\nseparator - Upload to cloud - Send both user and assistant content to EverMem API
Race Condition Handling:
The Stop hook runs before turn_duration is written. To ensure complete content extraction:
// Retry until turn_duration appears (max 5 attempts, 100ms delay)
async function readTranscriptWithRetry(path) {
for (let attempt = 1; attempt <= 5; attempt++) {
const lines = readFile(path);
const lastLine = JSON.parse(lines[lines.length - 1]);
// turn_duration = turn complete
if (lastLine.type === 'system' && lastLine.subtype === 'turn_duration') {
return lines;
}
await sleep(100); // Wait and retry
}
}
Why merge? A single Claude response spans multiple JSONL lines:
thinkingโtool_useโtool_resultโ ... โtext(final response)
The hook merges all text blocks to capture the complete response.
API Upload
Each message is sent to POST /api/v1/memories/group (or POST /api/v1/memories for personal memories):
{
"group_id": "claude-code:/path/to/project",
"messages": [
{
"sender_id": "claude-code-user",
"role": "user",
"timestamp": 1770367656189,
"content": "How do I add authentication?"
}
],
"async_mode": true
}
Response on success:
{
"message": "Message accepted and queued for processing",
"request_id": "<request-uuid>",
"status": "queued"
}
Hook Output (stdout)
The hook returns JSON via stdout to communicate with Claude Code:
{
"systemMessage": "๐พ Memory saved (2) [user: 59, assistant: 127]"
}
This message is displayed to the user after Claude finishes responding.
Memory Hub Implementation
The /evermem:hub command opens a web dashboard for visualizing memories. Due to browser limitations (GET requests can't have body), a local proxy server bridges the dashboard and EverMem API.
Architecture
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ /evermem:hub Command โ
โ 1. Start proxy server: node server/proxy.js & โ
โ 2. Generate URL: http://localhost:3456/?key=${EVERMEM_API_KEY} โ
โ 3. User opens URL in browser โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Browser (dashboard.html) โ
โ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ
โ โ Stats โ โ Heatmap โ โ 7-Day โ โ Project โ โ
โ โ Cards โ โ (6 months) โ โ Chart โ โ Cards โ โ
โ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โโโโโโโโโโโโโโโ โ
โ โ
โ Data Flow: โ
โ 1. GET /api/groups โ Local groups.jsonl (filtered by keyId) โ
โ 2. For each group: POST /api/v1/memories/get โ Fetch memories โ
โ 3. Render dashboard with aggregated data โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Proxy Server (localhost:3456) โ
โ โ
โ Routes: โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ GET / โ Serve dashboard.html โ โ
โ โ GET /api/groups โ Read groups.jsonl, filter by keyId โ โ
โ โ POST /api/v1/memories/search โ Forward to EverMind API โ โ
โ โ POST /api/v1/memories/get โ Forward to EverMind API โ โ
โ โ GET /health โ Health check โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ Why Proxy? โ
โ - The proxy forwards browser calls to the EverMind API and serves the โ
โ dashboard. โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ EverMem Cloud API โ
โ https://api.evermind.ai โ
โ โ
โ POST /api/v1/memories/search { query, filters, method, memory_types, top_k } โ
โ POST /api/v1/memories/get { memory_type, filters, page, page_size, ... } โ
โ Response: { data: { episodes[], total_count, count } } โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Proxy Server (server/proxy.js)
// Key function: Convert API key to keyId (for groups filtering)
function computeKeyId(apiKey) {
const hash = createHash('sha256').update(apiKey).digest('hex');
return hash.substring(0, 12); // First 12 chars of SHA-256
}
// Key function: Read groups.jsonl and filter by keyId
function getGroupsForKey(keyId) {
const content = readFileSync(GROUPS_FILE, 'utf8');
const lines = content.trim().split('\n');
const groupMap = new Map();
for (const line of lines) {
const entry = JSON.parse(line);
if (entry.keyId !== keyId) continue; // Filter by current API key
// Aggregate: count sessions, track first/last seen
// ...
}
return Array.from(groupMap.values());
}
// Key route: Forward POST requests to EverMind API
// Browser sends: POST /api/v1/memories/search { query, filters, ... }
// Browser sends: POST /api/v1/memories/get { memory_type, filters, ... }
// Proxy forwards to upstream API with Authorization header
Dashboard (assets/dashboard.html)
Data Loading Flow:
async function loadGroups() {
// 1. Fetch groups from local storage (via proxy)
const groupsData = await fetch('/api/groups', {
headers: { 'Authorization': `Bearer ${apiKey}` }
});
// 2. For each group, fetch memories with pagination
for (const group of groups) {
const data = await fetch('/api/v1/memories/get', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
memory_type: 'episodic_memory',
filters: { group_id: group.id },
page: 1,
page_size: 100,
rank_by: 'timestamp',
rank_order: 'desc'
})
});
// Store: memories[], totalCount, hasMore, offset
groupMemories[group.id] = { ... };
}
// 3. Render dashboard
renderDashboard(totalMemories);
}
UI Components:
| Component | Description |
|---|---|
| Stats Grid | 5 cards: Total Memories, Projects, Active Days, Avg/Day, Avg/Project |
| Heatmap | GitHub-style 6-month activity grid with tooltips |
| Growth Chart | Last 7 days bar chart |
| Project Cards | Expandable cards showing memories per project |
| Timeline | Within each project, memories grouped by date |
| Load More | Pagination button when has_more: true |
Timeline within Project:
๐ evermem-claude-code (25 memories)
โโโ โ Sun, Feb 9 [Today] 3 memories
โ โโโ ๐ญ Discussion about JWT... 10:30 AM
โ โโโ ๐ง Fixed authentication... 09:15 AM
โ โโโ โจ Created new API endpoint 08:00 AM
โ
โโโ โ Sat, Feb 8 5 memories
โ โโโ ๐ Updated README... 16:20 PM
โ โโโ ...
โ
โโโ [Load more (17 remaining)]
Debug Logging
Both inject-memories.js and store-memories.js use a shared debug utility:
import { debug, setDebugPrefix } from './utils/debug.js';
setDebugPrefix('inject'); // Log lines will show [inject] prefix
debug('hookInput:', data); // Only writes when EVERMEM_DEBUG=1
Debug output by script:
| Script | Prefix | Debug Points |
|---|---|---|
inject-memories.js | [inject] | hookInput, search query, search results, filtered/selected memories, output |
store-memories.js | [store] | hookInput, read attempts, turn range, line types, extracted content, results |
Example debug log:
# Memory injection (UserPromptSubmit hook)
[2026-02-06T08:47:30.100Z] [inject] hookInput: { "prompt": "How do I add auth?", ... }
[2026-02-06T08:47:30.150Z] [inject] searching memories for prompt: How do I add auth?
[2026-02-06T08:47:30.500Z] [inject] search results: {"total": 5, "memories": [...]}
[2026-02-06T08:47:30.520Z] [inject] selected memories: [{"score": 0.85, "subject": "JWT implementation"}]
# Memory storage (Stop hook)
[2026-02-06T08:47:36.184Z] [store] hookInput: { "transcript_path": "...jsonl", ... }
# Retry logic - waiting for turn_duration
[2026-02-06T08:47:36.200Z] [store] read attempt 1: { "totalLines": 525, "isComplete": false, "lastLineType": "progress" }
[2026-02-06T08:47:36.201Z] [store] turn not complete, waiting 100ms before retry...
[2026-02-06T08:47:36.310Z] [store] read attempt 2: { "totalLines": 527, "isComplete": false, "lastLineType": "system/stop_hook_summary" }
[2026-02-06T08:47:36.311Z] [store] turn not complete, waiting 100ms before retry...
[2026-02-06T08:47:36.420Z] [store] read attempt 3: { "totalLines": 528, "isComplete": true, "lastLineType": "system/turn_duration" }
# Content extraction
[2026-02-06T08:47:36.425Z] [store] turn range: { "turnStartIndex": 500, "turnEndIndex": 528, "totalLines": 528 }
[2026-02-06T08:47:36.430Z] [store] assistantTexts count: 3
[2026-02-06T08:47:36.435Z] [store] extracted: { "userLength": 59, "assistantLength": 847, ... }
# API upload results
[2026-02-06T08:47:36.970Z] [store] results: [
{
"type": "USER",
"len": 59,
"status": 202,
"ok": true,
"response": {
"message": "Message accepted and queued for processing",
"status": "queued"
}
},
{
"type": "ASSISTANT",
"len": 127,
"status": 202,
"ok": true,
"response": { ... }
}
]
[2026-02-06T08:47:36.975Z] [store] skipped: []
Using debug.js in your own hooks:
import { debug, setDebugPrefix, isDebugEnabled } from './utils/debug.js';
// Set prefix to identify your script in logs
setDebugPrefix('my-hook');
// Log objects (auto JSON stringified) or strings
debug('processing:', { key: 'value' });
// Check if debug is enabled
if (isDebugEnabled()) {
// expensive debug operations
}
Project Structure
evermem-plugin/
โโโ plugin.json # Plugin manifest
โโโ commands/
โ โโโ help.md # /evermem:help command
โ โโโ search.md # /evermem:search command
โ โโโ hub.md # /evermem:hub command
โ โโโ debug.md # /evermem:debug command
โ โโโ projects.md # /evermem:projects command
โโโ data/
โ โโโ groups.jsonl # Local storage for tracked projects (JSONL format)
โ โโโ sessions.jsonl # Local storage for session summaries (JSONL format)
โโโ hooks/
โ โโโ hooks.json # Hook configuration
โ โโโ scripts/
โ โโโ inject-memories.js # Memory recall (UserPromptSubmit)
โ โโโ store-memories.js # Memory save (Stop)
โ โโโ session-context.js # Session context (SessionStart)
โ โโโ session-summary.js # Session summary (SessionEnd)
โ โโโ utils/
โ โโโ evermem-api.js # EverMem Cloud API client
โ โโโ config.js # Configuration utilities
โ โโโ debug.js # Shared debug logging utility
โ โโโ groups-store.js # Local groups persistence
โโโ assets/
โ โโโ dashboard.html # Memory Hub dashboard
โโโ server/
โ โโโ proxy.js # Local proxy server for dashboard
โโโ README.md
API Reference
The plugin uses the EverMem Cloud API at https://api.evermind.ai:
POST /api/v1/memories/group- Store a new memory (group);POST /api/v1/memoriesfor personal memoriesPOST /api/v1/memories/search- Search memories (hybrid retrieval, with JSON body)POST /api/v1/memories/get- Get memories (list with JSON body)
Development
Local Development
# Clone the repository
git clone https://github.com/EverMind-AI/evermem-claude-code.git
cd evermem-claude-code
# Install dependencies
npm install
# Run Claude Code with local plugin
claude --plugin-dir .
Testing Hooks
# Test memory recall
echo '{"prompt":"How do I handle authentication?"}' | node hooks/scripts/inject-memories.js
# Test memory save (requires transcript file)
echo '{"transcript_path":"/path/to/transcript.json"}' | node hooks/scripts/store-memories.js