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
156 skills3 agents2 commands2 hooks

Claude Code

$ npx -y skills add AdrianV101/obsidian-pkm-plugin --agent claude-code

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.

@adrianv101
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.

156 skills3 agents
Get the whole plugin

Stars on GitHub

Momentum

stars in total
15
Star history of AdrianV101/obsidian-pkm-plugin on GitHub, a chart by star-history.com

More plugins that ship hooks.

i-have-adhd

i-have-adhd

A skill that shapes Claude Code's output for readers with ADHD: it leads with the next action, numbers multi-step work, restates state across turns, drops tangents, gives specific time estimates, and makes progress visible. Invoke it with /i-h.

1 skill1 hook61k
@ayghri@ayghri
Install Plugin
pua

pua

A Claude Code plugin that makes the assistant work harder when it stalls or repeats failures. It adds pressure-style coaching, plus skills for workplace reminders and senior engineer, CTO and team-lead modes for planning and execution.

12 skills+320k
@tanweai@tanweai
Install Plugin
claude-obsidian

claude-obsidian

Turns the sources you read, such as papers, articles and transcripts, into a linked wiki of Markdown notes in Obsidian. Claude keeps sources and citations so every summary can be traced back to its origin.

15 skills+216k
@agricidaniel@agricidaniel
Install Plugin
obsidian-second-brain

obsidian-second-brain

Gives Claude Code a persistent second brain stored as plain markdown in an Obsidian vault. Notes rewrite themselves as new sources arrive, and scheduled agents handle research synthesis and note capture.

1 skill+25.3k
@eugeniughelbur@eugeniughelbur
Install Plugin
arscontexta

arscontexta

A Claude Code plugin that builds a personal knowledge system through conversation, with skills for adding knowledge domains, checking vault health, getting methodology guidance, and evolving the system over time.

10 skills+23.5k
@agenticnotetaking@agenticnotetaking
Install Plugin
pro-workflow

pro-workflow

A Claude Code workflow plugin that learns from your corrections and keeps a self-correcting memory across sessions. It adds skills for agent teams, parallel worktree batch changes, bug capture as GitHub issues, project auto-setup and context compaction.

41 skills+33k
@rohitg00@rohitg00
Install Plugin
engram

engram

Helps you learn any topic with a first-principles curriculum, guided tutoring, and spaced review that schedules free-recall practice. Includes coaching for retention stats and study strategy.

3 skills+21.5k
@nagisanzenin@nagisanzenin
Install Plugin
hey

hey

A command line tool for HEY that lets you read and send email, manage contacts, labels, calendars, todos, habits, time tracking and journal entries from the terminal or from your coding agent.

1 skill1 hook426
@basecamp@basecamp
Install Plugin

Questions about the hooks in vault-pkm.

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