Thanks for your interest in Vault Cortex! This guide covers everything you need to get started.
-
Prerequisites: Node.js >= 24 (see
.nvmrc), Docker (optional, for container mode),curl, andtarwith xz support (both preinstalled on macOS and most Linux distributions). The firstnpm run lint:shell, or the first commit that changes a shell script, uses them to download the ShellCheck release the repo pins for macOS and Linux on x86_64 and arm64 -
Clone and install:
git clone https://github.com/aliasunder/vault-cortex.git cd vault-cortex npm install npx sst installLinux (x64): run the install as
ONNXRUNTIME_NODE_INSTALL=skip npm install. Without the variable,onnxruntime-node's install script downloads GPU binaries the server never uses, and the download fails — its archive extractor (adm-zip) is stubbed out to resolve a security advisory. macOS and arm64 Linux skip the download on their own.Windows: this repo uses symlinks (
CLAUDE.md → AGENTS.md). Rungit config core.symlinks truebefore cloning, or re-clone after setting it — otherwise Git checks out symlinks as plain text files containing the target path.npx sst installfetches the SST platform types thatnpm run buildtypecheckssst.config.tsagainst. -
Run the checks:
npm test npm run lint npm run build
npm test runs the full suite: unit tests for individual modules, and
integration tests that boot a real server as a child process and call
every tool and prompt over real HTTP. The integration tests verify that
each config combination serves the correct tool surface, that auth
rejects invalid tokens, that write operations actually mutate the vault
(read-back verified), and that misconfiguration fails fast at boot.
npm run test:coverage runs the same suite with a statement-coverage
report (V8 provider); the coverage/ output directory is gitignored.
Vault Cortex can run in three modes during development:
The fastest feedback loop — runs the MCP server directly with hot reload:
PUBLIC_URL=http://localhost:8000 MCP_AUTH_TOKEN=local-dev-token VAULT_PATH=~/your-vault npm run dev:mcpRuns the MCP server in Docker against your local vault (the Dockerfile's
local target — no Lightsail, no Obsidian Sync):
npm run dev:dockerInteractive browser UI for testing all tools:
# Terminal 1 — start the server
PUBLIC_URL=http://localhost:8000 MCP_AUTH_TOKEN=local-dev-token VAULT_PATH=~/your-vault npm run dev:mcp
# Terminal 2 — launch the inspector
npx @modelcontextprotocol/inspectorSee the README for full details on each mode.
cli/ is a separate npm package (npx vault-cortex@latest init) that scaffolds the
deploy quickstarts. It is not an npm workspace — its two
runtime dependencies (commander, @clack/prompts) are also pinned in the
root devDependencies at identical versions, so the root npm ci covers
development. A test fails if the versions drift.
- Build:
npm run buildcompiles both the server andcli/(ornpx tsc -p cli/tsconfig.jsonalone) - Test:
npm testincludescli/src/**/*.test.ts - Try it:
node cli/dist/bin.js init --help
Env block sync: The optional env blocks in cli/src/env.ts are derived
from deploy/*/.env.example. If you change any .env.example, run
npm run sync:cli-env-blocks in the same PR — drift tests fail CI otherwise.
Publishing: CLI releases are explicit and independent of server releases —
nothing publishes to npm as a side effect of a server release. The maintainer
runs the "Release CLI" workflow (Actions tab), choosing a
patch/minor/major bump (or none to publish the current version); it
bumps cli/package.json on main, tags cli-v<version>, publishes to npm
via Trusted Publishing (OIDC) —
no npm token secret is stored in the repo — and creates a cli-v<version>
GitHub release (marked non-latest so server releases keep the "Latest"
badge). The trusted publisher is
configured in the npm package settings for this repo + cli_release.yml. PRs
that change cli/ should not bump the version — the release workflow owns
it. The npm package is deliberately absent from server.json — it's a
scaffolder, not a way to run the server.
Beta testing: The same "Release CLI" workflow supports a beta bump
option that publishes a prerelease under the beta dist-tag for testing CLI
changes before a real release. Dispatch from any branch — the version is set
to {current}-beta.{run_number} ephemerally (no commits, no tags, no GitHub
release). Test with npx vault-cortex@beta. The latest dist-tag is never
touched.
All code conventions — style, naming, logging, test patterns, MCP tool naming — are documented in AGENTS.md. That file is the single source of truth. Key points:
- Functional over OOP, arrow functions, factory/closure pattern
- TypeScript strict mode, Zod for MCP schemas, no
any - MCP wire format uses
snake_case; internal TypeScript usescamelCase - Tests read as a behavioral spec — one focused
it()per behavior
-
Branch from
main— use a descriptive prefix (feat/,fix/,docs/,refactor/,chore/) -
Keep PRs focused — one logical change per PR
-
Check before pushing — every commit runs the pre-commit hook: the typecheck, knip, and ESLint, Prettier, markdownlint and ShellCheck on the staged files. Before pushing, run the two checks it leaves out:
npm test && npm run build
-
Fill out the PR template — its checklist repeats step 3
-
Required checks must pass — the
mainruleset requires all seven; each blocks the merge and the finding details are in its job log:checks— prettier, lint, markdownlint, lint:shell, knip, test, and buildcli-smoke (22)/cli-smoke (24)— builds the CLI and runsiniton the engines floor (22.12) and the newest major (24), catching APIs too new for the CLI'senginesrange; the floor row also asserts the too-old refusal on Node 20arch-smoke (amd64)/arch-smoke (arm64)— builds the Docker image and boots it on a native runner for each architecture, then boots the remote image with a stubbed Sync client to run its init chain end-to-end (npm run test:remote-boot)gitleaks— secret detection overmainplus the PR's commits, so a finding onmainfails every open PR until it is cleaned uptrivy-pr— vulnerability scan of the Docker image built from your branch; a fixable CRITICAL/HIGH CVE fails it
- Bug reports: use the bug report template — include steps to reproduce and your environment
- Feature requests: use the feature request template — describe the problem before the solution
- Security issues: see SECURITY.md — report privately, not as a public issue
Release notes (.github/scripts/generate-notes.sh) lead with a ⚠ BREAKING
CHANGES section. Breaking changes are detected from the merged PR, read
via the API at release time — the reliable source: a squash commit's body is
often dropped at merge (e.g. the GitHub mobile app), but the PR body and labels
always survive.
To mark a PR as breaking, add a BREAKING CHANGE: footer (its own
paragraph) at the end of the PR description. Its text becomes the
descriptive line in the ⚠ section. Example:
BREAKING CHANGE:
vault_read_noteoutline mode now returns an object{ leading_callout?, headings }instead of a bare array; clients parsing it as an array must read.headings.
Also recognized as breaking signals: a breaking-change PR label (optional —
create it once under repo Settings → Labels if you want a clickable flag) and a
! type marker in the squash subject (feat(scope)!: …), which survives the
merge even when the body is dropped. The BREAKING CHANGE: footer is preferred
because it carries the descriptive line; the label and ! only flag that a change
is breaking.
The committed tool-surface baseline
(src/vault-mcp/mcp-core/__tests__/__snapshots__/tool-surface/) is the
byte-level record of the MCP wire surface. A PR that changes tool schemas,
descriptions, prompts, or server instructions regenerates it with
npm run snapshot:update, and the baseline diff is where reviewers judge
whether the change is breaking. The baseline captured at each release commit is
the regression reference for the
v1.0.0 stability contract.
tool-surface-snapshot.test.ts, which checks the baseline, also caps the tool
list's size, because some clients load every tool definition into each
conversation. Each checked combo's total must stay within a per-tool allowance
(CHARS_PER_TOOL_ALLOWANCE) times its tool count.
npm run report:tool-surface-size prints per-tool sizes, and a PR that raises
the allowance states why.
Releases are cut by the maintainer. Two paths:
- Manual Release: Actions tab → "Manual Release" → choose
patch/minor/major. Bumps version, deploys, creates GitHub Release. - Tag push: merge a version-bump PR into
main, thengit tag v<version> && git push --tags
Direct commits to main are blocked by a branch ruleset — all changes,
version bumps included, land via PR.
The CLI releases separately: Actions tab → "Release CLI" (see
The cli/ Package).
See the DEPLOY.md CI/CD section for details on each workflow.
The Railway one-click template (railway.com/deploy/vault-cortex) lives in
Railway's Template Composer, not in this repository; deploy/railway/README.md
is the user-facing guide. Re-creating the template from scratch is mechanical —
these are its settings:
| Setting | Value |
|---|---|
| Service name | vault-cortex |
| Source | Docker image ghcr.io/aliasunder/vault-cortex:remote |
| Volume | mount path /persist |
| Public networking | HTTP, port 8000 |
| Healthcheck path | /healthz |
| Restart policy | On failure (Railway's default) |
Variables, in the order the deploy form shows them (everything after the six inputs sits under Pre-Configured Environment Variables, collapsed):
| Variable | Value | Description shown on the deploy form |
|---|---|---|
TZ |
(optional input) | Your timezone as an IANA name (America/Toronto) — decides what "today" means for daily notes, task due dates, and memory timestamps. Leave empty for UTC |
VAULT_NAME |
(required input) | Your vault's name, the same as it is in Obsidian |
VAULT_PASSWORD |
(optional input) | Only if your vault uses end-to-end encryption; otherwise leave empty |
SYNC_FILE_TYPES |
(optional input) | Attachment types to sync, comma-separated — the same toggles as Obsidian's Sync → Selective sync. Valid values: image, audio, video, pdf, unsupported. Leave empty for the default: image,audio,video,pdf. To read CSV, JSON, TXT, XML, LOG, and YAML files, enter image,audio,video,pdf,unsupported and turn on Sync all other types in Obsidian's Sync settings on the device that has them |
OBSIDIAN_AUTH_TOKEN |
(optional input) | Leave empty to sign in through the /setup page after deploy, or paste a token from npx vault-cortex@latest get-sync-token |
SYNC_EXCLUDED_FOLDERS |
(optional input) | Folders to leave out of sync, comma-separated — the same list as Obsidian's Sync → Excluded folders. Leave empty to exclude nothing |
MCP_AUTH_TOKEN |
${{secret(64, "0123456789abcdef")}} |
Generated for you — the token your MCP client enters on the consent page |
PORT |
8000 |
The port the image listens on. Leave as is. |
STORAGE_ROOT |
/persist |
Where the volume is mounted — vault, search index, Sync device state, and logs live under it. Leave as is. |
DEVICE_NAME |
vault-cortex |
The device name that labels this container's changes in Obsidian's sync log |
TRUST_PROXY_HOPS |
2 |
Railway proxies between a visitor and the container, so the server sees the visitor's real address |
RAILWAY_HEALTHCHECK_TIMEOUT_SEC |
900 |
How long Railway waits for the first health check — the first start downloads the vault and builds the index |
MEMORY_ENABLED |
true |
The About Me/ memory layer and its tools. Set false to hide them and skip creating the folder |
EMBEDDING_ENABLED |
true |
Semantic search. Set false to skip the models and use keyword search only — fits in much less memory |
READONLY_MODE |
false |
Set true to hide every tool that changes the vault — clients can only read and search |
FILE_TOOLS_ENABLED |
true |
vault_read_file and vault_list_files. Set false when Obsidian Sync has attachment syncing off |
SYNC_MODE |
bidirectional |
Sync direction: bidirectional, pull-only (edits made on the server stay on the server and are never uploaded), or mirror-remote (Obsidian Sync overwrites edits made on the server, so the server always matches your vault). An invalid value stops the container at boot |
CONFLICT_STRATEGY |
merge |
Obsidian Sync conflict resolution: merge integrates changes automatically; conflict writes a separate conflict file |
Update the template whenever the image tag, a boot-required variable, the port, or the health path changes, then re-publish; existing deployments keep their settings until their owners redeploy.
By contributing, you agree that your contributions will be licensed under the
MIT License. Note that the published :remote image bundles
Obsidian's proprietary obsidian-headless CLI (see the README license note) —
the MIT license covers this repository's code, not that component.