daniel156161 902a3df8b5
Build and Push Docker Container / build-and-push (push) Successful in 4m26s
feat: add Obsidian CLI API service
- Add Docker image for the official Obsidian desktop app and CLI.

- Start Obsidian headlessly with Xvfb and DBus setup.

- Expose a safe structured HTTP command API without shell execution.

- Add JWT-based vault, path, and command authorization.

- Support single-vault and multi-vault container mounts.

- Add TypeScript SDK helpers for Obsidian CLI commands.

- Add n8n community node package with Obsidian operations.

- Add docs, compose config, tests, and production image workflow.
2026-06-24 09:11:13 +02:00
2026-06-24 09:11:13 +02:00
2026-06-24 09:11:13 +02:00
2026-06-24 09:11:13 +02:00
2026-06-24 09:11:13 +02:00
2026-06-24 09:11:13 +02:00
2026-06-24 09:11:13 +02:00
2026-06-24 09:11:13 +02:00

Obsidian API

Docker service that exposes an HTTP API for the official Obsidian CLI.

It is meant for:

  • n8n custom nodes, HTTP Request nodes, and Code nodes
  • Prism / TypeScript agent harnesses
  • local automation against a mounted Obsidian vault

Important: official CLI architecture

This uses the real Obsidian CLI from obsidian.md/help/cli.

That CLI is not an npm package. It ships inside the Obsidian desktop app and requires the app to be running. Therefore this container runs:

  • official Obsidian desktop app
  • Xvfb virtual display
  • official /opt/Obsidian/obsidian-cli exposed as obsidian
  • this Starlette HTTP API

Because Electron is required, the image is Debian-based and larger than Alpine.

Security model

The HTTP API does not accept shell commands and does not accept plain command strings.

Allowed:

{
  "args": ["read", "path=Inbox/Note.md"]
}

Rejected:

{
  "command": "read path=Inbox/Note.md"
}
{
  "mode": "shell",
  "args": ["ls -la"]
}

The service always runs the configured binary directly with exec semantics:

obsidian <args...>

No shell is involved.

JWT access policies

Auth uses signed HS256 JWT bearer tokens. The token itself contains the policy claims.

Create a signing secret:

export OBSIDIAN_JWT_SECRET="$(openssl rand -base64 48)"

Generate a token with the same secret:

python scripts/create_jwt.py \
  --sub n8n \
  --vault work \
  --path Inbox/n8n/ \
  --path Automation.md \
  --command create \
  --command read \
  --command append \
  --command search \
  --command search:context \
  --days 365

Set the same secret for the API:

OBSIDIAN_JWT_SECRET=change-this-long-random-secret

JWT claims:

{
  "sub": "n8n",
  "vaults": ["work"],
  "paths": ["Inbox/n8n/", "Automation.md"],
  "commands": ["create", "read", "append", "search", "search:context"],
  "iat": 1760000000,
  "exp": 1791536000
}

Policy claims:

  • vaults: allowed Obsidian vault names, e.g. ["work"]
  • paths: allowed note/path prefixes, e.g. ["Inbox/n8n/", "Automation.md"]
  • commands: allowed official CLI commands, e.g. ["read", "create", "append"]
  • exp: token expiry timestamp
  • nbf: optional not-before timestamp
  • iat: optional issued-at timestamp

Use ["*"] for unrestricted access in a dimension.

Example request:

JWT="$(python scripts/create_jwt.py --sub n8n --vault work --path Inbox/n8n/ --command create --days 365)"

curl -X POST http://localhost:8080/commands \
  -H "Authorization: Bearer $JWT" \
  -H "Content-Type: application/json" \
  -d '{"args":["vault=work","create","path=Inbox/n8n/Event.md","content=Hello","overwrite"]}'

Path restrictions are enforced before the Obsidian CLI process starts. If a token has path restrictions, commands that read/write notes must include an explicit path=, file=, folder=, name= or to= argument.

Start

cp .env.example .env

Edit:

OBSIDIAN_JWT_SECRET=change-this-long-random-secret
OBSIDIAN_VAULT_PATH=/absolute/path/to/your/vault
OBSIDIAN_API_PORT=8080

Multiple vaults

Single vault is the default:

OBSIDIAN_VAULT_PATH=/home/daniel/Documents/Obsidian/Main

For more vaults, mount a parent folder and register container paths:

OBSIDIAN_VAULT_PATH=/home/daniel/Documents/Obsidian/Main
OBSIDIAN_VAULTS_ROOT=/home/daniel/Documents/Obsidian
OBSIDIAN_VAULTS=main=/vault,work=/vaults/Work,personal=/vaults/Personal

Then target a vault with the official CLI prefix:

{
  "args": ["vault=work", "read", "path=Inbox/Note.md"]
}

If no vault=<name> prefix is provided, the first vault in OBSIDIAN_VAULTS is used as the default.

Start:

docker compose up -d --build

Logs:

docker compose logs -f

API

Health

curl http://localhost:8080/health

Run official Obsidian CLI command

curl -X POST http://localhost:8080/commands \
  -H "Authorization: Bearer change-me" \
  -H "Content-Type: application/json" \
  -d '{"args":["version"]}'

Create a note:

curl -X POST http://localhost:8080/commands \
  -H "Authorization: Bearer change-me" \
  -H "Content-Type: application/json" \
  -d '{"args":["create","path=Inbox/Hello.md","content=# Hello","overwrite"]}'

Read a note:

curl -X POST http://localhost:8080/commands \
  -H "Authorization: Bearer change-me" \
  -H "Content-Type: application/json" \
  -d '{"args":["read","path=Inbox/Hello.md"]}'

Search:

curl -X POST http://localhost:8080/commands \
  -H "Authorization: Bearer change-me" \
  -H "Content-Type: application/json" \
  -d '{"args":["search","query=Hello","format=json"]}'

TypeScript SDK

SDK path:

sdk/typescript

Install locally:

npm install ./sdk/typescript

Use in TypeScript / Node / Prism:

import { ObsidianCliClient } from '@obsidian-api/sdk';

const obsidian = new ObsidianCliClient({
  baseUrl: 'http://localhost:8080',
  token: 'change-me',
});

await obsidian.create({
  path: 'Inbox/Hello.md',
  content: '# Hello from TypeScript',
  overwrite: true,
});

const note = await obsidian.read({ path: 'Inbox/Hello.md' });
console.log(note.stdout);

const results = await obsidian.search('Hello', { format: 'json' });
console.log(JSON.parse(results.stdout));

// Multi-vault: scoped client
const work = obsidian.inVault('work');
await work.create({ path: 'Inbox/Work.md', content: 'Hello work', overwrite: true });

n8n options:

Raw escape hatch, still safe because it sends an args array:

await obsidian.runRaw('daily:append', 'content=- [ ] Buy groceries');

Official CLI command examples

The SDK covers the official command groups from obsidian.md/help/cli, including:

  • General: help, version, reload, restart
  • Files: create, read, append, prepend, move, rename, delete, files, folders
  • Daily notes: daily, daily:path, daily:read, daily:append, daily:prepend
  • Search: search, search:context, search:open
  • Links: backlinks, links, unresolved, orphans, deadends
  • Tags/tasks/properties/templates
  • Plugins/themes/snippets
  • Sync/Publish commands
  • Workspace/tabs/recents
  • Developer commands: dev:*, eval

Development

Python API tests:

uv run pytest -q

TypeScript SDK typecheck:

cd sdk/typescript
npm install
npm run typecheck
S
Description
No description provided
Readme 465 KiB
Languages
TypeScript 52.4%
Python 42.6%
Shell 2.7%
Dockerfile 2.3%