Search...Search plugins and themes...
⌘K
Sign in
  • Get started
  • Download
  • Pricing
  • Enterprise
  • Account
  • Obsidian
  • Overview
  • Sync
  • Publish
  • Canvas
  • Mobile
  • Web Clipper
  • CLI
  • Learn
  • Help
  • Developers
  • Changelog
  • About
  • Roadmap
  • Blog
  • Resources
  • System status
  • License overview
  • Terms of service
  • Privacy policy
  • Security
  • Community
  • Plugins
  • Themes
  • Discord
  • Forum / 中文论坛
  • Merch store
  • Brand guidelines
Follow us
DiscordTwitterBlueskyThreadsMastodonYouTubeGitHub
© 2026 Obsidian

Halyard Sync

Tim HelgesonTim Helgeson520 downloads

Sync your vault with a git repository (GitHub, GitLab, Bitbucket, Gitea/Forgejo, Azure DevOps, or any HTTPS host) on desktop and mobile. No native git required.

Add to Obsidian
Halyard Sync screenshot
Halyard Sync screenshot
Halyard Sync screenshot
  • Overview
  • Scorecard
  • Updates14

Halyard Sync icon

Halyard Sync

Sync an Obsidian vault with a git repository over HTTPS — on iOS, Android, macOS and Windows.

Git here is real libgit2, compiled to WebAssembly and bundled into the plugin, so nothing needs installing on the device. No native git binary, no shell, no SSH keys — but real git plumbing underneath, including a real git-crypt-compatible filter.

AI disclaimer

This plugin was developed with substantial AI assistance (Claude).

Supported providers

Provider Sign-in button PRs/MRs on conflict Notes
GitHub OAuth device flow* yes fine-grained PAT also works
GitLab (incl. self-managed 17.9+) OAuth device flow* yes self-managed base URL under Advanced
Bitbucket Cloud PAT only yes needs an API token, not an app password
Gitea / Forgejo / Codeberg PAT only yes self-managed base URL under Advanced
Azure DevOps PAT only yes PR creation resolves the repo GUID first
Any other host PAT only no (branch still pushed) configurable git username, default oauth2

* Appears only if the distribution has OAuth client IDs configured — see OAuth client IDs. GitHub and GitLab are the only forges with a usable device grant: Bitbucket has none, Gitea/Forgejo's is an unreleased feature request, and Azure DevOps's Entra ID flow routinely trips tenant conditional-access policies.

Bitbucket app passwords are being retired — no new ones since 2025-09-09, brownout 2026-06-09 to 2026-07-27, removed 2026-07-28. Use an Atlassian API token. Settings → Advanced also needs your Atlassian account email, which the REST API's Basic-auth convention requires for pull requests even though git sync itself doesn't.

How it works

  • The vault is the git repository; .git/ lives inside it.
  • Each sync commits your changes, checks the remote with one cheap request, fetches if it moved, merges (fast-forward or 3-way), and pushes.
  • Merges never write conflict markers into notes. Genuine collisions go through the conflict model.
  • .obsidian/workspace* and .trash/ never sync. Add your own ignore globs as needed.

Community plugin and Obsidian config policy

Settings → Plugin sync lists the community plugins installed in this vault. For each plugin choose one of:

  • Share plugin + settings — sync the plugin folder, including its standard data.json settings.
  • Share code; keep settings local — sync the plugin files but keep that plugin's data.json on each device.
  • Device-local plugin folder — keep the complete plugin folder on each device.

The enabled-plugin list has its own control for sharing or keeping <configDir>/community-plugins.json local. The default is the existing behavior: plugin files and the enabled list remain shareable. Halyard Sync's own operational data.json is always device-local, while its code remains shareable.

Non-shared choices are vault/repository-wide distribution policy. They are not per-consumer pull filters: every device using the repository sees the same managed policy. Halyard Sync records it in a marked block in the tracked .gitignore, preserving user-written lines. If a selected path is already tracked, Git's normal ignore rules cannot stop it from being distributed. The settings page shows the exact paths and requires an explicit “Review and apply” confirmation before removing them from the index. That migration keeps local files in place, creates one migration commit, and pushes it without rewriting history. A failed push leaves the policy visibly pending; ordinary sync remains available to fetch/merge and retry the push.

When a device receives a newer managed policy, Halyard Sync fetches that policy before staging local edits. During a clean fast-forward or merge it snapshots the matching local plugin files through Obsidian's DataAdapter and restores their exact bytes, including parent folders, after checkout. Only the deterministic plugin-policy patterns are protected; user ignore rules are not used for this restoration. Conflicted or failed integrations do not restore a stale snapshot over the working tree.

Generated content and managed exclusions

Halyard Sync supports both generated-content topologies. A producer plugin such as Halyard Fetch can claim a folder as a local cache, keeping it out of commits, or release that claim and write the completed result through Sync for other devices to pull. Managed exclusions appear separately from your own ignore patterns in Settings, with the owning plugin and label shown.

Generated writes use an external-write batch. Sync waits for the complete write before staging, then queues one sync only after success. If materialization fails, automatic and manual sync are blocked until the producer retries successfully or you clear the block from the sync panel after reviewing the destination. A broader user ignore such as Sources/ can still exclude a shared destination; the producer receives that pattern as a diagnostic, and the user pattern remains under their control.

Active note Git details

Open the Halyard Sync sidebar while viewing a Markdown note to see the latest commit that changed that exact path. The read-only section shows the commit timestamp, author identity from Git, short hash, full-hash copy action, commit message, and Halyard platform/device attribution when the commit uses the current sync message format. Existing vault sync: ... (desktop) commits are parsed as platform-only; unrelated commits do not invent a device.

The lookup reads the checked-out branch's commit tree through the bundled libgit2-WASM engine. It does not write frontmatter, note properties, or any other note content. An untracked note, a tracked path with unavailable/shallow history, and a Git read error are shown as distinct states. The panel refreshes when the active file changes and after a sync; results are cached by path and branch tip so ordinary panel redraws do not repeat history walks.

Platform support

Desktop (Win/macOS/Linux) Mobile (iOS/Android)
Sync engine libgit2 → WebAssembly (bundled) libgit2 → WebAssembly (bundled)
Transport HTTPS HTTPS
Background sync while Obsidian is open foreground only (OS limitation)
Token storage OS keychain secure storage, with fallback

Installation

Community plugin directory — not yet listed; submission is pending.

BRAT (beta) — install BRAT, then "Add beta plugin" with timrs2998/halyard-sync.

Manual — download manifest.json, main.js and styles.css from the latest release into <vault>/.obsidian/plugins/halyard-sync/. The libgit2 binary is embedded in main.js, so there is no fourth file to copy.

Setup

Run "Halyard Sync: Open setup wizard" from the command palette or click the ribbon icon. Three steps:

  1. Remote URL — the repository's HTTPS URL (https://github.com/you/vault.git). SSH URLs are rejected: SSH cannot run inside Obsidian on mobile, so HTTPS is the only transport that works everywhere.
  2. Authenticate — either Sign in with GitHub / GitLab (device flow: confirm a short code in your browser), or a personal access token:
    • GitHub: fine-grained PAT scoped to the repository with Contents: read/write, Metadata: read, Pull requests: read/write (the last lets conflict branches open PRs).
    • GitLab: scopes write_repository and api (api creates merge requests). Set your instance URL under Advanced for self-managed.
    • Bitbucket Cloud: an API token with Repositories and Pull requests read/write, plus your Atlassian account email under Advanced.
    • Gitea / Forgejo / Codeberg: any token with repository read/write. Self-hosted needs its base URL under Advanced.
    • Azure DevOps: a PAT with Code (Read & Write).
    • Any other host: any token with repo read/write; the username sent alongside it is configurable (default oauth2).
  3. Connect — Clone the repository into the vault (shallow), or Initialize it from the current vault contents if the remote is empty.

Conflict model

Most syncs merge cleanly. When edits truly overlap, the plugin never writes <<<<<<< markers into notes and never discards anything silently. Pick a strategy under Settings → Sync → On conflict:

  • PR branch (default). Your local state is pushed to sync-conflict/{device}-{timestamp}, a pull or merge request opens against the sync branch, and the vault follows the remote. Your work is parked on the forge where you can merge it with a real diff UI, from any device, whenever you like. If PR creation fails — a token scope, say — the branch is still pushed and you're told to open the PR yourself. Nothing is lost either way.
  • Discard local. Hard-reset to the remote, after confirmation listing the differing files.
  • Keep local and pause. Nothing changes; auto-sync pauses until you resolve it from the status bar or the "Resolve conflict" command.

Sync schedule

Setting Desktop default Mobile default
Sync on startup on on
Sync on foreground off on
Interval 5 min 30 min
Debounced sync after edits off off

Mobile battery. Mobile OSes suspend Obsidian in the background, so the interval only ticks while the app is open — the effective mechanism is startup plus foreground, with catch-up when more than one interval passed while closed. A no-op check costs roughly one small HTTPS request, but radio wakeups dominate battery cost, so prefer long intervals over tight polling. Battery saver disables the interval entirely and keeps only the startup and foreground triggers.

git-crypt support

Halyard Sync runs git-crypt's clean/smudge filter natively — a real compiled-in git_filter_register, not a subprocess — so a git-crypt-encrypted repository syncs correctly rather than being detected and refused. Both the default key and named keys (git-crypt init <name>, filter=git-crypt-<name> in .gitattributes) work, and one repository can mix a default key for most paths with named keys for specific subtrees.

  1. On a machine where the repository is already unlocked, export each key the device needs: git-crypt export-key <file>, or git-crypt export-key -k <name> <file> for a named one.
  2. In Settings → Encryption (git-crypt), you'll see every key the repository's .gitattributes references, marked ✓ configured or ✗ missing. Click Import key file… next to a missing entry. The file's embedded key name decides which slot it fills, so there's nothing to select manually.
  3. Sync as normal. Encrypted paths encrypt on commit and decrypt on checkout using the right key per path, and the conflict modal's per-file line counts decrypt too instead of showing "(binary)".

Until every referenced key is configured, the repository shows 🔑 key needed — distinct from 🔒 sync blocked — naming exactly which keys are missing. Auto-sync pauses, but nothing is broken and no re-clone is needed. This is all-or-nothing by design: one missing named key pauses the whole repository rather than syncing some paths and not others.

Security

  • Tokens live in Obsidian's SecretStorage (OS keychain on desktop) when available.
  • Where it isn't, they fall back to data.json in plain text, and settings shows a warning. Use the narrowest scope you can.
  • Git traffic goes through Obsidian's native requestUrl — no third-party proxy, no CORS middleman.

What this plugin can reach

  • Your vault, through Obsidian's own APIs. Notes are read and written through Vault/DataAdapter; the git repository lives in an in-memory mirror mounted into the WebAssembly module (see DESIGN.md). The shipped bundle contains no Node filesystem code — the compiled libgit2 module is linked for the web only, so there is no code path to a file outside the vault, on any platform.
  • The clipboard: writes only, one place. The OAuth device-flow dialog's "Copy code" button writes the one-time code this plugin just generated. Nothing in the plugin ever reads the clipboard, so content you copied elsewhere is never seen.
  • The network: your git host, and nothing else. Every request goes to the remote URL you configured (plus that provider's own token/PR endpoints). Nothing is sent anywhere else — no telemetry, no analytics, no update pings.

OAuth client IDs (for distributors)

The device flows need OAuth app client IDs. This repository ships with empty IDs (DEFAULT_GITHUB_CLIENT_ID / DEFAULT_GITLAB_CLIENT_ID), so the sign-in buttons stay hidden and PAT auth works out of the box with no registration. A distributor registers the OAuth apps — GitHub: a device-flow-enabled OAuth app; GitLab: an application with write_repository api scopes — and either fills the constants in or uses the settings overrides under Account → Advanced.

Limitations

  • HTTPS only, no SSH — impossible on mobile: no subprocess, no SSH transport.
  • Shallow clones by default (mobile memory; requestUrl buffers whole responses). Escape hatch: re-clone on desktop.
  • Active-note history follows the exact current path and checked-out branch; it does not follow renames. A shallow clone can only report history present in the downloaded objects.
  • No submodules, no LFS, no rebase or history rewrite.
  • No gitattributes filter drivers besides git-crypt. Git LFS and any other custom clean/smudge filter are unconditionally unsupported: the wizard and every sync refuse a repo whose .gitattributes declares one, and auto-sync pauses rather than silently committing plaintext — or a literal LFS pointer — into what should stay transformed.
  • No background sync on mobile — iOS and Android suspend the app; catch-up on launch and foreground compensates.
  • Very large vaults or huge binaries can hit mobile memory limits during clone and fetch: the whole working tree is mirrored into memory for the duration of a sync (see src/git/libgit2/fs-backend.ts).

Development

npm install
npm run dev      # esbuild watch -> main.js
npm run build    # typecheck + production bundle
npm run lint     # eslint
npm test         # vitest: pure logic + real tests against the compiled libgit2
npm run test:e2e # real Obsidian, driven headlessly via WebdriverIO

Both dev and build copy src/git/libgit2/build/dist/halyard-libgit2.wasm next to main.js — that file must ship alongside main.js, manifest.json and styles.css. The loader reads it via app.vault.adapter.readBinary against a path derived from the plugin's own manifest.dir; see src/git/libgit2/loader.ts.

The compiled .wasm and .js glue are committed, so contributing needs no Docker or Emscripten. Regenerate them only when src/git/libgit2/native/*.c changes — see src/git/libgit2/build/BUILD.md.

npm run test:e2e uses wdio-obsidian-service, which downloads a real Obsidian build (cached in .obsidian-cache/, gitignored) and drives it against e2e/vaults/simple — once as desktop Obsidian, once under emulated-mobile UI. Requires main.js to be built first. GitHub Actions runs this coverage on pushes, pull requests, and release tags on Ubuntu with Xvfb and herbstluftwm; it does not test a physical iOS device.

Architecture lives in DESIGN.md, the engine layer in src/git/libgit2/README.md, and contributor conventions in AGENTS.md.

Releasing

  • npm version x.y.z — bumps package.json, manifest.json and versions.json together, commits, and tags x.y.z
  • git push origin main --tags
  • GitHub Actions builds, verifies the tag matches manifest.json, and attaches manifest.json, main.js and styles.css to the release

No v prefix on the tag — Obsidian requires it to equal manifest.json's version exactly. .npmrc's tag-version-prefix="" is what keeps npm version from adding one.

Prior art and credits

  • obsidian-git (Vinzent03) — the plugin that defined git-in-Obsidian, and the reference every design decision here was measured against. Halyard Sync differs by compiling libgit2 to WebAssembly for real mobile support, and by refusing to write conflict markers into notes.
  • libgit2 — the actual git implementation this plugin runs. GPLv2 with a linking exception; see THIRD-PARTY-NOTICES.md.
  • isomorphic-git — powered the engine before the libgit2 cutover, and its onAuth and HttpClient shapes still inform the binding's API.
  • git-crypt (Andrew Ayer) — the file format the native filter interoperates with. Implemented from scratch against the format; no git-crypt source is used.
  • Emscripten — the toolchain that makes libgit2 run in a webview at all.
  • wdio-obsidian-service (Jesse Hines) — end-to-end testing against real Obsidian.
  • obsidian-sample-plugin — build scaffolding conventions.
  • BRAT (TfTHacker) — the beta distribution path used above.

License

MIT — see LICENSE. Third-party notices, including libgit2's GPLv2 linking exception, are in THIRD-PARTY-NOTICES.md.

HealthExcellent
ReviewPassed
About
Sync your Obsidian vault with a Git repository over HTTPS across iOS, Android, macOS, and Windows. Run real Git in WebAssembly with no native binary or SSH keys, including a git-crypt-compatible filter; commit, fetch, merge without writing conflict markers, and push changes.
GitSyncingBackup
Details
Current version
0.6.0
Last updated
3 days ago
Created
Last month
Updates
14 releases
Downloads
520
Compatible with
Obsidian 1.13.0+
Platforms
Desktop, Mobile
License
MIT
Report bugRequest featureReport plugin
Sponsor
Buy Me a Coffee
Author
Tim HelgesonTim Helgesontimrs2998
GitHubtimrs2998
tim-helgeson-b458a558
  1. Community
  2. Plugins
  3. Git
  4. Halyard Sync

Related plugins

GitHub

GitHub Sync

Sync vault to personal GitHub.

Direct Git Sync

Sync your vault natively with a GitHub repository on both Desktop and Mobile.

Git

Integrate Git version control with automatic backup and other advanced features.

YAOS

Simple real-time sync powered by your own Cloudflare Worker.

Remotely Save

Sync notes between local and cloud with smart conflict: S3, Dropbox, webdav, OneDrive, Google Drive, Box, pCloud, Yandex Disk, Koofr, Azure Blob Storage.

Differential ZIP Backup

Back our vault up with lesser storage.

GitHub

Git Sync

Sync your vault across all devices using your own GitHub account. Free forever.

GitHub

GitHub Gitless Sync

Sync a GitHub repository with vaults on different platforms without requiring git installation

Settings profiles

Create various global settings profiles, that sync between vaults.

Time Machine

Browse, compare, and restore previous versions of your notes using built-in file-recovery snapshots.