Development
Hook
Hooks
What claude-mem-sync runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.
Install
> /plugin marketplace add lopadova/claude-mem-sync > /plugin install claude-mem-sync@claude-mem-sync
Ships with claude-mem-sync. Installing the plugin gets these hooks.
What fires, and when
Setup
- Matches
*bun "${CLAUDE_PLUGIN_ROOT}/scripts/setup.js"
PostToolUse
- Matches
mcp__plugin_claude-mem_mcp-search__.*bun ${CLAUDE_PLUGIN_ROOT}/dist/hooks/post-tool-use.js
Where it lives
- hooks/post-tool-use.tsGitHub
Read the script
/** * PostToolUse hook for Claude Code. * * Reads hook payload from stdin, extracts observation IDs from the tool * response, and logs each access to access.db so the eviction scorer * can reward frequently-used observations. * * Safety: wrapped entirely in try/catch — never throws, never blocks Claude, * never writes to stdout (stdout goes back to Claude Code). */ import { openAccessDb, logAccess } from "../src/core/access-db"; import { loadConfig, getEnabledProjects } from "../src/core/config"; import { openMemDb, getObservationProjectMap } from "../src/core/mem-db"; import { LOGS_DIR, DEFAULT_CLAUDE_MEM_DB } from "../src/core/constants"; import { readAllStdin } from "../src/core/compat"; import { mkdirSync, appendFileSync } from "fs"; import { join } from "path"; // ── Types ────────────────────────────────────────────────────────────── interface McpContentBlock { type: string; text?: string; } interface HookInput { session_id?: string; cwd?: string; tool_name?: string; tool_input?: Record<string, unknown>; tool_response?: string | McpContentBlock[]; } // ── Helpers ──────────────────────────────────────────────────────────── function logError(msg: string, data?: unknown): void { try { mkdirSync(LOGS_DIR, { recursive: true }); const ts = new Date().toISOString(); const line = `[${ts}] [HOOK-ERROR] ${msg}${data ? " " + JSON.stringify(data) : ""}\n`; appendFileSync(join(LOGS_DIR, "hook-errors.log"), line); } catch { // absolutely nothing — we must not fail } } /** * Extract observation IDs from the tool response text. * * Strategies (in order): * 1. JSON-parse the response and walk for `"id": <number>` fields * 2. Regex fallback for `"id": 123`, `#123`, `id=123` patterns */ function extractObservationIds(response: string): number[] { const ids = new Set<number>(); // Strategy 1: parse as JSON (response may be a JSON string or JSON object) try { const parsed = JSON.parse(response); walkForIds(parsed, ids); } catch { // not valid JSON — fall through to regex } // Strategy 2: regex patterns const patterns = [ /"id"\s*:\s*(\d+)/g, /\bid[=:]\s*(\d+)/g, /#(\d+)\b/g, ]; for (const pattern of patterns) { let match: RegExpExecArray | null; while ((match = pattern.exec(response)) !== null) { const n = Number(match[1]); if (n > 0 && Number.isInteger(n)) { ids.add(n); } } } return [...ids]; } /** Recursively walk a parsed JSON value collecting numeric `id` fields. */ function walkForIds(value: unknown, ids: Set<number>): void { if (value === null || value === undefined) return; if (Array.isArray(value)) { for (const item of value) walkForIds(item, ids); return; } if (typeof value === "object") { const obj = value as Record<string, unknown>; if (typeof obj.id === "number" && Number.isInteger(obj.id) && obj.id > 0) { ids.add(obj.id); } for (const key of Object.keys(obj)) { walkForIds(obj[key], ids); } } } /** * Extract the project name from the MCP tool input or response. * claude-mem tools accept a `project` parameter and observations include a * `project` field — use these directly instead of guessing from cwd. */ function extractProjectFromInput( toolInput?: Record<string, unknown>, toolResponse?: string, ): string | null { // 1. Check tool_input.project (most reliable — explicitly passed by Claude) if (toolInput && typeof toolInput.project === "string" && toolInput.project) { return toolInput.project; } // 2. Check tool_response for a project field in the JSON payload if (toolResponse) { try { const parsed = JSON.parse(toolResponse); // Single observation response if (typeof parsed.project === "string" && parsed.project) { return parsed.project; } // Direct array of observations (e.g. get_observations returns [{...}]) const items = Array.isArray(parsed) ? parsed : (parsed.observations ?? parsed.results ?? parsed.data); if (Array.isArray(items)) { for (const item of items) { if (typeof item?.project === "string" && item.project) { return item.project; } } } } catch { // not JSON — skip } } return null; } /** * Normalize tool_response to a plain string. * Claude Code passes MCP responses as an array of content blocks: * [{ "type": "text", "text": "..." }] * We need to extract the text from these blocks. */ function normalizeToolResponse(response: string | McpContentBlock[] | undefined): string | undefined { if (!response) return undefined; if (typeof response === "string") return response; if (Array.isArray(response)) { const texts = response .filter((block) => block.type === "text" && typeof block.text === "string") .map((block) => block.text!); return texts.length > 0 ? texts.join("\n") : undefined; } return undefined; } /** * Determine the project name from `cwd` by matching against enabled projects * in the config. Heuristic: check if cwd path contains the project name or * the project's `memProject` value. */ function resolveProject(cwd: string): string | null { try { const config = loadConfig(); const enabled = getEnabledProjects(config); const normalizedCwd = cwd.replace(/\\/g, "/").toLowerCase(); for (const name of enabled) { const project = config.projects[name]; const memProject = project.memProject ?? name; if ( normalizedCwd.includes(name.toLowerCase()) || normalizedCwd.includes(memProject.toLowerCase()) ) { return memProject; } } // Fallback: return the first enabled project's memProject if only one exists if (enabled.length === 1) { const name = enabled[0]; return config.projects[name].memProject ?? name; } return null; } catch { return null; } } // ── Main ───────
Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.
Ships withclaude-mem-sync
Team memory sharing for claude-mem — sync AI memories across developers via git.
Get the whole plugin
Stats
27
Stars
4
Forks
Maintained
Maintenance
TypeScript
Language
MIT
License
3mo ago
Last commit
6mo ago
Created
Repo: lopadova/claude-mem-sync

