Skip to content
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.

From plugin
claude-mem-sync
271 skill2 hooks
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

  • Matchesmcp__plugin_claude-mem_mcp-search__.*bun ${CLAUDE_PLUGIN_ROOT}/dist/hooks/post-tool-use.js
Read hooks/hooks.json

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