Saiki77927 downloadsA secure, plug-and-play way to sync individual vault folders or sections with GitHub. Private repos supported, multiple folder mappings, bidirectional sync, and conflict resolution.
End-to-end: open Settings, add a mapping, pick the repo, save, sync. (MP4)
![]() |
![]() |
![]() |
![]() |
Obsidian's built-in Sync covers your whole vault. Easy Git is for the case where you want to share only one or two folders with a repo: a notes folder you keep public, course material you collaborate on, a snippets section you want backed up under version control. You pick the folder, you pick the repo, you pick the direction. That's it.
Easy Git started life as a GitHub plugin and now also syncs to self-hosted Forgejo / Gitea instances. Pick the provider under Settings → Easy Git → Authentication; everything else (mappings, directions, conflict handling, backups, the markdown transforms) works the same no matter where your repos live.
Whatever the provider, Easy Git holds to the same simple spirit:
If you'd like Easy Git to reach another host such as GitLab, Bitbucket, or a different self-hosted server, that's very welcome as long as it keeps both of those true. The extension points are documented in CONTRIBUTING.md.
From inside Obsidian (recommended)
Via BRAT (for early-access builds between releases)
Saiki77/Easy-Git.Manual: download main.js, manifest.json, styles.css from the latest release into <your vault>/.obsidian/plugins/easy-git/.
Either works for private repos.
repo scope (or a fine-grained token with Contents: Read and write + Metadata: Read), paste it in settings, hit Test connection.Settings → Easy Git → + Add mapping. Pick the vault folder (or the vault root for whole-vault sync), add one or more destinations (each = repo + branch + path inside the repo), the direction (push only, pull only, or both), and how often to sync (manual, on interval, on startup, or on save). Save.
If you rename or move the mapping's folder inside Obsidian later, Easy Git updates the mapping path automatically and shows a Notice. If the folder is missing entirely (deleted, or moved while Obsidian was closed), the next sync aborts with a clear error instead of interpreting the missing folder as "delete everything on the remote."
After that, sync from the ribbon menu, the command palette (Easy Git: Sync mapping…), or the Sync button next to each mapping.
A single mapping can push the same vault folder to (or pull it from) several places at once. The mapping's direction (push / pull / both) applies to every destination of that mapping.
Mirror to several repos
Vault Remote
───── ──────
Notes/blog ──┬──> public-blog/main/posts/
└──> backup/main/blog-mirror/
Useful for keeping a public-facing copy and a private backup in sync from one source.
Fan out to several folders of one repo (e.g. a static site)
Vault Remote (one repo)
───── ──────
Notes/blog ──> site/main/src/content/blog/
Notes/projects ──> site/main/src/content/projects/
Notes/about ──> site/main/src/about/
The same mechanism works in the other direction. Set the mapping's direction to pull and add multiple destinations to aggregate several remote sources into a single vault folder:
Remote Vault
────── ─────
team-repo/main/notes/ ──┐
shared-team/main/docs/ ──┼──> Notes/aggregated/
upstream/main/handbook/ ──┘
Each destination pulls its own remote into the shared vault folder and tracks its own last-sync state. Each remote's files keep their existing relative paths. If team-repo brings intro.md and upstream also brings intro.md, whichever destination syncs last overwrites the file in your vault. Use distinct remote paths or rename files on the remote side if you need them to coexist.
In the mapping modal, scroll to Destinations and click + Add destination for another row, or Remove on an existing one. Each row needs a repo and a branch; the path inside the repo can be empty (= repo root). Save when done.
Obsidian uses wikilink embeds like ![[Pasted image …png]]. GitHub's Markdown renderer doesn't understand them, so they'd show as literal text. Easy Git rewrites them to standard CommonMark at push time:
| In your vault | What lands on GitHub |
|---|---|
![[image.png]] |
 |
![[image.png|Caption]] |
 |
![[image.png|400]] |
<img src="https://raw.githubusercontent.com/saiki77/Easy-Git/HEAD/image.png" width="400" alt=""> (width hint preserved as inline HTML) |
![[note#header]] |
unchanged (GitHub can't transclude) |
If a wikilink points to an attachment outside the mapping's vault folder, the file is copied to attachments/<basename> inside the mapping's remote folder and the rewritten link points there. That keeps each remote folder self-contained, you can browse it on GitHub without broken references.
Your vault is never modified. The rewrite only affects the bytes pushed to GitHub. Pulling those notes back into Obsidian renders fine because both wikilink and standard-Markdown forms work in Obsidian.
Toggle off per mapping if you want the raw wikilinks pushed verbatim (the mapping summary will show (raw wikilinks)).
Excalidraw drawings embedded as ![[Drawing.excalidraw|700]] (or .excalidraw.md for the newer format) won't render on GitHub by themselves, since GitHub doesn't understand the source files. Easy Git auto-resolves them to their companion image: when the rewriter sees an Excalidraw embed, it looks for a sibling <name>.svg or <name>.png in the same folder and rewrites the embed to point there. Width hints (|700) are preserved as inline HTML so drawings keep their intended size.
To enable this, turn on Auto-export SVG (or PNG) in the Excalidraw plugin's settings. Excalidraw will then write a companion .svg next to every drawing each time you edit it, and Easy Git will pick it up automatically.
If no companion exists, Easy Git falls back to a plain link and surfaces a one-time Notice telling you how to enable auto-export.
Obsidian uses several markdown features that GitHub doesn't render the same way:
[!info], [!example], [!question], and about a dozen more types. GitHub renders only five: NOTE, TIP, IMPORTANT, WARNING, CAUTION. Anything outside that set shows the literal [!type] text in a plain blockquote.==highlighted text== is Obsidian syntax. GitHub shows the == literally.\phantom (and \hphantom / \vphantom). The math block renders as a red error.At push time, Easy Git rewrites each of these into the form GitHub renders correctly:
| Source | Pushed to GitHub |
|---|---|
> [!example] Title |
> [!TIP] <!--easygit-callout:original=example,collapse=-->Title |
==highlighted== |
<mark>highlighted</mark> |
$$\phantom{(\ast)\quad} x$$ |
<!--easygit-math:phantoms=[{"kind":"phantom","args":"(\\ast)\\quad"}]-->$$\hspace{0.5em} x$$ |
The HTML comments are invisible in rendered Obsidian and rendered GitHub. They survive in the raw .md source on GitHub (you'll see them if you open the file in raw view), which is what makes the round-trip work: on pull, Easy Git reads the markers and restores the original Obsidian syntax byte-for-byte.
What this means in practice:
[!example] and ==highlight==; the rewrite is push-side only.<!--easygit-…--> markers in raw view). Rendered view on github.com is clean.The math transform has one small trade-off: GitHub renders \hspace{0.5em} instead of the original \phantom{…}, so the visual alignment can be slightly off (the spacer width is constant rather than matched to the phantom's content). The round-trip back to Obsidian is exact — pulling restores \phantom{…} with its original arguments.
All of this is gated on the same per-mapping toggle as the wikilink rewrite ("GitHub rendering pass"), which is on by default.
A pull-only mapping treats the remote folder as the source of truth on every sync. The engine scans the live remote tree, scans your local vault folder, and reconciles — no dependency on a stored "sync history" cache that could drift out of step with reality.
What this means in practice:
.easy-git-backup/<timestamp>/ first, so nothing is lost.Push-only is symmetric: local is the source of truth; remote-only files the plugin never pushed are left alone; the plugin only deletes remote files it previously pushed.
Bidirectional mappings use the full 3-way diff (with lastSyncState as the common ancestor) — that's the one direction where the cache is structurally necessary.
When both sides of a sync changed a file since the last sync, Easy Git resolves it through a layered approach. Each step handles a different category; what survives lands in the conflict modal.
lastSyncState.files[path].sha — the SHA both sides forked from), runs a 3-way diff, and if the merge is clean, writes the merged content locally. The pre-merge content is backed up first (see below). Skipped for binary files and for .md files in mappings with wikilink rewrite on (where merging rewritten markdown back to the vault would destroy your wikilink notation).In all three paths, local files are protected by the backup mechanism below.
Easy Git never overwrites a local file without first writing a snapshot of the pre-existing content to a backup folder, unless the mapping is set to pull only (in which case you've explicitly opted into remote-wins behavior).
Before any pull-modify or pull-delete operation, the engine snapshots the current local file to:
<vault>/.easy-git-backup/<YYYY-MM-DD-HHmmss>/<original-vault-path>
One folder per sync run, files preserved with their full vault paths inside. The folder is hardcoded to be excluded from any sync, so backups never travel to your remote — they're local-only.
If a backup write itself fails (disk full, permission error, etc.), the sync aborts with a clear error rather than risk overwriting your file without a safety net. The promise: your local content is never lost unless you've explicitly told the plugin "pull only" for this mapping.
Backups can be auto-pruned. Open Settings → Easy Git → Backups and set "Auto-prune backups older than (days)" to anything above 0 — the engine deletes timestamped subfolders past that window at the end of each sync. Leave it at 0 to keep every snapshot forever (the default for existing installs).
If the same file changed on both sides since the last sync, Easy Git pauses and lets you pick keep local, keep remote, or keep both (renames your local copy with a -conflict-local-<timestamp> suffix so neither side is lost). Cancelling the conflict modal aborts the entire run without touching anything.
Each run produces one atomic commit via GitHub's Git Data API: blob → tree (with base_tree so unrelated files in the repo are preserved) → commit → ref update. The branch's current HEAD is fetched right before the commit is built, and the ref update is non-fast-forward-protected, so if someone else pushes mid-run the sync retries from scratch (up to 3×, 1s/3s/9s backoff) instead of clobbering.
File identity is the git blob SHA-1 (matches git hash-object), so we compare local and remote without round-tripping content.
For a step-by-step walkthrough of one sync run, the three-way classifier, the conflict-resolution choices, the wikilink rewriter, and the OAuth Device Flow, see docs/how-it-works.md.
.obsidian/**, .trash/**, .git/**, node_modules/** (editable in settings)..easygitignoreDrop a .easygitignore file at the root of any mapping's vault folder and its patterns are added on top of the global excludes for that mapping only. Same syntax as the global list: one glob per line, # for comments, blank lines ignored. Useful when you want to exclude *.pdf in one mapping but not another. The .easygitignore file itself is never pushed.
Every sync run is recorded in an in-Obsidian log so you can see exactly what happened on each mapping without opening the developer console. Open it via:
Easy Git: Show sync logEach entry shows the mapping and destination, when it ran, the trigger (manual, interval, startup, on-save, command), the duration, and the outcome. Successful runs list the files added, modified, or deleted. Failed runs surface the full error including the vault path that caused it. The log keeps the most recent 100 entries and can be cleared from the log modal.
If a sync errors with File already exists at "Notes/Foo.md" (or similar), that's a case-insensitive filename collision: the remote has one casing and your vault has another. Easy Git tries to recover automatically by writing the new content to the existing file regardless of case; the error only surfaces if recovery itself fails.
A small indicator sits in Obsidian's bottom-right status bar showing the aggregate sync state across all mappings:
↻ Ready: at least one mapping configured, nothing has synced yet↻ Synced 5m ago: last successful sync (most recent across mappings)↻ Syncing…: a sync is in progress! Easy Git error: at least one mapping has an unresolved errorClick it to jump straight to Easy Git's settings. Hidden when you have no mappings configured.
Easy Git tries hard to keep working even when settings drift, get hand-edited, or carry state from an older version:
rewriteWikilinks flag default to wikilink rewrite on.healSettings() pass that normalizes vault folder paths (strips slashes), clamps invalid numeric values back to defaults (max file size, retention days, interval minutes, debounce ms), regenerates missing destination IDs, repairs half-cleared auth state, and tops up the global exclude list with safety patterns (.easy-git-backup/**, .obsidian/**, etc.) without removing any of your additions.The settings tab is grouped into foldable sections so you can collapse the parts you don't need to look at:
.easygitignore)All three conflict layers (mtime auto-resolve, 3-way merge, the modal) and the backup mechanism are still on by default; the new toggles only exist for the rare case where you want to disable one.
api.github.com (and github.com/login/... for Device Flow). With a self-hosted Forgejo/Gitea provider, calls go only to the Instance URL you configure. No third-party servers either way.npm install
npm run build
main.js is the bundled output. The release workflow at .github/workflows/release.yml builds and uploads main.js + manifest.json + styles.css on tag push.