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
reposcope
Workflow
Step 1: Locate the vault
Check OBSIDIAN_VAULT_PATH environment variable first. If unset, search for .obsidian directories:
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
cd "<vault_path>" && git init
Set local user config (avoid leaking system identity):
git config user.email "user@obsidian"
git config user.name "Obsidian User"
Rename branch to main if Git defaulted to master:
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:
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:
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:
[
"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:
{
"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 minutesautoPullOnBoot: true— pull latest on Obsidian startuppushOnCommit: true— push after each auto-commitpullBeforePush: true— pull before pushing to avoid conflictssyncMethod: "merge"— safe merge, won't lose datadisablePopups: true— quiet background operation
Step 7: Set up GitHub remote
Ask the user to create a private repo on GitHub:
- Go to https://github.com/new
- Name it (e.g.
obsidian-vaultorobsidian-work) - Set to Private
- Do NOT initialize with README or .gitignore
- 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:
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
git clone https://github.com/USERNAME/REPO.gitinto a new folder- Point Obsidian to that folder as a vault
- Install Obsidian Git community plugin (Community Plugins → Browse → "Obsidian Git")
- Ensure auto-pull on startup is enabled
- 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.obsidiandirectory 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.jsonto.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/configpersists 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-worktreeon 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
- Create
~/.hermes/scripts/obsidian-pull-sync.py:
#!/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()
- 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.exefor 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=Truewatchdog