Skip to content
Productivity
Hook

Hooks

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

From plugin
vault-pkm
136 skills3 agents2 commands2 hooks
Install
$ npx -y skills add AdrianV101/obsidian-pkm-plugin --agent claude-code

Ships with vault-pkm. Installing the plugin gets these hooks.

What fires, and when

SessionStart

Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.

  • Matchesstartup|clear|compactVAULT_PATH=${VAULT_PATH} node ${CLAUDE_PLUGIN_ROOT}/hooks/session-start.js

PreToolUse

  • MatchesBash${CLAUDE_PLUGIN_ROOT}/hooks/pre-commit-reminder.sh
Read hooks/hooks.json

Where it lives

  • hooks/load-context.jsGitHub
    Read the script
    // IMPORTANT: Hooks run via raw `node` from the installed plugin directory,
    // which has no node_modules. All functions here MUST be self-contained —
    // do NOT import from ../helpers.js or ../utils.js (they depend on js-yaml).
    import fs from "fs/promises";
    import path from "path";
    
    /**
     * Extract YAML frontmatter as simple key-value pairs.
     * Lightweight parser — only handles scalar values (sufficient for status/priority).
     */
    function extractFrontmatter(content) {
      if (!content.startsWith("---")) return null;
      const endIndex = content.indexOf("\n---", 3);
      if (endIndex === -1) return null;
      const yamlContent = content.slice(4, endIndex);
      const result = {};
      for (const line of yamlContent.split("\n")) {
        const match = line.match(/^(\w+):\s*(.+)$/);
        if (match) result[match[1]] = match[2].trim();
      }
      return result;
    }
    
    function parseHeadingLevel(line) {
      const match = line.match(/^(#{1,6})\s/);
      return match ? match[1].length : 0;
    }
    
    /**
     * Extract the last N sections at a given heading level from markdown content.
     */
    function extractTailSections(content, n, level) {
      let frontmatter = "";
      let body = content;
      if (content.startsWith("---")) {
        const endIndex = content.indexOf("\n---", 3);
        if (endIndex !== -1) {
          frontmatter = content.slice(0, endIndex + 4);
          body = content.slice(endIndex + 4);
        }
      }
    
      const lines = body.split("\n");
      const headingPositions = [];
      let offset = 0;
      for (const line of lines) {
        if (parseHeadingLevel(line) === level) {
          headingPositions.push(offset);
        }
        offset += line.length + 1;
      }
    
      if (headingPositions.length === 0) {
        return content;
      }
    
      const startIdx = Math.max(0, headingPositions.length - n);
      const sliceStart = headingPositions[startIdx];
      const tail = body.slice(sliceStart);
    
      return frontmatter + (frontmatter && !frontmatter.endsWith("\n") ? "\n" : "") + tail;
    }
    
    export async function loadProjectContext(vaultPath, projectPath) {
      const projectName = path.basename(projectPath);
      const projectDir = path.join(vaultPath, projectPath);
      const sections = [];
      const meta = { index: false, devlog: false, tasks: 0 };
    
      sections.push(`## PKM Project Context: ${projectName}`);
    
      try {
        const indexContent = await fs.readFile(path.join(projectDir, "_index.md"), "utf-8");
        sections.push(`### Project Index\n${indexContent}`);
        meta.index = true;
      } catch (e) {
        if (e.code !== "ENOENT") console.error("PKM load-context: error reading _index.md:", e.message);
      }
    
      try {
        const devlogContent = await fs.readFile(
          path.join(projectDir, "development", "devlog.md"), "utf-8"
        );
        const sectionLevel = /^## Sessions\s*$/m.test(devlogContent) ? 3 : 2;
        const tailSections = extractTailSections(devlogContent, 3, sectionLevel);
        sections.push(`### Recent Development Activity\n${tailSections}`);
        meta.devlog = true;
      } catch (e) {
        if (e.code !== "ENOENT") console.error("PKM load-context: error reading devlog:", e.message);
      }
    
      const tasks = [];
      try {
        const taskDir = path.join(projectDir, "tasks");
        const entries = await fs.readdir(taskDir);
        for (const entry of entries) {
          if (!entry.endsWith(".md")) continue;
          const content = await fs.readFile(path.join(taskDir, entry), "utf-8");
          const fm = extractFrontmatter(content);
          if (!fm || (fm.status !== "active" && fm.status !== "pending")) continue;
    
          const bodyStart = content.indexOf("\n---", 3);
          const body = bodyStart !== -1 ? content.slice(bodyStart + 4).trim() : content;
          const lines = body.split("\n");
          const titleLine = lines.find(l => l.startsWith("# "));
          const title = titleLine ? titleLine.slice(2).trim() : entry.replace(".md", "");
          const descLines = lines
            .filter(l => l.trim() && !l.startsWith("#"))
            .slice(0, 2)
            .map(l => `  ${l.trim()}`)
            .join("\n");
    
          tasks.push(`- ${title} (status: ${fm.status}, priority: ${fm.priority || "normal"})\n${descLines}`);
          meta.tasks++;
        }
      } catch (e) {
        if (e.code !== "ENOENT") console.error("PKM load-context: error reading tasks:", e.message);
      }
    
      if (tasks.length > 0) {
        sections.push(`### Active Tasks\n${tasks.join("\n")}`);
      } else {
        sections.push("### Active Tasks\nNo active tasks");
      }
    
      const context = sections.join("\n\n");
      return { context, meta };
    }
    
  • hooks/pre-commit-reminder.shRunsGitHub
    Read the script
    #!/usr/bin/env bash
    # Inject a PKM capture reminder into Claude's context before git commits.
    # Triggered by PreToolUse hook on Bash(git commit*).
    cat <<'EOF'
    {
      "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "additionalContext": "IMPORTANT: After this commit completes, dispatch the pkm-capture agent in the background to update the project devlog and capture any PKM-worthy content from this work block."
      }
    }
    EOF
    
  • hooks/resolve-project.jsGitHub
    Read the script
    // IMPORTANT: Hooks run via raw `node` from the installed plugin directory,
    // which has no node_modules. All functions here MUST be self-contained —
    // do NOT import from ../helpers.js or ../utils.js (they depend on js-yaml).
    import fs from "fs/promises";
    import path from "path";
    
    function assertPathWithinVault(relativePath, vaultPath) {
      const resolved = path.resolve(vaultPath, relativePath);
      if (resolved !== vaultPath && !resolved.startsWith(vaultPath + path.sep)) {
        throw new Error("Path escapes vault directory");
      }
    }
    
    export async function resolveProject(cwd, vaultPath) {
      try {
        await fs.access(vaultPath);
      } catch (e) {
        if (e.code === "ENOENT") {
          return { error: `VAULT_PATH does not exist: ${vaultPath}` };
        }
        return { error: `Cannot access VAULT_PATH (${e.code}): ${vaultPath}` };
      }
    
      const projectsDir = path.join(vaultPath, "01-Projects");
      const cwdBasename = path.basename(cwd).toLowerCase();
    
      try {
        const entries = await fs.readdir(projectsDir, { withFileTypes: true });
        for (const entry of entries) {
          if (entry.isDirectory() && entry.name.toLowerCase() === cwdBasename) {
            return { projectPath: `01-Projects/${entry.name}` };
          }
        }
      } catch (e) {
        if (e.code !== "ENOENT") {
          return { error: `Error reading 01-Projects/: ${e.message}` };
        }
        // 01-Projects/ doesn't exist -- fall through to CLAUDE.md check
      }
    
      try {
        const claudeMd = await fs.readFile(path.join(cwd, "CLAUDE.md"), "utf-8");
        const match = claudeMd.match(/^#\s+PKM:\s*(.+)$/m);
        if (match) {
          const annotatedPath = match[1].trim();
          try {
            assertPathWithinVault(annotatedPath, vaultPath);
          } catch (e) {
            if (e.message === "Path escapes vault directory") {
              return { error: `CLAUDE.md annotation escapes vault directory: ${annotatedPath}` };
            }
            throw e;
          }
          try {
            await fs.access(path.join(vaultPath, annotatedPath));
            return { projectPath: annotatedPath };
          } catch (e) {
            if (e.code === "ENOENT") {
              return { error: `CLAUDE.md annotation points to non-existent vault path: ${annotatedPath}` };
            }
            return { error: `Cannot access annotated vault path (${e.code}): ${annotatedPath}` };
          }
        }
      } catch (e) {
        if (e.code !== "ENOENT" && e.code !== "EACCES") {
          return { error: `Error reading CLAUDE.md: ${e.message}` };
        }
        // No CLAUDE.md or not readable -- fall through
      }
    
      return {
        error: `No vault project found for "${path.basename(cwd)}". ` +
          `To fix: ensure your project folder name matches the repo name in 01-Projects/, ` +
          `or add "# PKM: 01-Projects/YourProject" to your project's CLAUDE.md.`
      };
    }
    
  • hooks/session-start.jsRunsGitHub
    Read the script
    #!/usr/bin/env node
    
    import { resolveProject } from "./resolve-project.js";
    import { loadProjectContext } from "./load-context.js";
    import fs from "node:fs/promises";
    import path from "node:path";
    import os from "node:os";
    
    const VAULT_PATH = process.env.VAULT_PATH;
    
    async function getSemanticStats(vaultPath) {
      try {
        const statsPath = path.join(vaultPath, ".obsidian", "semantic-stats.json");
        const raw = await fs.readFile(statsPath, "utf-8");
        const stats = JSON.parse(raw);
        if (typeof stats.indexed_files !== "number") return null;
        return stats;
      } catch {
        return null;
      }
    }
    
    async function main() {
      let inputJson = "";
      for await (const chunk of process.stdin) {
        inputJson += chunk;
      }
    
      let input;
      try {
        input = JSON.parse(inputJson);
      } catch {
        const output = {
          hookSpecificOutput: {
            hookEventName: "SessionStart",
            additionalContext: "PKM hook error: could not parse hook input JSON."
          }
        };
        console.log(JSON.stringify(output));
        process.exit(0);
      }
    
      const { cwd } = input;
    
      if (!cwd || typeof cwd !== "string") {
        const output = {
          hookSpecificOutput: {
            hookEventName: "SessionStart",
            additionalContext: "PKM hook error: hook input missing 'cwd' field."
          }
        };
        console.log(JSON.stringify(output));
        process.exit(0);
      }
    
      if (!VAULT_PATH) {
        const output = {
          hookSpecificOutput: {
            hookEventName: "SessionStart",
            additionalContext: "PKM hook warning: VAULT_PATH not set. Run /vault-pkm:setup to configure your vault path."
          }
        };
        console.log(JSON.stringify(output));
        process.exit(0);
      }
    
      // Check for stale VAULT_PATH (user changed settings but didn't restart)
      let staleEnvWarning = "";
      try {
        const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
        const settings = JSON.parse(await fs.readFile(settingsPath, "utf-8"));
        const settingsVaultPath = settings?.env?.VAULT_PATH;
        if (settingsVaultPath && settingsVaultPath !== VAULT_PATH) {
          staleEnvWarning = `PKM warning: VAULT_PATH may be stale — settings.json says "${settingsVaultPath}" but the running server uses "${VAULT_PATH}". Restart Claude Code (/quit then relaunch) to pick up the change.\n\n`;
        }
      } catch {
        // settings.json missing or unreadable — not an error
      }
    
      const { projectPath, error } = await resolveProject(cwd, VAULT_PATH);
    
      if (error) {
        const output = {
          hookSpecificOutput: {
            hookEventName: "SessionStart",
            additionalContext: staleEnvWarning + `PKM: ${error}\n\n` +
              "The vault-pkm plugin is installed but no vault project could be resolved for this directory. " +
              "If the user asks about PKM or documentation, suggest running /vault-pkm:init-project."
          },
          systemMessage: "Vault PKM: No vault project found. Run /vault-pkm:init-project to set up vault integration."
        };
        console.log(JSON.stringify(output));
        process.exit(0);
      }
    
      // Check if CWD's CLAUDE.md has ## PKM Integration section
      let hasPkmSection = false;
      try {
        const claudeMdContent = await fs.readFile(path.join(cwd, "CLAUDE.md"), "utf-8");
        hasPkmSection = /^## PKM Integration/m.test(claudeMdContent);
      } catch (e) {
        if (e.code !== "ENOENT") {
          console.error(`PKM session-start: error reading CLAUDE.md: ${e.message}`);
        }
        // hasPkmSection stays false
      }
    
      if (!hasPkmSection) {
        let context = "";
        try {
          ({ context } = await loadProjectContext(VAULT_PATH, projectPath));
        } catch (e) {
          console.error(`PKM session-start: failed to load project context: ${e.message}`);
          // context stays "" — still show the nudge
        }
    
        const output = {
          hookSpecificOutput: {
            hookEventName: "SessionStart",
            additionalContext: staleEnvWarning + (context ? context + "\n\n" : "") +
              "PKM: This project's CLAUDE.md does not have a ## PKM Integration section. " +
              "The vault-pkm plugin is installed but this project is not configured for proactive vault usage. " +
              "If the user asks about PKM or documentation, suggest running /vault-pkm:init-project."
          },
          systemMessage: "Vault PKM: This project isn't configured yet. Run /vault-pkm:init-project to set up vault integration."
        };
        console.log(JSON.stringify(output));
        process.exit(0);
      }
    
      let context, meta;
      try {
        ({ context, meta } = await loadProjectContext(VAULT_PATH, projectPath));
      } catch (e) {
        const output = {
          hookSpecificOutput: {
            hookEventName: "SessionStart",
            additionalContext: staleEnvWarning + `PKM hook error: failed to load project context: ${e.message}`
          }
        };
        console.log(JSON.stringify(output));
        process.exit(0);
      }
    
      const projectName = path.basename(projectPath);
      const loaded = [];
      if (meta.index) loaded.push("index");
      if (meta.devlog) loaded.push("devlog");
      if (meta.tasks > 0) loaded.push(`${meta.tasks} task${meta.tasks !== 1 ? "s" : ""}`);
      const missing = [];
      if (!meta.index) missing.push("index");
      if (!meta.devlog) missing.push("devlog");
      if (meta.tasks === 0) missing.push("tasks");
    
      let msg = `Obsidian PKM: Loaded ${projectName}`;
      if (loaded.length > 0) msg += ` (${loaded.join(", ")})`;
      if (missing.length > 0) msg += ` [missing: ${missing.join(", ")}]`;
      msg += ` \u2014 ${context.length.toLocaleString()} chars`;
    
      const stats = await getSemanticStats(VAULT_PATH);
      if (stats) {
        if (stats.vault_files > 0 && stats.indexed_files < stats.vault_files) {
          msg += ` | semantic: ${stats.indexed_files}/${stats.vault_files} notes`;
        } else {
          msg += ` | semantic: ${stats.indexed_files} notes`;
        }
      }
    
      const output = {
        hookSpecificOutput: {
          hookEventName: "SessionStart",
          additionalContext: staleEnvWarning + context
        },
        systemMessage: msg
      };
      console.log(JSON.stringify(output));
    }
    
    main().catch((err) => {
      console.error(`PKM SessionStart 

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 withvault-pkm

Give Claude persistent, structured memory across conversations using your Obsidian vault. Read, write, search, and navigate your knowledge base — all from within Claude Code.

Get the whole plugin