# Obsidian Git Sync

> Obsidian Git Sync

- Skill: `lucadominguez/obsidian-git-sync` (Agent Skill)
- Install (CLI): `npx skillmds@latest add lucadominguez/obsidian-git-sync`
- Raw SKILL.md: https://api.skillmd.com/api/skills/lucadominguez/obsidian-git-sync/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: lucadominguez (https://skillmd.com/u/lucadominguez)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/lucadominguez/obsidian-git-sync

---

# Obsidian Git Sync

Set up free, cross-device Obsidian sync using Git + the Obsidian Git community plugin. Works on all platforms. No paid Obsidian Sync required.

## When to use

User wants to sync their Obsidian vault between devices for free, or set up automated versioned backups of their notes.

## Prerequisites

- Git installed (any version)
- GitHub account (free tier, private repos are unlimited)
- GitHub Personal Access Token (classic) with `repo` scope

## Workflow

### Step 1: Locate the vault

Check `OBSIDIAN_VAULT_PATH` environment variable first. If unset, search for `.obsidian` directories:

```bash
find /mnt/c/Users -maxdepth 4 -name ".obsidian" -type d 2>/dev/null
# On macOS/Linux:
find ~/Documents ~/Desktop -maxdepth 4 -name ".obsidian" -type d 2>/dev/null
```

If multiple vaults found, ask the user which one to sync. Save the correct path to memory so this doesn't need repeating.

Once the vault path is known, use `search_files` with `target='files'` to list notes, confirming it's the right vault.

### Step 2: Initialize Git

```bash
cd "<vault_path>" && git init
```

Set local user config (avoid leaking system identity):

```bash
git config user.email "user@obsidian"
git config user.name "Obsidian User"
```

Rename branch to `main` if Git defaulted to `master`:

```bash
git branch -m main
```

### Step 3: Create .gitignore

Use `write_file` to create `<vault_path>/.gitignore`:

```
# Obsidian
.obsidian/workspace.json
.obsidian/workspace-mobile.json
.obsidian/hotkeys.json

# OS
.DS_Store
Thumbs.db
desktop.ini

# Trash
.trash/
**/.trash/

# Sync conflicts
*.sync-conflict-*
```

Key principle: track plugins and their configs, but NOT `workspace.json` (changes every session, creates noise). Track `app.json`, `appearance.json`, `core-plugins.json`, `community-plugins.json`, `graph.json`, and all plugin directories.

### Step 4: Install Obsidian Git plugin

Fetch the latest release from GitHub API:

```bash
curl -sL "https://api.github.com/repos/Vinzent03/obsidian-git/releases/latest" | python3 -c "import sys,json; r=json.load(sys.stdin); [print(a['browser_download_url']) for a in r['assets']]"
```

Download the three required files into the plugin directory:

```bash
PLUGIN_DIR="<vault_path>/.obsidian/plugins/obsidian-git"
mkdir -p "$PLUGIN_DIR"
cd "$PLUGIN_DIR"
curl -sLO "<main.js URL from release>"
curl -sLO "<manifest.json URL from release>"
curl -sLO "<styles.css URL from release>"
```

Use the exact version URLs from the API response — don't guess version numbers.

### Step 5: Enable the plugin

Create or update `<vault_path>/.obsidian/community-plugins.json`:

```json
[
  "obsidian-git"
]
```

If community plugins were previously disabled, the user will need to click "Turn on community plugins" in Obsidian's settings once.

### Step 6: Configure auto-sync settings

Use `write_file` to create `<vault_path>/.obsidian/plugins/obsidian-git/data.json`:

```json
{
  "autoSaveInterval": 10,
  "autoPullInterval": 0,
  "autoPullOnBoot": true,
  "disablePush": false,
  "pullBeforePush": true,
  "disablePopups": true,
  "disableStatusBar": false,
  "listChangedFilesInMessageBody": true,
  "commitMessage": "vault backup: {{date}} {{time}}",
  "commitDateFormat": "YYYY-MM-DD HH:mm:ss",
  "autoCommitMessage": "vault auto-sync: {{numFiles}} files",
  "currentBranch": "main",
  "syncMethod": "merge",
  "submoduleRecurseCheckout": false,
  "updateSubmodules": false,
  "pushOnCommit": true,
  "showStatusBar": true,
  "autoCommitAndPush": true,
  "autoCommit": true
}
```

Key settings explained:
- `autoSaveInterval: 10` — commit every 10 minutes
- `autoPullOnBoot: true` — pull latest on Obsidian startup
- `pushOnCommit: true` — push after each auto-commit
- `pullBeforePush: true` — pull before pushing to avoid conflicts
- `syncMethod: "merge"` — safe merge, won't lose data
- `disablePopups: true` — quiet background operation

### Step 7: Set up GitHub remote

Ask the user to create a private repo on GitHub:
1. Go to https://github.com/new
2. Name it (e.g. `obsidian-vault` or `obsidian-work`)
3. Set to **Private**
4. Do NOT initialize with README or .gitignore
5. Give you the repo URL

Also ask for a GitHub PAT (classic) with `repo` scope from https://github.com/settings/tokens.

**IMPORTANT SECURITY RULE**: Never expose the token in shell commands or shell history. Use `patch` or `write_file` to embed the token directly into `.git/config`. Do NOT use `echo`, `printf`, or any command that would log the token to `.bash_history`.

Embed the token in the remote URL using `patch` on `.git/config`:

```
url = https://USERNAME:TOKEN@github.com/USERNAME/REPO.git
```

Then push:

```bash
cd "<vault_path>" && git add -A && git commit -m "Initial vault sync setup" && git push -u origin main
```

### Step 8: Verify and clean up

- The Obsidian Git plugin should appear in the status bar (bottom of Obsidian window)
- Wait 10 minutes and check GitHub — you should see an auto-commit
- Delete any credential files created during setup

### Syncing another device

1. `git clone https://github.com/USERNAME/REPO.git` into a new folder
2. Point Obsidian to that folder as a vault
3. Install Obsidian Git community plugin (Community Plugins → Browse → "Obsidian Git")
4. Ensure auto-pull on startup is enabled
5. That's it — no additional config needed, the plugin settings are in the repo

## Pitfalls

- **Wrong vault**: Always confirm the vault path with the user before running `git init`. Check for `.obsidian` directory to confirm it's actually a vault.
- **Token exposure**: Never pass the GitHub token through shell commands. Use file tools (write_file, patch) to embed it.
- **WSL path translation**: On Windows with WSL, the vault is at `/mnt/c/Users/<user>/...`. Git commands work fine from WSL at these paths.
- **Workspace noise**: Always add `.obsidian/workspace.json` to `.gitignore` — it changes on every keystroke and creates useless commits.
- **Plugin not appearing**: If community plugins are completely disabled in Obsidian, the user needs to enable them manually once in Settings → Community Plugins.
- **Commit frequency**: 10-minute intervals are reasonable. Don't go below 5 minutes — it creates excessive commits and drains battery on laptops.
- **Credential persistence**: Token in `.git/config` persists across sessions. Obsidian Git plugin uses whatever git is configured with. No credential helper needed on headless systems.

## Tips

- After setup, commit the plugin files and push immediately so the plugin config is available on other devices
- The repository is private — notes are never exposed publicly
- Git provides full version history for every note — restore any version from any point in time
- If the user wants to stop sync temporarily: `git update-index --skip-worktree` on specific files

## Fallback: cron-based pull when Obsidian is closed

The Obsidian Git plugin only syncs when Obsidian is open. Deploy a zero-token cron watchdog that pulls changes when Obsidian isn't running. **All git operations go through PowerShell (Windows host)**, not WSL, because GitHub is often unreachable from the WSL network stack.

### Deploy the fallback

1. Create `~/.hermes/scripts/obsidian-pull-sync.py`:

```python
#!/usr/bin/env python3
"""Pull-only sync via Windows PowerShell (bypasses WSL network issues)."""
import subprocess, sys

VAULT_WIN = r"C:\Users\<username>\Desktop\<vault_folder>"

def powershell(cmd, timeout=30):
    try:
        r = subprocess.run(
            ["powershell.exe", "-NoProfile", "-Command", cmd],
            capture_output=True, text=True, timeout=timeout
        )
        return r.returncode, r.stdout.strip(), r.stderr.strip()
    except subprocess.TimeoutExpired:
        return -1, "", "timeout"

def obsidian_is_running():
    rc, _, _ = powershell(
        "Get-Process obsidian -ErrorAction SilentlyContinue | Select-Object -First 1",
        timeout=10
    )
    return rc == 0

def git_ps(cmd, timeout=30):
    return powershell(f"git -C '{VAULT_WIN}' {cmd}", timeout=timeout)

def main():
    if obsidian_is_running():
        sys.exit(0)

    rc, _, stderr = git_ps("fetch origin main", timeout=30)
    if rc != 0:
        if "timeout" in stderr.lower() or rc == -1:
            sys.exit(0)
        print(f"Fetch failed: {stderr}", file=sys.stderr)
        sys.exit(1)

    rc, stdout, stderr = git_ps("rev-list --count HEAD..origin/main", timeout=10)
    behind = int(stdout or 0)
    if behind == 0:
        sys.exit(0)

    rc, _, stderr = git_ps("merge origin/main --ff-only", timeout=30)
    if rc != 0:
        print(f"Merge failed: {stderr}", file=sys.stderr)
        sys.exit(1)

    rc, stdout, _ = git_ps(f"log -{behind} --oneline --no-decorate", timeout=10)
    if rc == 0 and stdout:
        print(f"Pulled {behind} commit(s):")
        print(stdout)

if __name__ == "__main__":
    main()
```

2. Register as a cron job:

```
cronjob(action='create', name='Obsidian vault pull sync',
        schedule='every 30m', script='obsidian-pull-sync.py',
        no_agent=True, deliver='origin')
```

**Key design decisions:**
- Uses `powershell.exe` for all git operations — network calls originate from Windows, not WSL
- `Obsidian running` → skip (plugin handles it)
- `0 commits behind` → silent exit (nothing to do)
- `Network flake` → silent exit (retry next cycle)
- Only reports when it actually pulled changes
- Zero LLM tokens — `no_agent=True` watchdog

