Skip to content
Agent Memory
Hook

Hooks

What shodh-memory runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
shodh-memory
3162 skills6 hooks3 MCP

Claude Code

$ npx -y skills add varun29ankuS/shodh-memory --agent claude-code

Where it lives

  • hooks/claude-code-ingest.shGitHub
    Read the script
    #!/bin/bash
    # Shodh-Memory Auto-Ingest Hook for Claude Code
    #
    # This hook runs after each Claude Code response to automatically
    # store conversations in shodh-memory for persistent learning.
    #
    # Installation:
    #   1. Copy this file to ~/.claude/hooks/
    #   2. Add to ~/.claude/settings.json:
    #      {
    #        "hooks": {
    #          "Stop": [{
    #            "matcher": "",
    #            "hooks": [{
    #              "type": "command",
    #              "command": "bash ~/.claude/hooks/claude-code-ingest.sh"
    #            }]
    #          }]
    #        }
    #      }
    #   3. Ensure shodh-memory-server is running on localhost:3030
    #
    # Environment Variables:
    #   SHODH_API_URL - API endpoint (default: http://127.0.0.1:3030)
    #   SHODH_API_KEY - API key (default: dev key)
    #   SHODH_USER_ID - User ID for memory isolation (default: claude-code)
    
    set -e
    
    API_URL="${SHODH_API_URL:-http://127.0.0.1:3030}"
    USER_ID="${SHODH_USER_ID:-claude-code}"
    
    # API Key - env, else the shared key file persisted by the MCP server
    # (<data-root>/.api-key, see mcp-server/api-key-store.ts). No hardcoded fallback.
    if [ -z "$SHODH_API_KEY" ]; then
        for KEY_FILE in \
            ${SHODH_MEMORY_PATH:+"$SHODH_MEMORY_PATH/.api-key"} \
            "${XDG_DATA_HOME:-$HOME/.local/share}/shodh-memory/.api-key" \
            "$HOME/Library/Application Support/shodh-memory/.api-key" \
            ${APPDATA:+"$APPDATA/shodh-memory/.api-key"}; do
            if [ -f "$KEY_FILE" ]; then
                SHODH_API_KEY=$(tr -d '[:space:]' < "$KEY_FILE")
                if [ -n "$SHODH_API_KEY" ]; then break; fi
            fi
        done
    fi
    if [ -z "$SHODH_API_KEY" ]; then
        echo "ERROR: SHODH_API_KEY not set and no shared key file found (connect the shodh-memory MCP server once to create it)" >&2
        exit 1
    fi
    API_KEY="$SHODH_API_KEY"
    
    # Read hook input from stdin
    INPUT=$(cat)
    
    # Extract transcript path from hook input
    TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // empty')
    
    if [ -z "$TRANSCRIPT_PATH" ] || [ ! -f "$TRANSCRIPT_PATH" ]; then
        exit 0
    fi
    
    # Extract the last exchange (user message + assistant response)
    LAST_MESSAGES=$(jq -c '.[-2:]' "$TRANSCRIPT_PATH" 2>/dev/null || echo "[]")
    
    if [ "$LAST_MESSAGES" = "[]" ] || [ "$LAST_MESSAGES" = "null" ]; then
        exit 0
    fi
    
    # Format conversation content - extract text from both user and assistant messages
    CONTENT=$(echo "$LAST_MESSAGES" | jq -r '
      map(
        if .role == "user" then
          "User: " + (.content | if type == "array" then map(select(.type == "text") | .text) | join("\n") else tostring end)
        elif .role == "assistant" then
          "Assistant: " + (.content | if type == "array" then map(select(.type == "text") | .text) | join("\n") else tostring end)
        else
          empty
        end
      ) | join("\n\n")
    ' 2>/dev/null || echo "")
    
    # Skip if content is empty or too short (< 50 chars = likely noise)
    if [ -z "$CONTENT" ] || [ ${#CONTENT} -lt 50 ]; then
        exit 0
    fi
    
    # Truncate if too long (max 4000 chars for a single memory)
    if [ ${#CONTENT} -gt 4000 ]; then
        CONTENT="${CONTENT:0:4000}..."
    fi
    
    # Escape content for JSON
    CONTENT_ESCAPED=$(echo "$CONTENT" | jq -Rs '.')
    
    # Extract project context from working directory
    CWD=$(echo "$INPUT" | jq -r '.cwd // "unknown"')
    PROJECT=$(basename "$CWD")
    
    # Send to shodh-memory API (fire and forget, don't block Claude)
    curl -s -X POST "$API_URL/api/record" \
        -H "Content-Type: application/json" \
        -H "X-API-Key: $API_KEY" \
        --connect-timeout 2 \
        --max-time 5 \
        -d "{
            \"user_id\": \"$USER_ID\",
            \"experience\": {
                \"content\": $CONTENT_ESCAPED,
                \"experience_type\": \"Conversation\",
                \"tags\": [\"claude-code\", \"auto-ingest\", \"$PROJECT\"]
            }
        }" > /dev/null 2>&1 || true
    
    exit 0
    
  • hooks/memory-hook.test.tsGitHub
    Read the script
    import { describe, expect, it } from "bun:test";
    import {
      buildPreToolContext,
      formatRelativeTime,
      formatMemoriesForContext,
      isErrorOutput,
      describeKeyOrigin,
      reportAuthFailure,
    } from "./memory-hook";
    
    describe("formatRelativeTime", () => {
      it("returns today for current date", () => {
        const nowIso = new Date().toISOString();
        expect(formatRelativeTime(nowIso)).toBe("today");
      });
    
      it("returns yesterday for one day old date", () => {
        const d = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
        expect(formatRelativeTime(d)).toBe("yesterday");
      });
    
      it("returns Xd ago for dates under a week", () => {
        const d = new Date(Date.now() - 3 * 24 * 60 * 60 * 1000).toISOString();
        expect(formatRelativeTime(d)).toBe("3d ago");
      });
    
      it("returns calendar date for older memories", () => {
        const d = new Date(Date.now() - 10 * 24 * 60 * 60 * 1000).toISOString();
        const value = formatRelativeTime(d);
        expect(value).not.toContain("ago");
        expect(value).not.toBe("today");
        expect(value).not.toBe("yesterday");
      });
    });
    
    describe("formatMemoriesForContext", () => {
      const memory = (over: Partial<Record<string, unknown>> = {}) => ({
        id: "m1",
        content: "Remember to review deployment checklist before release",
        memory_type: "Task",
        score: 0.83,
        importance: 0.7,
        created_at: new Date().toISOString(),
        tags: ["deploy"],
        relevance_reason: "matches query",
        matched_entities: ["release"],
        ...over,
      });
    
      // Returns SurfaceResult | null, not a string: callers need `meta` for the
      // surfacing decision, and null is how "nothing worth surfacing" is expressed.
      it("returns null for empty input", () => {
        expect(formatMemoriesForContext([])).toBeNull();
      });
    
      it("returns null when the best score is under the noise floor", () => {
        expect(formatMemoriesForContext([memory({ score: 0.01 })])).toBeNull();
      });
    
      // Displayed percentages are normalised across the set, so a lone memory is
      // always 100% — its raw score survives on meta.bestScore.
      it("normalises the displayed score and keeps the raw one in meta", () => {
        const out = formatMemoriesForContext([memory()]);
        expect(out).not.toBeNull();
        expect(out!.text).toContain("100% match");
        expect(out!.text).toContain("today");
        expect(out!.text).toContain("Remember to review deployment checklist");
        expect(out!.meta.bestScore).toBe(83);
        expect(out!.meta.count).toBe(1);
      });
    
      it("spreads the set across the normalised range", () => {
        const now = new Date().toISOString();
        const out = formatMemoriesForContext([
          memory({ id: "m1", content: "A", score: 0.2, created_at: now }),
          memory({ id: "m2", content: "B", score: 0.3, created_at: now }),
        ]);
        expect(out).not.toBeNull();
        expect((out!.text.match(/\u2022/g) || []).length).toBe(2);
        expect(out!.text).toContain("0% match");
        expect(out!.text).toContain("100% match");
      });
    
      it("truncates long content at 120 chars with an ellipsis", () => {
        const out = formatMemoriesForContext([memory({ content: "x".repeat(130) })]);
        expect(out).not.toBeNull();
        expect(out!.text).toContain("...");
        expect(out!.text).toContain("x".repeat(120));
        expect(out!.text).not.toContain("x".repeat(121));
      });
    });
    
    describe("buildPreToolContext", () => {
      it("builds edit context", () => {
        expect(buildPreToolContext("Edit", { file_path: "src/main.ts" })).toBe("Editing file: src/main.ts");
      });
    
      it("builds write context from the same branch", () => {
        expect(buildPreToolContext("Write", { file_path: "src/new.ts" })).toBe("Editing file: src/new.ts");
      });
    
      // Bash deliberately has no branch of its own. handlePreToolUse returns before
      // calling this for anything but Edit/Write (see the guard in that handler),
      // and routing a raw shell command through here would send it to the memory
      // server — commands carry credentials often enough that the narrow surface
      // is the point, not an oversight.
      it("falls through to the generic form for tools it no longer specialises", () => {
        expect(buildPreToolContext("Bash", { command: "curl -H 'X-API-Key: sk-live-abc' https://api" }))
          .toBe("About to use Bash");
        expect(buildPreToolContext("Read", {})).toBe("About to use Read");
      });
    
      it("falls back when the expected input field is absent", () => {
        expect(buildPreToolContext("Edit", {})).toBe("About to use Edit");
      });
    });
    
    describe("isErrorOutput", () => {
      it("detects the error shapes it targets", () => {
        expect(isErrorOutput("error[E0308]: mismatched types")).toBe(true);
        expect(isErrorOutput("Operation FAILED quickly")).toBe(true);
        expect(isErrorOutput("thread 'main' panicked at src/lib.rs:4")).toBe(true);
        expect(isErrorOutput("fatal: not a git repository")).toBe(true);
        expect(isErrorOutput("process exited with exit code 1")).toBe(true);
        expect(isErrorOutput("bash: foo: command not found")).toBe(true);
      });
    
      // Cargo, npm and tsc all prefix diagnostics with a lowercase "error:", so a
      // case-sensitive match missed the most common failure line in this repo.
      // Only this pattern is case-insensitive — see the trade pinned below.
      it("detects lowercase error: diagnostics", () => {
        expect(isErrorOutput("error: could not compile `shodh-memory`")).toBe(true);
        expect(isErrorOutput("Error: connect ECONNREFUSED")).toBe(true);
      });
    
      it("returns false on clean output", () => {
        expect(isErrorOutput("Command completed successfully")).toBe(false);
      });
    
      // The matching is case-sensitive on FAILED by design. Relaxing it would make
      // "0 tests failed" — an ordinary green-build line — read as a failure, and a
      // false positive here mislabels a successful run in memory, which is worse
      // than missing one. These cases pin that trade so it is not relaxed by accident.
      it("does not fire on clean lines that merely contain the words", () => {
        expect(isErrorOutput("0 tests failed")).toBe(false
  • hooks/memory-hook.tsGitHub
    Read the script
    #!/usr/bin/env bun
    /**
     * Shodh Memory Hook — Native Claude Code Integration
     *
     * Aggressive proactive context surfacing at every opportunity.
     * Memory is woven into every interaction — the AI thinks with memory.
     *
     * Events: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, SubagentStop, Stop
     *
     * Architecture: Each hook event spawns a new Bun process. All state is persisted
     * to a temp file keyed by session_id, loaded at process start, saved on exit.
     */
    
    // ---------------------------------------------------------------------------
    // 1. Config Constants
    // ---------------------------------------------------------------------------
    
    const SHODH_API_URL = process.env.SHODH_API_URL || "http://127.0.0.1:3030";
    
    /**
     * API key resolution — MUST match the MCP shim's persistence scheme
     * (mcp-server/api-key-store.ts; this file ships standalone and cannot import
     * it). The auto-spawned server only accepts the key the shim generated, which
     * the shim persists to `<data-root>/.api-key`. Resolution order:
     *   1. SHODH_API_KEY env (explicit)
     *   2. `<data-root>/.api-key` — the shared key persisted by the MCP shim
     *      (data root = SHODH_MEMORY_PATH, else the platform data dir, mirroring
     *      the Rust server's default_storage_path in src/config.rs)
     *   3. Legacy dev key — only correct when the user started the server
     *      manually with that key; SessionStart verifies auth and fails LOUDLY
     *      instead of letting capture die silently on a 401.
     */
    interface ResolvedApiKey {
      key: string;
      source: "env" | "shared-key-file" | "legacy-dev-fallback";
      file?: string;
    }
    
    /**
     * Where the key came from, for the auth-failure report.
     *
     * Derived from the discriminant alone. The resolved path lives on
     * RESOLVED_API_KEY.file and is deliberately not read here: printing it puts the
     * operator's home directory into stderr and into the user-visible
     * systemMessage, and the fix lines below name the remedy without it.
     */
    export function describeKeyOrigin(source: ResolvedApiKey["source"]): string {
      switch (source) {
        case "env":
          return "the SHODH_API_KEY environment variable";
        case "shared-key-file":
          return "the shared key file in the shodh-memory data directory";
        case "legacy-dev-fallback":
          return "the legacy dev key (no SHODH_API_KEY set and no shared key file found)";
        default: {
          // Unreachable for the declared union — adding a variant without a case
          // above fails this assignment at compile time. The return keeps the
          // message sane if a value ever arrives from outside the type system.
          const unhandled: never = source;
          void unhandled;
          return "an unrecognised key source";
        }
      }
    }
    
    function resolveApiKey(): ResolvedApiKey {
      if (process.env.SHODH_API_KEY) {
        return { key: process.env.SHODH_API_KEY, source: "env" };
      }
      const fs = require("fs");
      const path = require("path");
      const os = require("os");
    
      const roots: string[] = [];
      if (process.env.SHODH_MEMORY_PATH) {
        roots.push(process.env.SHODH_MEMORY_PATH);
      }
      const home = os.homedir();
      if (process.platform === "win32") {
        roots.push(path.join(process.env.APPDATA || path.join(home, "AppData", "Roaming"), "shodh-memory"));
      } else if (process.platform === "darwin") {
        roots.push(path.join(home, "Library", "Application Support", "shodh-memory"));
      } else {
        roots.push(path.join(process.env.XDG_DATA_HOME || path.join(home, ".local", "share"), "shodh-memory"));
      }
    
      for (const root of roots) {
        const file = path.join(root, ".api-key");
        try {
          const raw = fs.readFileSync(file, "utf-8").trim();
          if (raw) return { key: raw, source: "shared-key-file", file };
        } catch {
          // File absent or unreadable — try the next candidate.
        }
      }
      return { key: "sk-shodh-dev-local-testing-key", source: "legacy-dev-fallback" };
    }
    
    const RESOLVED_API_KEY = resolveApiKey();
    const SHODH_API_KEY = RESOLVED_API_KEY.key;
    const SHODH_USER_ID = process.env.SHODH_USER_ID || "claude-code";
    const HOOK_TIMEOUT_MS = 5000;
    const CIRCUIT_BREAKER_THRESHOLD = 3;
    
    // ---------------------------------------------------------------------------
    // 2. Type Interfaces
    // ---------------------------------------------------------------------------
    
    interface HookInput {
      hook_event_name: string;
      session_id?: string;
      transcript_path?: string;
      cwd?: string;
      // UserPromptSubmit
      prompt?: string;
      // PreToolUse / PostToolUse
      tool_name?: string;
      tool_input?: Record<string, unknown>;
      tool_output?: string;
      tool_response?: unknown;
      // Stop
      stop_reason?: string;
      // SubagentStop
      agent_id?: string;
      agent_type?: string;
      agent_transcript_path?: string;
      // Legacy field names (backward compat)
      subagent_type?: string;
      subagent_result?: string;
    }
    
    interface SurfacedMemory {
      id: string;
      content: string;
      memory_type: string;
      score: number;
      importance: number;
      created_at: string;
      tags: string[];
      relevance_reason: string;
      matched_entities: string[];
    }
    
    interface ProactiveContextResponse {
      memories: SurfacedMemory[];
      due_reminders: unknown[];
      context_reminders: unknown[];
      memory_count: number;
      reminder_count: number;
      ingested_memory_id: string | null;
      feedback_processed: { memories_evaluated: number; reinforced: string[]; weakened: string[] } | null;
      relevant_todos: { id: string; short_id: string; content: string; status: string; priority: string; project: string | null; due_date: string | null; relevance_reason: string }[];
      todo_count: number;
      relevant_facts: { id: string; fact: string; confidence: number; support_count: number; related_entities: string[] }[];
      latency_ms: number;
      detected_entities: { name: string; entity_type: string }[];
    }
    
    interface ToolAction {
      tool_name: string;
      inputs: Record<string, string>;
      success: boolean;
      output_snippet?: string;
    }
    
    interface RememberResponse {
      id?: string;
      memory_id?: string;
    }
    
    interface SurfaceMetadata {
      count: number;
     
  • hooks/session-start.shGitHub
    Read the script
    #!/bin/bash
    # Shodh Memory - Session Start Hook
    # Loads proactive context at session start
    
    SHODH_API_URL="${SHODH_API_URL:-http://127.0.0.1:3030}"
    SHODH_USER_ID="${SHODH_USER_ID:-claude-code}"
    
    # Resolve API key: env > shared key file persisted by the MCP server > legacy dev key.
    # The shared file lives at <data-root>/.api-key (see mcp-server/api-key-store.ts);
    # the data root mirrors the Rust server's default_storage_path (src/config.rs).
    if [ -z "$SHODH_API_KEY" ]; then
        for KEY_FILE in \
            ${SHODH_MEMORY_PATH:+"$SHODH_MEMORY_PATH/.api-key"} \
            "${XDG_DATA_HOME:-$HOME/.local/share}/shodh-memory/.api-key" \
            "$HOME/Library/Application Support/shodh-memory/.api-key" \
            ${APPDATA:+"$APPDATA/shodh-memory/.api-key"}; do
            if [ -f "$KEY_FILE" ]; then
                SHODH_API_KEY=$(tr -d '[:space:]' < "$KEY_FILE")
                if [ -n "$SHODH_API_KEY" ]; then break; fi
            fi
        done
    fi
    # Last resort: legacy dev key (only valid when the server was started with it).
    # A wrong key is reported loudly below instead of failing silently.
    SHODH_API_KEY="${SHODH_API_KEY:-sk-shodh-dev-local-testing-key}"
    
    # Get project directory for context
    PROJECT_DIR="${CLAUDE_PROJECT_DIR:-.}"
    PROJECT_NAME=$(basename "$PROJECT_DIR")
    
    # Build context from recent git activity and current directory
    CONTEXT="Working in: $PROJECT_NAME"
    if [ -d "$PROJECT_DIR/.git" ]; then
        RECENT_FILES=$(cd "$PROJECT_DIR" && git diff --name-only HEAD~5 2>/dev/null | head -10 | tr '\n' ', ')
        if [ -n "$RECENT_FILES" ]; then
            CONTEXT="$CONTEXT. Recently modified: $RECENT_FILES"
        fi
    fi
    
    # Query proactive context from brain, capturing the HTTP status so an auth
    # failure is reported LOUDLY instead of memory dying silently for the session.
    RESPONSE_FILE=$(mktemp)
    trap 'rm -f "$RESPONSE_FILE"' EXIT
    HTTP_CODE=$(curl -s -o "$RESPONSE_FILE" -w "%{http_code}" -X POST "$SHODH_API_URL/api/proactive_context" \
        -H "Content-Type: application/json" \
        -H "X-API-Key: $SHODH_API_KEY" \
        -d "{
            \"user_id\": \"$SHODH_USER_ID\",
            \"context\": \"$CONTEXT\",
            \"max_results\": 5,
            \"auto_ingest\": false
        }" 2>/dev/null)
    
    if [ "$HTTP_CODE" = "401" ] || [ "$HTTP_CODE" = "403" ]; then
        echo "[shodh] ============================================================" >&2
        echo "[shodh] MEMORY CAPTURE DISABLED - server rejected API key ($HTTP_CODE)" >&2
        echo "[shodh] Set SHODH_API_KEY to the key the server was started with, or" >&2
        echo "[shodh] connect the shodh-memory MCP server once so it persists the" >&2
        echo "[shodh] shared key file (<data-dir>/.api-key) that hooks read." >&2
        echo "[shodh] ============================================================" >&2
        echo "{\"systemMessage\": \"shodh-memory: capture DISABLED - the memory server rejected the hook's API key ($HTTP_CODE). Set SHODH_API_KEY or connect the MCP server once to create the shared key file.\"}"
        exit 0
    fi
    
    RESPONSE=$(cat "$RESPONSE_FILE")
    
    # Extract memories if response is valid
    MEMORIES=$(echo "$RESPONSE" | jq -r '.memories[]? | "- [\(.memory_type)] \(.content | .[0:200])"' 2>/dev/null)
    
    if [ -n "$MEMORIES" ] && [ "$MEMORIES" != "null" ]; then
        # Write to CLAUDE.local.md for automatic injection
        cat > "$PROJECT_DIR/.claude/memory-context.md" << EOF
    # Proactive Memory Context
    
    The following memories from past sessions may be relevant:
    
    $MEMORIES
    
    Use these to maintain continuity. If they conflict with current instructions, prioritize current.
    EOF
        echo "Loaded $(echo "$MEMORIES" | wc -l) memories from brain"
    fi
    
  • hooks/stop.shGitHub
    Read the script
    #!/bin/bash
    # Shodh Memory - Stop Hook
    # Stores the interaction when Claude finishes responding
    
    SHODH_API_URL="${SHODH_API_URL:-http://127.0.0.1:3030}"
    SHODH_USER_ID="${SHODH_USER_ID:-claude-code}"
    
    # Resolve API key: env > shared key file persisted by the MCP server > legacy dev key.
    # (Shared file: <data-root>/.api-key, see mcp-server/api-key-store.ts.)
    if [ -z "$SHODH_API_KEY" ]; then
        for KEY_FILE in \
            ${SHODH_MEMORY_PATH:+"$SHODH_MEMORY_PATH/.api-key"} \
            "${XDG_DATA_HOME:-$HOME/.local/share}/shodh-memory/.api-key" \
            "$HOME/Library/Application Support/shodh-memory/.api-key" \
            ${APPDATA:+"$APPDATA/shodh-memory/.api-key"}; do
            if [ -f "$KEY_FILE" ]; then
                SHODH_API_KEY=$(tr -d '[:space:]' < "$KEY_FILE")
                if [ -n "$SHODH_API_KEY" ]; then break; fi
            fi
        done
    fi
    SHODH_API_KEY="${SHODH_API_KEY:-sk-shodh-dev-local-testing-key}"
    
    # Read hook input from stdin (JSON with stop_hook_active, etc.)
    INPUT=$(cat)
    
    # Extract relevant fields
    STOP_REASON=$(echo "$INPUT" | jq -r '.stop_reason // "end_turn"')
    
    # Get the transcript if available (from environment or temp file)
    TRANSCRIPT_FILE="${CLAUDE_TRANSCRIPT_FILE:-}"
    
    if [ -n "$TRANSCRIPT_FILE" ] && [ -f "$TRANSCRIPT_FILE" ]; then
        # Parse last exchange from transcript
        LAST_USER=$(tail -100 "$TRANSCRIPT_FILE" | grep -A 50 '"role": "user"' | head -50)
        LAST_ASSISTANT=$(tail -100 "$TRANSCRIPT_FILE" | grep -A 50 '"role": "assistant"' | head -50)
    
        if [ -n "$LAST_USER" ] && [ -n "$LAST_ASSISTANT" ]; then
            # Store to brain
            CONTENT="User: $(echo "$LAST_USER" | jq -r '.content' 2>/dev/null | head -c 500)
    Assistant: $(echo "$LAST_ASSISTANT" | jq -r '.content' 2>/dev/null | head -c 1000)"
    
            curl -s -X POST "$SHODH_API_URL/api/remember" \
                -H "Content-Type: application/json" \
                -H "X-API-Key: $SHODH_API_KEY" \
                -d "{
                    \"user_id\": \"$SHODH_USER_ID\",
                    \"content\": $(echo "$CONTENT" | jq -Rs .),
                    \"memory_type\": \"Conversation\",
                    \"tags\": [\"source:hook\", \"stop:$STOP_REASON\"]
                }" > /dev/null 2>&1
        fi
    fi
    
    # Always exit successfully - don't block Claude
    exit 0
    
  • hooks/user-prompt.shGitHub
    Read the script
    #!/bin/bash
    # Shodh Memory - User Prompt Submit Hook
    # Enriches context based on what user is asking
    
    SHODH_API_URL="${SHODH_API_URL:-http://127.0.0.1:3030}"
    SHODH_USER_ID="${SHODH_USER_ID:-claude-code}"
    
    # Resolve API key: env > shared key file persisted by the MCP server > legacy dev key.
    # (Shared file: <data-root>/.api-key, see mcp-server/api-key-store.ts.)
    if [ -z "$SHODH_API_KEY" ]; then
        for KEY_FILE in \
            ${SHODH_MEMORY_PATH:+"$SHODH_MEMORY_PATH/.api-key"} \
            "${XDG_DATA_HOME:-$HOME/.local/share}/shodh-memory/.api-key" \
            "$HOME/Library/Application Support/shodh-memory/.api-key" \
            ${APPDATA:+"$APPDATA/shodh-memory/.api-key"}; do
            if [ -f "$KEY_FILE" ]; then
                SHODH_API_KEY=$(tr -d '[:space:]' < "$KEY_FILE")
                if [ -n "$SHODH_API_KEY" ]; then break; fi
            fi
        done
    fi
    SHODH_API_KEY="${SHODH_API_KEY:-sk-shodh-dev-local-testing-key}"
    
    # Read hook input from stdin
    INPUT=$(cat)
    
    # Extract the prompt
    PROMPT=$(echo "$INPUT" | jq -r '.prompt // ""')
    
    # Skip if empty or very short
    if [ ${#PROMPT} -lt 10 ]; then
        exit 0
    fi
    
    # Query brain for relevant memories based on this specific prompt
    RESPONSE=$(curl -s -X POST "$SHODH_API_URL/api/recall" \
        -H "Content-Type: application/json" \
        -H "X-API-Key: $SHODH_API_KEY" \
        -d "{
            \"user_id\": \"$SHODH_USER_ID\",
            \"query\": $(echo "$PROMPT" | head -c 500 | jq -Rs .),
            \"limit\": 3
        }" 2>/dev/null)
    
    # Extract memories
    MEMORIES=$(echo "$RESPONSE" | jq -r '.results[]? | "[\(.memory_type)] \(.content | .[0:150])..."' 2>/dev/null | head -3)
    
    if [ -n "$MEMORIES" ] && [ "$MEMORIES" != "null" ]; then
        # Output additional context (Claude Code will inject this)
        echo "{\"additionalContext\": \"Relevant memories: $MEMORIES\"}"
    fi
    
    exit 0
    

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.

@varun29ankus
Ships withshodh-memory

Local, LLM-free memory for AI agents. A single offline Rust binary — deterministic and auditable — that learns from use, forgets the irrelevant, and strengthens what matters. No cloud, no API keys.

3162 skills3 MCP
Get the whole plugin

Stars on GitHub

Momentum

stars in total
316
Star history of varun29ankuS/shodh-memory on GitHub, a chart by star-history.com

More plugins that ship hooks.

milla-jovovich-mempalace
Plugin

milla-jovovich-mempala…

Gives Claude Code a private memory that stays on your machine. It stores past conversations word for word, so you can ask what was decided, what happened before, or who was involved and get the original context back. It can also hand tasks to other agents.

3 skills+360k
@milla-jovovich@milla-jovovich
Install Plugin
mempalace
Plugin

mempalace

Local-first memory for Claude Code. It stores conversations verbatim in a private palace you can search before answering about past work, people, projects, or decisions, with optional shared-brain hub setup.

3 skills+360k
@mempalace@mempalace
Install Plugin
mksglu-context-mode-2
Plugin

mksglu-context-mode-2

Keeps large command outputs, logs and data files out of your AI assistant's context by running processing in a sandbox and returning only the relevant parts. You also get a searchable index of local files and diagnostics for checking the setup.

9 skills+233k
@mksglu@mksglu
Install Plugin
mksglu-context-mode
Plugin

mksglu-context-mode

Keeps large tool outputs, logs, and files out of Claude Code's context window by processing them in a sandbox and indexing them for focused search. Includes diagnostics and a knowledge base indexer.

9 skills+233k
@mksglu@mksglu
Install Plugin
ai-memory
Plugin

ai-memory

Gives coding agents a git-backed wiki for long-term project memory, with skills for retrieval, durable notes, session handoffs between agents, cross-project messaging, and memory consolidation.

7 skills+214k
@akitaonrails@akitaonrails
Install Plugin
letta-code
Plugin

letta-code

A coding agent harness whose agents keep memory and identity across sessions, so they learn and improve over time. You can run it in a terminal, in the browser, or through Telegram, Slack and Discord.

27 skills8 hooks3.7k
@letta-ai@letta-ai
Install Plugin
claude-subconscious
Plugin

claude-subconscious

A background agent for Claude Code that watches your sessions and reads your files, builds up memory over time, and offers guidance back during your work.

4 hooks2.9k
@letta-ai@letta-ai
Install Plugin
token-optimizer
Plugin

token-optimizer

Audits a Claude Code or Codex setup for context-window waste, fixes it with the user's approval, and measures the savings. It also tracks costs, helps resume prior sessions, and offers a usage dashboard.

5 skills+22.7k
@alexgreensh@alexgreensh
Install Plugin

Questions about the hooks in shodh-memory.

Which coding agents can use the hooks in shodh-memory?
The hooks in shodh-memory install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.