- Node.js 22+
- pnpm 11
- Obsidian installed with a vault open
- The
obsidianCLI on your PATH (or configured viaOBSIDIAN_VFS_CLI_PATH)
git clone https://github.com/otaviof/obsidian-vfs.git
cd obsidian-vfs
pnpm install
pnpm build
pnpm testTypeScript 6 strict, ESM-only ("type": "module") | pnpm 11 workspaces, Node 22+ | ESLint 10 flat config + Prettier (printWidth: 101) | Vitest 4.x | module/moduleResolution: "nodenext"
| Command | Description |
|---|---|
pnpm build |
Build all packages (tsc -b) |
pnpm test |
Run tests (Vitest) |
pnpm run ci |
Full check: lint, build, test (not bare pnpm ci, which wipes node_modules) |
pnpm lint |
Run ESLint |
pnpm format |
Format with Prettier |
pnpm format:check |
Check formatting without writing |
pnpm package:vscode |
Build + package VS Code extension (.vsix) |
pnpm cli |
Run the CLI (after building) |
pnpm audit |
Check dependencies for known vulnerabilities |
pnpm check:bundles |
Verify tracked Claude plugin bundles match build output |
pnpm check:versions:npm |
Verify core/cli versions match |
pnpm check:versions:vscode |
Print VS Code extension version |
pnpm check:published:npm |
Check if npm packages need publishing |
pnpm check:published:vscode |
Check if VS Code extension needs publishing |
pnpm publish:vscode |
Publish VS Code extension to marketplace (requires VSCE_PAT) |
pnpm reset |
Wipe and reinstall node_modules |
The consumer packages (vscode, claude-plugin, cli) depend on core but have no cross-dependencies between each other. Shared logic belongs in packages/core.
| Package | Entry point | Output |
|---|---|---|
packages/core |
src/index.ts |
ESM (dist/) |
packages/core — mount tree |
src/mount-tree.ts |
Pure visibility logic for partial autoMount paths |
packages/vscode |
src/extension.ts |
CJS via esbuild (dist/extension.js) |
packages/claude-plugin |
src/hook-handler.ts, src/entry-expansion.ts, src/entry-subagent.ts |
ESM (dist/) + self-contained bundles (bundle/*.mjs) |
packages/cli |
src/main.ts |
ESM (dist/main.js) |
- Small composable functions — one responsibility each; prefer many small units over few large ones.
- DRY — extract shared logic to the lowest common package.
- Explicit over implicit — no barrel re-exports, no magic defaults.
- Simplest wins — fewest abstractions, most readable.
- Result types — return
VFSResult<T>(ok/error union), never nulls. - CLI parsing —
search/backlinksreturn JSON;vault/files/folders/readreturn plain text. Exit code always 0. Detect errors viaError:stdout prefix. - Reads bypass CLI —
readVirtualFileusesnode:fsdirectly. - Degraded mode (Obsidian not running) — reads/enumeration via
node:fs; search, wikilinks unavailable. - Mount tree — visibility logic for partial
autoMountpaths extracted to core (mount-tree.ts). Pure functions, no I/O, exhaustively tested. Used bysyncFilesExclude()to compute sub-directory exclusions. - Security —
path.resolve+ vault-root prefix check on all I/O. Reject symlinks outside vault.allowed/blockedenforced on all general vault content;agents/skillsimplicitly allowed.
- JSDoc — one-sentence
/** */on every export (classes, interfaces, types, functions, methods). - Top-level constants — after imports, each with
/** */comment. - Function ordering — helpers before callers; exported functions at bottom.
Shared ignores across .gitignore, .prettierignore, eslint.config.ts: node_modules/, dist/, coverage/. When adding generated/vendored directories, add to all three files. Exception: bundle/ is ignored by ESLint and Prettier but tracked in git — it must be committed for marketplace installs.
npm packages (@obsidian-vfs/core, @obsidian-vfs/cli): share a single version, bumped in lockstep. Patch for fixes, minor for new features, major for breaking changes. The cli depends on core via workspace:^ (pnpm converts to ^x.y.z at publish time).
VSCode extension (packages/vscode/package.json): independent version following semver.
Claude plugin (plugin.json): independent version tracking marketplace releases. The plugin stays private: true — it is not published to npm.
- Bump versions in the relevant
package.jsonfiles (core + cli must match). - Merge to
main.
The publish.yml workflow runs automatically on every push to main, detects which packages have unpublished versions, and publishes only those. If no versions changed, nothing is published.
To publish manually: gh workflow run publish.yml
Set OBSIDIAN_VFS_PROJECT_DIR so provisioned skill proxies use ./bin/obs-read instead of npx:
export OBSIDIAN_VFS_PROJECT_DIR=.
pnpm cli provision-skillsRebuild after changes — Claude Code picks up the new dist/ on the next prompt:
pnpm build
# Next prompt in Claude Code uses the updated hook handlerTo load the plugin during development:
claude --plugin-dir .Three hook handlers run via bin/obs-* scripts, each importing a bundled entry point from packages/claude-plugin/bundle/. These self-contained ESM files (produced by esbuild) inline all dependencies — including @obsidian-vfs/core — so marketplace installs (git clone) work without node_modules or a build step.
| Hook event | Bin script | Bundle |
|---|---|---|
UserPromptSubmit |
bin/obs-hook-handler |
bundle/hook-handler.mjs |
UserPromptExpansion |
bin/obs-expansion-handler |
bundle/entry-expansion.mjs |
SubagentStart |
bin/obs-subagent-handler |
bundle/entry-subagent.mjs |
bundle/is tracked in git (not gitignored);dist/is not.pnpm buildproduces bothdist/(for tests and local dev) andbundle/(for distribution).- For local development,
settings.local.jsoncan override the hook paths to point at the unbundleddist/output.
Build and package the extension:
pnpm package:vscode
code --install-extension packages/vscode/obsidian-vfs.vsixReload the window after installing (Developer: Reload Window).