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
2542 skills6 hooks3 MCP
Install
$ npx -y skills add varun29ankuS/shodh-memory --agent claude-code

Ships with shodh-memory. Installing the plugin gets these hooks.

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.

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.

Get the whole plugin
Stats
273
Stars
37
Forks
Active
Maintenance
Rust
Language
Apache-2.0
License
54m ago
Last commit
8mo ago
Created

Repo: varun29ankuS/shodh-memory