One-click deployment of headless Obsidian with CLI on remote Linux VMs — built for AI agents.
Obsidian's CLI (Catalyst beta 1.12+) is powerful: it can read, write, search, and manage vaults programmatically. But the CLI requires the Obsidian GUI app to be running, which doesn't exist on headless servers.
agentic-obsidian solves this by:
- Extracting an Obsidian AppImage on a headless Linux VM
- Running it on a virtual framebuffer (Xvfb) so the CLI works
- Overlaying the Catalyst
.asarto unlock CLI 1.12+ features - Exposing the vault via CLI, HTTP API, and SSH
The result: any AI agent, cron job, or automation pipeline can use Obsidian as a knowledge backend.
# Set vault path
export OBSIDIAN_VAULT=/home/user/obsidian-vault
# Install base (downloads AppImage, sets up Xvfb, systemd, API)
curl -sSL https://raw.githubusercontent.com/Zpankz/agentic-obsidian/main/install.sh | bash
# Authenticate, upgrade to insider CLI, and connect sync (one command)
./agent-setup.sh --email you@example.com --password 'your-password' --vault-name my-vaultOr if you already have the insider .asar:
export OBSIDIAN_ASAR_PATH=/path/to/obsidian-1.12.x.asar
export OBSIDIAN_VAULT=/home/user/obsidian-vault
curl -sSL https://raw.githubusercontent.com/Zpankz/agentic-obsidian/main/install.sh | bashgit clone https://github.com/Zpankz/agentic-obsidian.git
cd agentic-obsidian
./exe-install.sh my-obsidian-vmThis creates a new exe.dev VM, installs everything, and configures the HTTP proxy. Your vault is accessible at https://my-obsidian-vm.exe.xyz/.
After install.sh, the base Obsidian 1.11.7 is running but the CLI doesn't work yet (requires 1.12+ insider build). agent-setup.sh automates everything in one command:
# Full setup: authenticate, upgrade to 1.12+, create & connect sync vault
./agent-setup.sh --email you@example.com --password 'your-password' --vault-name my-vault
# Connect to an existing remote vault instead of creating a new one
./agent-setup.sh --email you@example.com --password 'your-password' \
--vault-name my-vault --connect-existing
# Just upgrade to insider, skip sync
./agent-setup.sh --email you@example.com --password 'your-password' --skip-sync
# Use environment variables (good for CI/automation)
OBSIDIAN_EMAIL=you@example.com OBSIDIAN_PASSWORD='your-password' \
./agent-setup.sh --vault-name my-vault
# JSON output for programmatic consumption
./agent-setup.sh --email you@example.com --password 'your-password' --json- Authenticates with your Obsidian account (Catalyst license required)
- Triggers the insider auto-updater — Obsidian downloads the signed 1.12+
.asarvia its own update mechanism - Stores the account token in Obsidian's renderer localStorage
- Creates or connects a remote sync vault
- Verifies CLI and sync are working
See docs/POST_INSTALL_GUIDE.md for the full technical breakdown of each step.
| Flag | Description |
|---|---|
--email |
Obsidian account email |
--password |
Obsidian account password |
--vault-name |
Remote vault name (default: local vault dirname) |
--vault-path |
Local vault path (default: $OBSIDIAN_VAULT) |
--region |
Sync region (default: auto-detected) |
--connect-existing |
Connect to existing vault instead of creating |
--skip-insider |
Skip insider upgrade |
--skip-sync |
Skip sync setup |
--timeout |
Max wait for insider download (default: 60s) |
--json |
Output results as JSON |
- Linux x86_64 (Ubuntu 22.04+ recommended)
- Obsidian Catalyst license — the CLI requires the insider build (1.12+)
- sudo access — for apt packages and systemd services
- Node.js 18+ — for the HTTP API server (installed automatically)
agent-setup.sh handles this automatically via Obsidian's signed auto-updater. If you prefer to copy the .asar manually:
macOS: ~/Library/Application Support/obsidian/obsidian-1.12.x.asar
Linux: ~/.config/obsidian/obsidian-1.12.x.asar
┌──────────────────────────────────────────────────────────┐
│ Headless Linux VM │
│ │
│ ┌─────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ Xvfb │───▶│ Obsidian │◀───│ Obsidian CLI │ │
│ │ :99 │ │ (headless) │ │ (1.12+ .asar) │ │
│ └─────────┘ └──────────────┘ └────────┬─────────┘ │
│ │ │
│ ┌─────────────────────────┤ │
│ │ │ │
│ ┌──────▼──────┐ ┌───────▼────────┐ │
│ │ HTTP API │ │ SSH / cron │ │
│ │ :3000 │ │ direct CLI │ │
│ └──────┬──────┘ └────────────────┘ │
│ │ │
└─────────────────────┼─────────────────────────────────────┘
│
┌───────▼────────┐
│ AI Agents / │
│ Automations │
└────────────────┘
| Component | Location | Purpose |
|---|---|---|
| Obsidian AppImage | /opt/obsidian/squashfs-root/ |
Extracted Electron app |
| Xvfb | Display :99 |
Virtual framebuffer for headless GUI |
| CLI .asar | ~/.config/obsidian/ |
Catalyst build enabling CLI |
| HTTP API | Port 3000 | REST wrapper around CLI |
| Heartbeat | ~/obsidian-vault/heartbeat.md |
5-minute health pulse |
| Backups | /opt/obsidian/backups/ |
Daily vault tar.gz (7-day rotation) |
obsidian.service— Starts Xvfb + Obsidian headlessobsidian-api.service— Starts the HTTP API server (depends on obsidian.service)
DISPLAY=:99 obsidian help
DISPLAY=:99 obsidian files
DISPLAY=:99 obsidian read file="my-note"
DISPLAY=:99 obsidian search query="meeting"
DISPLAY=:99 obsidian create name="New Note" content="# Hello"
DISPLAY=:99 obsidian daily:append content="- [ ] New task" silent
DISPLAY=:99 obsidian tags all countsssh myvm.exe.xyz 'DISPLAY=:99 obsidian search query="project ideas"'Or source the helper functions:
source integrations/ssh-pipe.sh
export OBS_HOST=myvm.exe.xyz
obs-search "project ideas"
obs-read "my-note"
obs-daily "- [ ] Review PRs"# Health check
curl https://myvm.exe.xyz/health
# List files
curl https://myvm.exe.xyz/files
# Read a note
curl https://myvm.exe.xyz/read?file=my-note
# Search
curl https://myvm.exe.xyz/search?q=meeting
# Create a note
curl -X POST https://myvm.exe.xyz/create \
-H "Content-Type: application/json" \
-d '{"name": "New Note", "content": "# Hello\nWorld"}'
# Append to daily note
curl -X POST https://myvm.exe.xyz/daily/append \
-H "Content-Type: application/json" \
-d '{"content": "- [ ] Ship the feature"}'
# Run any CLI command
curl -X POST https://myvm.exe.xyz/command \
-H "Content-Type: application/json" \
-d '{"command": "tags all counts"}'The API supports three auth methods (checked in order):
- exe.dev proxy header —
X-ExeDev-Email(auto-injected by exe.dev HTTP proxy) - Bearer token —
Authorization: Bearer <token>(set viaAPI_TOKENenv var) - Localhost only — if no token is configured, only
127.0.0.1can access
Returns service health. Always accessible without auth.
{"status": "healthy", "obsidian": true, "timestamp": "2025-01-01T00:00:00.000Z"}Returns Obsidian version string.
Returns vault metadata (path, file count, etc).
Query params: folder, ext, total
Query params: file (by name) or path (by path)
Query params: q (required), limit, matches
Query params: counts, sort
Query params: daily, todo, done
Body: {name, path, content, template, overwrite}
Body: {file, path, content} — content is required
Body: {content} — appends to today's daily note
Body: {command} — runs any CLI command (e.g. "tags all counts")
| Variable | Default | Description |
|---|---|---|
OBSIDIAN_ASAR_PATH |
(required) | Path to the Catalyst .asar file |
OBSIDIAN_VAULT |
~/obsidian-vault |
Vault directory path |
OBSIDIAN_VERSION |
1.11.7 |
AppImage version to download |
OBSIDIAN_USER |
current user | User to run services as |
API_PORT |
3000 |
HTTP API port |
API_TOKEN |
(none) | Bearer token for API auth |
SKIP_API |
false |
Skip API server installation |
SKIP_CRON |
false |
Skip cron job installation |
The heartbeat cron writes heartbeat.md to your vault every 5 minutes with:
---
status: healthy | degraded
obsidian_version: 1.12.1 (installer 1.11.7)
files: 959
vault_size: 245M
uptime: 5 days, 3:22
services:
obsidian: active
obsidian-api: active
last_check: 2025-01-01T00:00:00Z
---Daily at 3am, the vault is backed up to /opt/obsidian/backups/ as a tar.gz archive. The 7 most recent backups are kept; older ones are automatically pruned.
exe.dev provides managed Linux VMs with built-in HTTP proxying and SSH access.
exe-install.shcreates a VM viassh exe.dev new <name>- Transfers the
.asarfile from your local machine - Runs
install.shon the VM - Configures the HTTP proxy to forward port 3000
- HTTP API:
https://<name>.exe.xyz/ - SSH:
ssh <name>.exe.xyz - Direct CLI:
ssh <name>.exe.xyz 'DISPLAY=:99 obsidian help'
Set up daily vault summaries in Slack:
ssh myvm.exe.xyz 'echo "0 9 * * * SLACK_WEBHOOK_URL=https://hooks.slack.com/... DISPLAY=:99 /opt/obsidian/integrations/slack-webhook.sh" | crontab -'Source this file to get shell functions for quick remote vault access:
source integrations/ssh-pipe.sh
export OBS_HOST=myvm.exe.xyz
obs-files # list files
obs-read "note" # read a note
obs-search "query" # search vault
obs-daily "task" # append to daily note
obs-tags # list all tags
obs-tasks # list all tasks
obs-beat # read heartbeatPosts a vault summary (file count, version, vault path) to a Slack channel.
The repo includes an AGENTS.md file that provides quick-reference instructions for AI agents operating on the VM. Compatible with exe.dev's AGENTS.md convention.
agentic-obsidian/
├── install.sh # Generic Linux installer (base setup)
├── agent-setup.sh # One-shot: insider upgrade + auth + sync
├── exe-install.sh # exe.dev one-click installer
├── api/
│ ├── server.js # HTTP API server (Node.js, zero deps)
│ └── package.json
├── config/
│ ├── obsidian.service # systemd: Xvfb + Obsidian
│ └── obsidian-api.service # systemd: HTTP API
├── cron/
│ ├── heartbeat.sh # 5-min heartbeat writer
│ └── vault-backup.sh # Daily backup with rotation
├── docs/
│ └── POST_INSTALL_GUIDE.md # Technical deep-dive on insider + sync
├── integrations/
│ ├── ssh-pipe.sh # SSH helper functions
│ └── slack-webhook.sh # Slack notifications
├── AGENTS.md # AI agent instructions
├── llms.txt # LLM-readable docs
├── LICENSE # MIT
└── README.md
Ensure the systemd service is active:
sudo systemctl status obsidian
sudo journalctl -u obsidian --no-pager -n 50Check that :99 is available:
ls /tmp/.X99-lock # if exists, Xvfb is running
DISPLAY=:99 xdpyinfo # should show display infoEither set API_TOKEN in the service env, use the exe.dev proxy (auto-injects auth header), or access from localhost.
The process name contains "obsidian" which matches SSH session names. Use:
pkill -f "squashfs-root/obsidian" # targets only the Obsidian binaryMIT