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.
Install
$ npx -y skills add AdrianV101/obsidian-pkm-plugin --agent claude-codeShips 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.
- Matches
startup|clear|compactVAULT_PATH=${VAULT_PATH} node ${CLAUDE_PLUGIN_ROOT}/hooks/session-start.js
PreToolUse
- Matches
Bash${CLAUDE_PLUGIN_ROOT}/hooks/pre-commit-reminder.sh
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
Stats
13
Stars
3
Forks
Maintained
Maintenance
JavaScript
Language
MIT
License
2mo ago
Last commit
8mo ago
Created
Repo: AdrianV101/obsidian-pkm-plugin

