A command-line tool for syncing Obsidian vaults on headless servers. Uses the official Obsidian Sync protocol over WebSocket with full end-to-end encryption.
Built for running Obsidian vaults on servers where the desktop app isn't available — perfect for automated workflows, CI/CD pipelines, or keeping a server-side copy of your notes.
- Bidirectional sync — pull remote changes and push local edits
- Real-time watch mode — continuous sync via WebSocket with filesystem monitoring
- End-to-end encryption — AES-256-GCM encryption, scrypt key derivation, compatible with Obsidian's E2E encryption
- Background service — install as a systemd user service (Linux) or launchd Launch Agent (macOS) for always-on sync
- Vault config sync — syncs
.obsidian/directory (themes, plugins, settings) - Headless operation — file-based keyring backend, no GUI required
- Chunked transfers — handles large files with 2MB chunked uploads/downloads
- Automatic reconnection — exponential backoff (1s–60s) on connection loss
- Hooks — run custom scripts on sync events (file received, pre/post push, connection loss, etc.)
brew install bpauli/tap/obsyncRequires Go 1.25+.
git clone https://github.com/bpauli/obsync.git
cd obsync
go build -o obsync ./cmd/obsyncobsync loginEnter your Obsidian account email and password. If you have MFA enabled, you'll be prompted for the code. Your auth token is stored in the system keyring.
obsync listShows all vaults on your account with their IDs, names, and encryption status.
obsync pull "My Notes" ~/notes -p "your-e2e-password"Downloads all files from the remote vault to a local directory. Use --save-password / -s to store the E2E password in the keyring for future use.
obsync push "My Notes" ~/notes -p "your-e2e-password"Uploads new and modified files, and sends delete notifications for removed files. Only changed files are pushed (compared by SHA-256 hash).
obsync watch "My Notes" ~/notes -p "your-e2e-password"Starts bidirectional real-time sync. Remote changes are pulled immediately via WebSocket. Local changes are detected via filesystem events (fsnotify) with a 500ms debounce.
Hooks let you run custom scripts in response to sync events — post-process files, trigger builds, send notifications, or commit changes to git.
Hooks are configured in JSON files at two levels:
| Location | Scope |
|---|---|
~/.config/obsync/hooks.json |
All vaults (global) |
<vault-path>/.obsync-hooks.json |
Single vault (local) |
Both files are loaded and merged additively.
Keep your vault backed up in a git repository. This hooks config pulls from git before pushing to Obsidian (to pick up any external changes), and commits + pushes to git after pulling from Obsidian.
Create ~/notes/.obsync-hooks.json:
{
"hooks": {
"PrePush": [
{
"hooks": [
{
"type": "command",
"command": "git pull --rebase origin main",
"timeout": 60
}
]
}
],
"PostPull": [
{
"hooks": [
{
"type": "command",
"command": "git add -A && git diff --cached --quiet || (git commit -m \"sync: $(date +%Y-%m-%d_%H:%M:%S)\" && git pull --rebase origin main && git push origin main)",
"timeout": 60
}
]
}
]
}
}- PrePush — before obsync pushes local changes to the vault,
git pull --rebasebrings in any commits pushed to git externally (e.g. from CI or another machine). If the rebase fails, the hook exits non-zero and obsync continues anyway (non-blocking by default). - PostPull — after obsync pulls remote changes to disk, stages everything, commits if there's a diff, rebases on the remote to avoid merge commits, and pushes to git.
| Event | When it fires | Matcher |
|---|---|---|
PostFileReceived |
After a file is pulled and written to disk | regex on file path |
PostFilePushed |
After a file is pushed to remote | regex on file path |
PostFileDeleted |
After a file is deleted | regex on file path |
| Event | When it fires |
|---|---|
PrePull |
Before a pull begins |
PostPull |
After a pull completes |
PrePush |
Before a push begins |
PostPush |
After a push completes |
| Event | When it fires |
|---|---|
WatchStart |
When watch mode begins (after initial sync) |
WatchStop |
When watch mode ends (graceful shutdown) |
ConnectionLost |
When the WebSocket connection drops |
ConnectionRestored |
When reconnection succeeds |
SyncError |
When a non-fatal sync error occurs |
{
"hooks": {
"PostFileReceived": [
{
"matcher": ".*\\.md$",
"hooks": [
{
"type": "command",
"command": "./scripts/process-note.sh",
"timeout": 30
}
]
}
]
}
}| Field | Required | Default | Description |
|---|---|---|---|
type |
yes | — | Only "command" for now |
command |
yes | — | Shell command (run via sh -c) |
timeout |
no | 30 | Seconds before killing the process |
The matcher field is a regex matched against the file path for file-level events. Omit it or set to "" / "*" to match all files. Operation and watch events ignore matchers.
Hooks control flow via their exit code:
| Exit code | Behavior |
|---|---|
0 |
Success — continue |
2 |
Block — abort the current operation (stderr is shown as the error) |
| any other | Non-blocking warning — log and continue |
This means the hook script itself decides whether a failure should stop the sync or just warn. For example, a PrePush validation hook can exit 2 to prevent the push, while a PostFileReceived notification hook can exit 1 on failure without interrupting the pull.
Each hook receives JSON context on stdin:
{
"event": "PostFileReceived",
"vault_name": "My Notes",
"vault_id": "abc123",
"vault_path": "/home/user/notes",
"file": {
"path": "Daily Notes/2024-01-15.md",
"local_path": "/home/user/notes/Daily Notes/2024-01-15.md",
"size": 1234,
"hash": "abcdef..."
}
}Hooks also receive these environment variables:
| Variable | Description |
|---|---|
OBSYNC_EVENT |
Event name (e.g. PostFileReceived) |
OBSYNC_VAULT_NAME |
Vault name |
OBSYNC_VAULT_ID |
Vault UID |
OBSYNC_VAULT_PATH |
Local vault directory path |
OBSYNC_FILE_PATH |
(file events only) Relative file path |
| Command | Description |
|---|---|
login |
Log in to Obsidian Sync |
list |
List available vaults |
pull |
Pull remote vault changes to a local directory |
push |
Push local changes to a remote vault |
watch |
Watch and continuously sync a vault bidirectionally |
install |
Install a background service for continuous sync |
uninstall |
Uninstall the background service for a vault |
status |
Show the status of the background service |
-v, --verbose Enable verbose/debug logging
-j, --json Output JSON to stdout
--config Path to config file (or OBSYNC_CONFIG env var)
--version Print version and exit
-p, --password E2E encryption password
-s, --save-password Save E2E password to keyring for future use
Install obsync as a background service for always-on vault sync. The CLI auto-detects the platform and uses the native service manager.
# Install and start the service
obsync install "My Notes" ~/notes
# Check service status
obsync status "My Notes"
# Stop and remove the service
obsync uninstall "My Notes"The install command creates a systemd user service at ~/.config/systemd/user/obsync@<vault-id>.service.
# View logs
journalctl --user -u obsync@<vault-id>.service -fFor headless servers (no active login session), enable lingering:
loginctl enable-linger $USERThe generated service file uses the file keyring backend automatically. Set OBSYNC_KEYRING_PASSWORD before installing if you use a custom keyring password.
The install command creates a Launch Agent at ~/Library/LaunchAgents/com.obsync.<vault-id>.plist. The agent starts automatically on login and restarts on failure.
# View logs
tail -f ~/Library/Logs/obsync/<vault-id>.err.log
tail -f ~/Library/Logs/obsync/<vault-id>.out.logNo additional configuration is needed — macOS Keychain works natively for user Launch Agents.
obsync stores credentials in the system keyring:
- Auth token — obtained via
obsync login - E2E password — optionally saved with
--save-password
| Backend | Description | Platforms |
|---|---|---|
auto |
Auto-detect (default) | All |
keychain |
macOS Keychain | macOS |
secret-service |
GNOME Keyring / KWallet via D-Bus | Linux (desktop) |
kwallet |
KWallet directly | Linux (KDE) |
file |
Encrypted file (~/.obsync-keyring) | All (headless) |
Set the backend via environment variable:
export OBSYNC_KEYRING_BACKEND=file
export OBSYNC_KEYRING_PASSWORD=mysecret # password for the file backendThe file backend is recommended for headless servers where no desktop keyring is available.
Config is stored at ~/.config/obsync/config.json:
{
"email": "user@example.com",
"device": "my-server"
}| Field | Description |
|---|---|
email |
Obsidian account email (set by obsync login) |
device |
Device name sent to sync server (defaults to hostname) |
Override the config path with --config or OBSYNC_CONFIG.
| Variable | Description |
|---|---|
OBSYNC_CONFIG |
Path to config file |
OBSYNC_KEYRING_BACKEND |
Keyring backend (auto, file, etc.) |
OBSYNC_KEYRING_PASSWORD |
Password for the file keyring backend |
- Go 1.25+
# Build
go build ./cmd/obsync
# Run tests
go test ./...
# Run with verbose logging
obsync -v watch "My Notes" ~/notescmd/obsync/ Entry point
internal/
api/ REST API client (auth, vault listing, access tokens)
cmd/ CLI commands (login, list, pull, push, watch, install, ...)
config/ Config file management
crypto/ E2E encryption (AES-256-GCM, scrypt, path encoding)
hooks/ Hook system (config loading, event dispatch, command runner)
secrets/ Keyring abstraction (token + E2E password storage)
sync/ WebSocket sync client (connect, push, pull, heartbeat)
ui/ Terminal UI (colored output, prompts, spinners)
This is an unofficial, community-built tool. It is not affiliated with, endorsed by, or supported by Obsidian or Dynalist Inc. "Obsidian" is a trademark of Dynalist Inc.
obsync requires a valid Obsidian Sync subscription. It does not bypass any authentication or payment — users must log in with their own Obsidian account credentials.
Use at your own risk. The Obsidian Sync protocol is undocumented and may change without notice, which could break this tool at any time.
MIT