Automatically keep a full, versioned copy of your Obsidian vault in a GitHub repository.
your devices ⇄ Obsidian Sync ⇄ [ this bridge ] ⇄ GitHub
──────────── ───────────── ─────────────── ──────
Obsidian on Obsidian's Docker, on a your private
phone, laptop servers computer that repo — every
and tablet stays on version, forever
A small Docker container checks Obsidian Sync every 15 minutes, commits whatever changed to GitHub, and sends anything merged on GitHub back out to your devices.
- Backup and history — every version of every note, browsable on GitHub.
- Safe automation — tools like Claude Code can propose note edits as pull requests; nothing reaches your vault until you merge.
No git knowledge needed. Setup is about 15 minutes of copy-paste.
- A computer that stays on (home server, NAS, Raspberry Pi, …) with Docker (Docker Compose is included)
- An Obsidian account with a Sync subscription
- A GitHub account
Private is recommended — these are your notes. Leave it completely empty
(no README, no .gitignore, no license) or the first sync will fail. Note its
SSH address: git@github.com:YOUR-USERNAME/YOUR-REPO.git.
The bridge signs in with a token instead of your password. This one-off command logs in and prints it — nothing to install:
docker run -it --rm --entrypoint bash ghcr.io/puradox/obsidian-sync-git:latest \
-c 'ob login && ob sync-list-remote && cat "${XDG_CONFIG_HOME:-$HOME/.config}/obsidian-headless/auth_token" && echo'Enter your Obsidian email, password, and 2FA code if you use one. It prints your vault names and your token — copy the exact vault name and the token for step 4. (If Docker says the pull was denied, see Troubleshooting.)
This key lets the bridge write to your repository — and nothing else. Make a folder for the bridge and create the key inside it:
mkdir obsidian-bridge && cd obsidian-bridge
ssh-keygen -t ed25519 -N '' -C 'obsidian-bridge' -f ./deploy_keyCopy the output of cat deploy_key.pub into GitHub → your repository →
Settings → Deploy keys → Add deploy key, and tick Allow write access.
Save this as docker-compose.yml in the same folder, and fill in the three
values at the top. The optional settings are listed below them, commented out —
uncomment the ones you want and delete the rest.
services:
obsidian-bridge:
image: ghcr.io/puradox/obsidian-sync-git:latest
restart: unless-stopped
volumes:
- ./deploy_key:/keys/deploy_key:ro
- vault:/vault
- config:/config
environment:
# ---- fill in these three ----
VAULT_NAME: "My Vault"
GIT_REMOTE_URL: "git@github.com:YOUR-USERNAME/YOUR-REPO.git"
OBSIDIAN_AUTH_TOKEN: "paste-the-token-from-step-2"
# Where the deploy key is mounted (matches the volumes line above).
GIT_DEPLOY_KEY_FILE: "/keys/deploy_key"
# ---- optional: uncomment what you need, delete the rest ----
# Vault encryption password — only for end-to-end encrypted vaults.
# OBSIDIAN_VAULT_PASSWORD: "your-encryption-password"
# How often to sync, in cron syntax (https://crontab.guru).
# The default means every 15 minutes.
# CRON_SCHEDULE: "*/15 * * * *"
# Name and email shown on the commits.
# GIT_AUTHOR_NAME: "Obsidian Bridge"
# GIT_AUTHOR_EMAIL: "obsidian-bridge@localhost"
# Keep the vault in a subfolder of the repository — see
# "Keeping the vault in a subfolder of the repo" in the README.
# VAULT_SUBDIR: "vault"
# Deploy key for a folder shared as a git submodule (one per submodule,
# named after it) — see "Sharing a folder as a git submodule".
# GIT_SUBMODULE_DEPLOY_KEY_FILE_VAULT_COVE_QMS: "/keys/cove_qms_key"
# AI commit messages, option A: any OpenAI-compatible provider
# (Groq, Gemini, Ollama, …) — see "AI commit messages" in the README.
# LLM_API_BASE: "https://api.groq.com/openai/v1"
# LLM_MODEL: "llama-3.1-8b-instant"
# LLM_API_KEY: "your-api-key"
# LLM_MAX_TOKENS: "4096"
# AI commit messages, option B: Anthropic (used only if option A is unset).
# ANTHROPIC_API_KEY: "your-api-key"
# ANTHROPIC_MODEL: "claude-haiku-4-5"
# Report every cycle to an uptime monitor, which tells you when syncing
# breaks — or stops entirely. See "Monitoring with Uptime Kuma".
# HEARTBEAT_URL: "https://kuma.example.com/api/push/AbCdEf1234"
# Seconds without a successful sync before Docker reports "unhealthy".
# Set to ~2x your interval if CRON_SCHEDULE isn't every-N-minutes.
# HEALTH_STALE_SECONDS: "1800"
# Sync once immediately when the container starts.
# RUN_ON_START: "true"
# Kill a single sync that stalls this long (seconds) and retry next tick —
# guards against the sync engine hanging on a half-open connection. Raise
# only for very large vaults — and then also extend the healthcheck
# start_period so a long first sync isn't flagged unhealthy.
# OB_SYNC_TIMEOUT: "300"
# Only if your vault uses a non-default config folder.
# OBSIDIAN_CONFIG_DIR: ".obsidian"
# Device name shown in Obsidian Sync's version history.
# OBSIDIAN_DEVICE_NAME: "git-bridge"
volumes:
vault:
config:Vault end-to-end encrypted? Uncomment
OBSIDIAN_VAULT_PASSWORD. Not sure? Skip it — the bridge will tell you at startup if it's needed.
This file now contains your token, so keep it private and don't commit it to version control. To keep secrets out of the file entirely, see Security: Docker secrets.
docker compose up -dIt runs in the background, restarts after reboots, and keeps your vault in Docker volumes so nothing is lost across updates.
docker compose logs -fWithin a minute or two you should see cycle complete (press Ctrl+C to
stop watching). Open your GitHub repository — your notes are there.
Done. It now syncs on its own every 15 minutes. Something wrong? See Troubleshooting.
- One rule: only the bridge may write to its
vaultvolume. Make changes in Obsidian, or on GitHub via pull requests — nowhere else. - Clones and vaults stay separate. If you also clone the repository to work on it, never open that clone in Obsidian — why.
- Conflicts are safe. If a merged pull request clashes with a note you edited, your vault wins automatically, sync keeps going, nothing is lost, and the logs alert you. Details: How syncing works.
- You don't have to watch the logs. Point the bridge at an uptime monitor and it reports every cycle, so a broken — or stopped — sync finds you: Monitoring with Uptime Kuma.
- Changing a setting: edit
docker-compose.yml, then rundocker compose up -dagain. - Updating:
docker compose pull && docker compose up -d.
Everything is configured in docker-compose.yml (step 4).
| Setting | Required | Default | What it does |
|---|---|---|---|
VAULT_NAME |
✅ | — | Your vault's exact name from step 2. Fixed once the bridge has run. |
GIT_REMOTE_URL |
✅ | — | Your repository's SSH address: git@github.com:owner/repo.git. |
OBSIDIAN_AUTH_TOKEN |
✅ | — | The Obsidian token from step 2. |
GIT_DEPLOY_KEY_FILE |
✅ | — | Path of the mounted deploy key (/keys/deploy_key in the example). |
OBSIDIAN_VAULT_PASSWORD |
E2EE vaults only | — | The vault's encryption password. Ignored for normal vaults. |
CRON_SCHEDULE |
*/15 * * * * |
How often to sync, in cron syntax — the default means every 15 minutes. | |
GIT_AUTHOR_NAME / GIT_AUTHOR_EMAIL |
Obsidian Bridge / obsidian-bridge@localhost |
Name and email shown on the commits. | |
VAULT_SUBDIR |
— (vault = repo root) | Keep the vault in a subfolder of the repository (details). Fixed once the bridge has run. | |
GIT_SUBMODULE_DEPLOY_KEY_FILE_<NAME> |
per submodule | — | Deploy key for a folder shared as a git submodule; <NAME> is derived from .gitmodules (details). |
LLM_API_BASE / LLM_MODEL / LLM_API_KEY |
— | AI commit messages via an OpenAI-compatible provider (details). | |
LLM_MAX_TOKENS |
4096 |
Completion budget for that provider. On reasoning models this also covers the hidden "thinking" tokens — raise it if commits fall back to the plain label. | |
ANTHROPIC_API_KEY |
— | AI commit messages via Anthropic instead (details). | |
ANTHROPIC_MODEL |
claude-haiku-4-5 |
Model for the Anthropic option. | |
HEARTBEAT_URL |
— | Report each cycle to an uptime monitor's push URL (details). Its token is in the URL, so it's a secret and is never logged. | |
HEARTBEAT_TIMEOUT |
10 |
Seconds to wait for that monitor. A monitor that is slow or down never affects the sync cycle. | |
HEALTH_STALE_SECONDS |
2× the sync interval | Seconds without a successful sync before Docker reports unhealthy. Set it (≈2× your interval) if CRON_SCHEDULE isn't every-N-minutes. |
|
RUN_ON_START |
true |
Sync once immediately when the container starts. | |
OB_SYNC_TIMEOUT |
300 |
Seconds a single ob sync may run before it's killed and retried next tick. Guards against the sync engine stalling on a half-open connection. Raise it only for very large vaults. |
|
BRIDGE_CYCLE_TIMEOUT |
2×OB_SYNC_TIMEOUT + 300 |
Outer cap, in seconds, on a whole sync cycle. Safety net above OB_SYNC_TIMEOUT; keep it comfortably larger. If you raise either timeout, also extend the healthcheck start_period (16m by default) in compose so a long first sync isn't flagged unhealthy. |
|
OBSIDIAN_CONFIG_DIR |
.obsidian |
Only if your vault uses a different config folder. | |
OBSIDIAN_DEVICE_NAME |
— | Device name shown in Obsidian Sync's version history. |
The secret settings — OBSIDIAN_AUTH_TOKEN, OBSIDIAN_VAULT_PASSWORD,
LLM_API_KEY, ANTHROPIC_API_KEY, HEARTBEAT_URL — each also accept a _FILE variant that
reads the value from a file instead: see
Security: Docker secrets. (GIT_DEPLOY_KEY and
GIT_SUBMODULE_DEPLOY_KEY_<NAME> with the key's contents also exist, but
mounting the file is safer.)
The bridge keeps its data in two named Docker volumes, so nothing is lost when you update or recreate the container:
| Volume | Holds |
|---|---|
vault |
Your notes, as a git repository. Only the bridge writes here. |
config |
The bridge's Obsidian login and sync state. |
The compose file from step 4 holds your secrets in plain text. On a machine
only you use, that's acceptable — but anything that can talk to Docker can
read environment variables with docker inspect, and a compose file full of
secrets is easy to leak by accident.
Docker secrets keep the values in separate files, out of the compose file and
out of docker inspect. Every secret setting accepts a _FILE variant that
reads its value from a file — point those at /run/secrets/…:
services:
obsidian-bridge:
image: ghcr.io/puradox/obsidian-sync-git:latest
restart: unless-stopped
environment:
VAULT_NAME: "My Vault"
GIT_REMOTE_URL: "git@github.com:YOUR-USERNAME/YOUR-REPO.git"
OBSIDIAN_AUTH_TOKEN_FILE: "/run/secrets/obsidian_auth_token"
GIT_DEPLOY_KEY_FILE: "/run/secrets/deploy_key"
secrets:
- obsidian_auth_token
- deploy_key
volumes:
- vault:/vault
- config:/config
secrets:
obsidian_auth_token:
file: ./obsidian_auth_token.txt # a file containing just the token
deploy_key:
file: ./deploy_key
volumes:
vault:
config:The deploy key becomes a secret too, replacing the ./deploy_key:/keys/…
mount from step 4. The same pattern works for OBSIDIAN_VAULT_PASSWORD_FILE,
LLM_API_KEY_FILE, ANTHROPIC_API_KEY_FILE, and HEARTBEAT_URL_FILE.
To rotate a secret, update the file (for the deploy key, also swap the public
key on GitHub), then run docker compose restart. Secrets are never logged.
Problems show up in docker compose logs, which means you find them when you
go looking. Point the bridge at an uptime monitor and it tells you instead.
In Uptime Kuma: Add New Monitor → Monitor Type: Push, copy the Push URL it shows you, and attach your Pushover (or any other) notification to it. Then:
HEARTBEAT_URL: "https://kuma.example.com/api/push/AbCdEf1234"That URL contains the monitor's token, so it's a secret: it also takes the
_FILE variant from Docker secrets. The bridge
never writes it to the logs — not in an error, not from a response body — and
never follows a redirect away from it, which would hand the token to whichever
host the redirect names. Prefer https; over plain http the token crosses
the network in the clear, and the bridge says so at startup.
The bridge pushes up after a completed cycle and down after a failed one —
and sends the error along with it, so the alert says what broke rather than
just turning red:
[Obsidian Bridge] [🔴 Down] ob sync (device pull) timed out after 300s (stalled sync-server connection?) — killed; deferring to next tick
| Cycle outcome | Reported | The message says |
|---|---|---|
| Completed | up |
cycle complete, plus what was committed |
| Conflict auto-resolved (your vault won) | up |
which note overrode a merged change — nothing is broken, but you may want to re-apply it upstream |
| Submodule with no deploy key | up |
that it syncs to your devices but isn't being pushed |
| Network or sync-engine failure | down |
the step that failed and its error |
| A conflict that needs you | down |
what couldn't be auto-resolved |
Killed by BRIDGE_CYCLE_TIMEOUT, or the container stops |
nothing | — the monitor notices the missing beat, which is what a heartbeat is for |
| Tick skipped (previous cycle still running) | nothing | — the cycle that IS running reports for itself |
The cycle's duration is sent as the monitor's "ping" value, so Kuma graphs how long syncing takes. That's a useful early warning well before anything fails.
The bridge deliberately has no notification settings of its own — Kuma already does that job, and does it in a UI:
- Retries is your failure threshold.
0pages you on the very first failed cycle;2waits for three in a row. A single failure is often a network blip the next tick fixes, so1or2is usually the kinder setting. - Resend Interval decides whether a problem that stays broken keeps reminding you. Kuma only notifies on changes (up→down, down→up), so without this you get exactly one notification per outage.
- Heartbeat Interval should be comfortably longer than
CRON_SCHEDULE, or a healthy-but-idle bridge looks overdue. Roughly 2× your sync interval.
This is the real gain over reading logs. If the container is stopped, the Docker daemon is down, or the machine is off, the pushes simply stop and Kuma alerts on the silence — something nothing inside the container could ever report.
One limit worth knowing: if Kuma runs on the same server as the bridge, it can't tell you that server is off, because it's off too. Closing that last gap needs the monitor to live somewhere else.
Kuma can instead watch the container's own healthcheck, which is green only if
a cycle succeeded within HEALTH_STALE_SECONDS — no HEARTBEAT_URL needed.
The trade-offs: Kuma needs Docker socket access (effectively root on the host),
it takes up to ~45 minutes to notice with the default settings, and it can't
tell you why. Worth it if you'd rather not give the bridge an outbound URL.
Out of the box, each commit is labeled
vault auto-commit: N files changed (<timestamp>). Give the bridge an AI
provider and it writes a short summary of what actually changed instead. In
docker-compose.yml, uncomment one of the two options:
Option A — any OpenAI-compatible provider (LLM_API_BASE + LLM_MODEL +
LLM_API_KEY):
| Provider | LLM_API_BASE |
LLM_MODEL (example) |
|---|---|---|
| Groq | https://api.groq.com/openai/v1 |
llama-3.1-8b-instant |
| Gemini | https://generativelanguage.googleapis.com/v1beta/openai |
gemini-3.1-flash-lite |
| Ollama (local) | http://host.docker.internal:11434/v1 |
qwen2.5:3b |
Option B — Anthropic (ANTHROPIC_API_KEY; used only when option A isn't
set).
Good to know:
- Privacy: the diff of your notes is sent to the provider, and free tiers commonly use submitted data to improve their models. For fully private summaries, run a local Ollama — the diff never leaves your machine.
- It can never break a sync. If the provider is down, slow, or misconfigured, the bridge just falls back to the plain label.
- Reasoning models (gpt-oss, DeepSeek-R1, …) spend part of
LLM_MAX_TOKENSon hidden "thinking" tokens before the message itself. If commits come out as the plain label while your provider dashboard shows token use, raiseLLM_MAX_TOKENS(or pick a non-reasoning model). LLM_API_BASEmust start withhttp://orhttps://.
By default your vault is the repository: every file in it syncs to your
devices. If you want the repository to also hold things that should not
show up in Obsidian — a Quartz site that
publishes the vault, Claude Code skills, build scripts — set VAULT_SUBDIR:
VAULT_SUBDIR: "vault"giving a layout like:
your-repo/
├── vault/ ← synced with your Obsidian devices
├── quartz/ ← on GitHub, invisible to Obsidian
├── .claude/skills/ ← on GitHub, invisible to Obsidian
└── README.md
- Only files inside
vault/reach your devices; the bridge still commits and pushes the whole repository. - Work on the other folders in a separate clone of the repository — never inside the bridge's volume, and never in a clone you've opened in Obsidian (why) — and merge changes through GitHub.
- Decide up front: like
VAULT_NAME, this can't be changed once the bridge has run — you'd have to start over with fresh volumes.
One folder of your vault can be its own git repository — a
submodule — so you
can share just that folder with someone who isn't on Obsidian Sync. They
work in a clone of the folder's repository; you keep editing the notes in
Obsidian; the bridge keeps the two in step with the same vault-wins rules as
the rest of the vault. This works with or without VAULT_SUBDIR.
1. Add the submodule in a separate clone of your repository (never inside the bridge's volume) and merge it through GitHub. The folder's repository may already contain notes, or be empty:
git submodule add git@github.com:coveqms/notes.git "vault/Cove QMS"
git commit -m "Share Cove QMS as a submodule" && git push2. Give the bridge a deploy key for the folder's repository. A GitHub deploy key only ever opens one repository, so each submodule needs its own, passed in a setting named after the submodule:
- Take the submodule's name from
.gitmodules— the[submodule "…"]header, which is the path you added it at unless you chose otherwise:vault/Cove QMS. - Upper-case it and replace every character that isn't
A–Zor0–9with_:VAULT_COVE_QMS. - That gives
GIT_SUBMODULE_DEPLOY_KEY_FILE_VAULT_COVE_QMS. Create a key as in step 3, add it to the folder's repository with Allow write access, and mount it:
volumes:
- ./cove_qms_key:/keys/cove_qms_key:ro
environment:
GIT_SUBMODULE_DEPLOY_KEY_FILE_VAULT_COVE_QMS: "/keys/cove_qms_key"Restart the bridge (docker compose up -d). If a key is missing, every cycle
logs an ALERT naming the exact setting it expects. The _FILE variant works
as a Docker secret like the others; GIT_SUBMODULE_DEPLOY_KEY_<NAME> with the
key's contents also exists.
Good to know:
- The folder's repository is pushed first. Each cycle commits, rebases and pushes every submodule in its own repository before the outer one, so your main repository never points at a commit the folder's repository doesn't have. A submodule problem (network, a clash it can't resolve) is reported like any other failed cycle, but the rest of the vault still syncs.
- Your vault still wins. Your collaborator's changes are pulled into the
folder on every cycle; if one clashes with a note you edited, your note wins
and an
ALERTnames the file, exactly as for pull requests. If the folder already had notes on your devices when you added the submodule, the two are merged the same way, your notes winning any clash; an empty folder simply adopts the repository's content. - The pointer follows your notes. The commit your main repository records
for the folder always tracks the bridge's checkout. If a merged pull request
moves that pointer ahead, the folder fast-forwards to it; if the pointer and
your notes have diverged, your notes are re-applied on top (vault wins, with
an
ALERT); it is never moved backwards. - Without a key the folder still syncs to your devices and is committed
locally, but nothing is pushed to its repository, the pointer stays where it
was, and an
ALERTis logged each cycle. The cycle still succeeds. - Only folders inside the vault are handled; submodules elsewhere in the
repository are ignored. The branch is
mainunless.gitmodulessetsbranch = …(branch = .also meansmain). Onlygit@host:…andssh://URLs can be routed to a key — anhttps://submodule is treated like one without a key (committed locally, alerted, never pushed), so use the SSH address — and, as for the main repository, only github.com's host key is pinned. .gitmodulesstays canonical. Inside the container, each key is routed through an SSH host alias (bridge-submodule-…) set on the folder's ownoriginremote; other clones see the ordinary URL.- Removing the submodule (a merged pull request that drops it) detaches the folder on the bridge: its notes stay in your vault as plain files and nothing is deleted from your devices.
- The folder's
.gitnever reaches your devices. Obsidian Sync skips every file and folder whose name starts with a dot (apart from.obsidian), so the small.gitfile the bridge keeps in the folder stays on the bridge. Verified in the sync engine the bridge runs (obsidian-headless 0.0.12): the local scan skips dot-entries at every depth, and the file filter rejects dot-paths before the "sync all other file types" setting is consulted. - Your collaborator should treat their clone like any git clone of a vault — don't open it in Obsidian (why).
If automation (or other people) opens pull requests against your vault, enable
branch protection on main with "Require branches to be up to date before
merging." It forces every pull request to be updated with your latest notes
before it can merge, so conflicts surface on GitHub — where you can see and
fix them — instead of inside the bridge afterwards.
⚠️ Don't enable "Require a pull request before merging" or any other rule that blocks direct pushes tomain— the bridge pushes tomaindirectly, so every sync would be rejected. If you need that rule, use a repository ruleset instead and add the bridge's deploy key to its bypass list.
Every 15 minutes (or your CRON_SCHEDULE) the bridge runs one cycle:
- Pull note changes from your devices (Obsidian Sync).
- Commit them.
- Pull anything merged on GitHub.
- Push everything to GitHub.
- Send the merged changes back out to your devices (Obsidian Sync).
Two rules keep this safe:
- Your vault always wins. Edits made in Obsidian take priority over anything merged on GitHub — including when they conflict.
- Conflicts resolve automatically, in your vault's favour. If a merged pull
request clashes with a note you edited, the bridge keeps your note's version
of the conflicting lines, still merges in every non-conflicting change from
the pull request, pushes the result, and prints an
ALERTnaming the files so you can see what happened. The pull request's overridden version stays in the repository's history, so nothing is lost — re-apply it with a fresh pull request if you actually wanted it. Sync never halts waiting for you. (The rare conflict that can't be resolved even in your favour — e.g. a note you edited was deleted on GitHub — still stops and alerts for you to sort out upstream.)
The first sync. If the GitHub repository is empty, the first cycle fills
it from your vault. If your vault is empty and the repository already has
notes, the bridge adopts the repository and syncs it out to your devices. If
both sides already have content, they're merged with your vault winning any
clash (and an ALERT logged) — so still start from an empty repository unless
you specifically want your vault to override the repository's notes.
If you clone the repository to work on it, keep that clone out of Obsidian. A
folder that both Obsidian Sync and git write to always looks dirty: git status
lists notes you never edited, and git pull refuses with "Your local changes
to the following files would be overwritten by merge."
Use two folders — a plain vault folder for Obsidian, a clone for git — and the problem disappears. It costs one extra copy of your notes on disk, nothing else.
Obsidian Sync carries Markdown plus common images, audio and video. Other
extensions — .txt, .json, … — sync only when "Sync all other file
types" is enabled in Obsidian's Settings → Sync; files and folders whose
name starts with a dot (.gitignore, .git) never sync at all, apart from
.obsidian; and every file has a maximum size that depends on your plan.
Git has neither limit, so a file added by a pull request can sit in your repository and never reach a single device. The bridge can't warn you — from its side the sync succeeded. To list what's affected:
comm -23 <(cd ~/code/my-vault/vault && find . -type f | sort) \
<(cd ~/Obsidian && find . -type f | sort)Watch what's happening with docker compose logs -f;
docker compose ps shows the health status.
| Symptom | Fix |
|---|---|
denied when pulling the image |
The image package may be private. Sign in with docker login ghcr.io (username + a GitHub token with read:packages). |
| Startup error "vault '…' not found" | VAULT_NAME doesn't exactly match — use the vault name printed in step 2. |
| Startup error about end-to-end encryption or a rejected password | Set OBSIDIAN_VAULT_PASSWORD to your vault's correct encryption password. |
| Startup error "No SSH deploy key provided" | The key isn't mounted — check the ./deploy_key:/keys/deploy_key:ro line and GIT_DEPLOY_KEY_FILE. |
Permission denied (publickey) when pushing |
The deploy key wasn't added to the repository, or Allow write access wasn't ticked. |
Permission denied (publickey) running git yourself in docker compose exec |
The deploy key is now wired into the repo's git config, so plain git -C /vault fetch/push works. On an older image, first run export GIT_SSH_COMMAND="ssh -i $HOME/.ssh/id_deploy -o IdentitiesOnly=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts" in the shell. |
dubious ownership in repository |
Only with bind mounts: chown -R 1000:1000 the folder (the bridge runs as user 1000). Named volumes (the default) are unaffected. |
ALERT: rebase conflict … auto-resolved in favour of the vault |
Normal and handled — a merged pull request clashed with a note edit, so your note won and sync carried on; the PR's version is still in git history. See How syncing works. |
ALERT: rebase conflict … could NOT be auto-resolved (or it repeats every cycle) |
A structural conflict the bridge won't auto-resolve (e.g. modify-vs-delete), or — on the very first sync — the repository wasn't empty. Resolve it on GitHub. |
ALERT: submodule … no deploy key |
A folder shared as a submodule has no key, so it isn't being pushed. Set the setting the alert names — see Sharing a folder as a git submodule. |
ALERT: submodule … listed in .gitmodules but … is not a submodule pointer |
.gitmodules was edited by hand. Add the folder with git submodule add in a clone and merge that. |
ALERT: submodule … origin/main moved its pointer |
A merged pull request pointed the folder at commits your notes didn't have; they were combined with your notes winning. Normal and handled. |
heartbeat rejected ... (HTTP 404 ...) |
The push URL is wrong, or its monitor was deleted or paused — a paused monitor looks identical to a healthy quiet one. |
heartbeat to ... failed |
The bridge can't reach your monitor. Syncing is unaffected; only the reporting is. |
| Uptime Kuma flaps between up and down | Its Heartbeat Interval is too close to CRON_SCHEDULE — a healthy bridge is idle between cycles. Use roughly 2× your sync interval. |
Container shows as unhealthy |
No sync succeeded recently — check docker compose logs for the error. |
| A file is on GitHub but never appears in Obsidian | Sync doesn't carry every file type or size — see Not everything in git reaches your devices. |
git pull in your own clone says "local changes would be overwritten" |
You've opened that clone in Obsidian, so Sync and git are both writing it — see Don't open a git clone as an Obsidian vault. |
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.