README file from
GithubNative Git Bridge
Description
Android only. This plugin needs Termux and a small companion app; it cannot work on desktop, where it loads, explains itself and disables every operation. On desktop use plain Git or obsidian-git. Its natural habitat is a vault shared between a desktop (any Git tooling) and an Android device (this bridge).
Native Git for Obsidian on Android, executed by the real git binary inside Termux, with first-class sparse checkout support. No isomorphic-git, no HTTP server, no open ports, nothing running in the background.
[!note] Status: Work in progress
Implemented
Git operations, all executed by the real git binary in Termux: status, fetch, pull, push, commit, sync (fetch, merge, commit, push in one step), abort merge, stage and unstage per file, per folder or everything, discard local changes, reset the working tree and index to HEAD, restore a file from a commit.
Status panel (the primary surface on mobile):
- groups for conflicts, staged changes, changes and untracked files, each collapsible, with counts in the right-hand column;
- list or folder-tree layout, switched by one button; folder rows act only on the files in their group's state, and untracked folders expand into the files inside them;
- per-row actions (open, stage/unstage, discard) and a Git context menu on rows, folders and group headers with the same entries everywhere;
- renames shown as moves, with the old path on the row;
- an operation strip with progress, a cancel slot, and directional activity animations on fetch, pull and push;
- optional auto-refresh at a chosen interval.
Diff panes rendered by the plugin itself, line by line, with highlighting inside a changed line: a staged row shows HEAD → staged, an unstaged row staged → working tree, a commit shows what it changed. A hunk, or a set of lines picked in the gutter, can be staged, unstaged or discarded on its own, and each hunk says which lines of the file it is. Large diffs are trimmed at a hunk boundary against a configurable budget, and the pane says how much it is showing. Optional line wrapping, whitespace glyphs, a choice between comparing changed lines by word or by character, and optional custom colours for the diff and conflict panes (off by default, light and dark configured separately).
History: a repository-wide panel whose commits expand into their changed files, and a per-file panel that says what each commit did to the file (added, +25 −12, renamed from …), shows the file as it was at that commit, and can restore the whole file or a single block from it. Every route to a diff or a history — status panel, context menu, command palette — opens these panels; there is one surface per question.
A live output panel: what Termux is saying while it says it. Tapping the state line that counts the seconds opens git's own output as it arrives, the elapsed time against the budget it is spent from, and — the part that matters when the output is silent — whether the request ever reached Termux at all. The runner log and the streams of earlier operations sit below it, collapsed. Optionally opens by itself once an operation has run for thirty seconds.
Merge conflicts, resolved manually and only on an explicit choice: a resolution pane (same display preferences as the diff panes) with keep-local / keep-remote per block, the local and remote lines of a block compared against each other so the pane says what actually differs rather than only that something does, whole-file resolution from the context menu for anything the pane cannot display, git's own merge message prefilled for the commit that follows.
Sparse-checkout safety: protected paths are derived from the repository's own sparse exclusions, every commit and push is blocked while any of them shows as a change, staging always excludes them, and the block window offers the two recoveries that apply (trash the new files, or drop the exclusion).
Repository configuration from the app: sparse exclusions, .gitignore and .git/info/exclude, per item or in bulk.
Starting from nothing: a vault that is not a repository yet can be paired, given a repository (chosen default branch, optional first commit) or have one cloned into it. Cloning into a vault that already holds files never overwrites a single one of them: the repository's other files are checked out, and the ones that exist on both sides keep your version and appear in the panel as ordinary local changes, with a diff to look at before you decide.
Re-cloning a vault that already has a repository replaces it only after the new clone has succeeded, and the old repository is set aside with its history intact rather than deleted — with a daily reminder about the disk it uses, and one tap to remove it.
Authentication never leaves Termux: a PAT through a credential helper, an SSH key, or GitHub's OAuth device flow through the GitHub CLI (gh), configured per repository so two vaults can use two accounts.
Several vaults on one device: every vault is a repository of its own with its own Termux profile, its own token and its own credentials; one runner drains them all in one pass, oldest request first. A vault opened inside another vault's repository is kept out of it through the outer repository's .git/info/exclude (device-local, nothing tracked is edited), and each side's operations are pinned to its own repository. A second vault can pair itself from the plugin without re-running the installer; a moved vault is found again; a deleted one is reported, never silently replaced.
Operational: device-local settings that never sync through the vault, a pairing token imported automatically from the installer, a version handshake between plugin, runner and companion app, a local bridge check that diagnoses without contacting Termux, a redacting operation log, and diagnostics.
Why
obsidian-git uses isomorphic-git on mobile, which does not understand a native Git index containing sparse-checkout / skip-worktree data, so it can misread sparse omissions as mass deletions. This plugin delegates every Git operation to native Git in Termux through a file-based request/response protocol, and hard-blocks any commit/push in which protected sparse paths appear as changes.
Alternatives, and when to prefer them
The community directory already has several Git plugins. On mobile they take one of three approaches, and none of them works with a native sparse-checkout index, which is the entire reason this one exists:
- isomorphic-git in the app, e.g. obsidian-git. A JavaScript Git implementation that does not implement sparse checkout and does not honour skip-worktree, so a sparse index can be misread as mass deletions.
- The hosting provider's REST API, e.g. Hybrid Git Sync, Fit, Git Vault Sync. There is no repository on the phone at all: files are transferred over HTTP. That means no local history, no offline commits, no SSH, provider-specific limits, and an access token stored in the plugin's own settings.
- A diff viewer on top of another Git plugin, e.g. Version History Diff. It shows file history and diffs well, but takes its data from obsidian-git through that plugin's internals (its own README calls them private APIs), so it inherits whatever the underlying plugin can do. On a sparse-checkout vault that is the isomorphic-git backend again, and it offers no way for another plugin to supply the data instead. This plugin therefore renders its own history and diff views; proposing an adapter interface upstream remains an option (see research notes).
Prefer one of those if your vault is a normal full checkout, you sync only through GitHub/GitLab, and you would rather not install Termux and a companion app. They are simpler to set up and they work on iOS, which this plugin never will.
Use this plugin when you need the things only real Git can give you on the device: a sparse checkout that stays intact (with a safety gate that refuses to commit sparse omissions as deletions), full offline commits against a real local repository, SSH or a credential helper with credentials that never enter the vault or the plugin, and identical Git semantics on desktop and phone. That includes a vault whose .obsidian/plugins/ folder is itself tracked, which is why every device-specific setting here is stored outside data.json.
How it works
- You run a command (e.g. Native Git Bridge: Status).
- The plugin writes
runtime/requests/<id>.jsoninside the plugin folder (locally excluded from Git via.git/info/exclude). - The plugin opens
nativegitbridge://run; the companion app (the only supported trigger) forwards a RUN_COMMAND intent to Termux, which executes the fixed runner script once in the background. Seedocs/ADR-001-android-invocation.mdfor why a companion app is required. - The runner validates the pairing token, the action allow-list and all paths, runs git with argv arrays, writes
runtime/results/<id>.jsonatomically and exits. - The plugin (polling only while the operation is in flight) renders the result.
Install: two APKs and one pasted line
- Install Termux (from F-Droid) and the Git Bridge Companion APK (built by
.github/workflows/build-companion.yml; grant it the "Run commands in Termux environment" permission). - Paste one line into Termux:
curl -fsSL https://github.com/maxkalem/obsidian-native-git-bridge/releases/latest/download/bootstrap.sh | bash -s -- "/storage/emulated/0/<YourVault>"
[!warning] Be sure to replace <YourVault> in this command with the path to your vault! DO NOT COPY THE COMMAND AS-IS.
Better: take the command from the plugin (Settings → Copy command) or from the companion app. Those are pinned to the release you are actually running, so the runner they install is the one your build was tested against — which is what the version handshake expects. The line above tracks the newest release instead. Neither fetches from the main branch: that is the development state and may be mid-change.
No network, or GitHub having a bad day? The plugin folder already carries the same scripts — see Installing without a network.
The installer sets up everything inside Termux: packages (git, jq, openssh), storage access, allow-external-apps for the companion, an SSH key (it prints the public key to add as a repo deploy key), repo + sparse verification, the runner, a local .git/info/exclude entry, a self-test. It also drops a one-shot pairing.json that the plugin imports automatically, so the token never needs to be copied by hand. Credentials stay entirely inside Termux; the plugin never stores or sees them. Any auth you already use works unchanged: a GitHub PAT via the git credential helper (or embedded in the remote URL, which the installer offers to move into ~/.git-credentials), or an SSH key (generated only for SSH remotes). The runner always runs git with GIT_TERMINAL_PROMPT=0, so an expired PAT fails fast with a clear error instead of hanging, and diagnostics reports the detected auth method.
Sparse checkout safety
Protected paths are derived from the repository's own sparse-checkout exclusions (read from git through the runner on every status) and can be extended manually in the settings. There are no baked-in defaults. Before any commit or push the bridge runs
git status --porcelain=v1 -- "<protected>" …
git diff --cached --name-status -- "<protected>" …
and blocks the operation if either reports anything. Sparse omissions are never treated as deletions, and there is no automatic destructive "repair".
Sparse exclusions, .gitignore entries and .git/info/exclude entries can be managed per item from the settings (collapsible sections) and from the Git context menu (long tap / right click).
That menu is described once and rendered identically wherever it opens: on a file row, a folder row, a group header and in the file explorer. The order is fixed: stage/unstage, discard, keep local / keep remote for conflicts, open diff or conflict view, open file history, open in the default app, copy path, then the .gitignore / sparse / .git/info/exclude entries. Entries appear only where they apply: opening things needs a single file, discard is never offered for staged content, and a folder or group entry states how many paths it will touch. A single path can flip a config rule off again; a folder or group can only add rules, because a mixed selection has no single state to flip.
What this plugin accesses
Stated plainly, because Obsidian's developer policies require network use and any access outside the vault to be disclosed in the README.
- The plugin makes no network requests of its own. It contacts no server, sends no telemetry, needs no account, and downloads nothing. Every byte that travels does so because git in Termux talked to the remote you configured, during an operation you started (or an automatic sync you enabled). Links to GitHub in the setup screens open your browser; nothing is fetched in the background.
- The plugin only reads and writes inside the vault: the request and result files under
.obsidian/plugins/native-git-bridge/runtime/,.gitignoreat the vault root, and — only when you use the Git actions — your notes, through git. It never reads or writes anything elsewhere on the device. - Termux is a separate app, and everything outside the vault lives there: the runner script, the pairing profiles and your git credentials, all under Termux's private storage. The plugin cannot read them; it only leaves a request file in the vault and asks the companion app to wake the runner.
- Cloning writes into the vault only. The source may be a
file:///path elsewhere on the device (a backup on the SD card, say) — that is git in Termux reading a location you typed, and nothing outside the vault is ever written. Cloning a repository that tracks.obsidian/will write plugin files and settings into your vault, exactly as agit pullwould. That is your own repository doing it, chosen by you; nothing is fetched from this project's servers, and no plugin (this one included) is ever updated behind your back — but you should restart Obsidian afterwards, and the plugin says so. - Not required, not present: payment, an account, ads, telemetry, remote code, obfuscation.
Device-local by design
Enable state, integration type, pairing token, timeouts, automation flags and protected paths are stored in device-local storage scoped by vault identity, never in data.json. Syncing the plugin folder through Git therefore cannot leak one device's configuration to another. Only cosmetic UI preferences are shared.
Development
npm install
npm test # unit tests (vitest)
npm run test:e2e # runner end-to-end against a real sparse-checkout repo
npm run build # type check + bundle to main.js
Docs
Setup guide · Troubleshooting · Updating · Release engineering · Design notes for reviewers · ADR-001: why a companion app · ADR-002: several repositories per device · Threat model · Protocol · Limitations · Research notes
Installing without a network
The native-git-bridge/ folder holds the Termux scripts next to the plugin files, so a device that has the folder needs nothing else:
bash "<vault>/.obsidian/plugins/native-git-bridge/termux/bootstrap.sh" "<vault>"
bootstrap.sh takes install.sh and the runner from the directory it is started from. NGB_BASE_URL overrides that source and accepts an https:// URL, a file:// URL, or a plain directory path. See setup.md.
Installing the plugin manually
- Copy the entire
native-git-bridgefolder into your vault at(YourVault)/.obsidian/plugins/native-git-bridge/. After copying it must containmain.js,manifest.jsonandstyles.css. Thetermux/folder holds helper scripts; Obsidian ignores it. - Restart Obsidian, or use Settings -> Community plugins -> Reload.
- Settings -> Community plugins -> enable "Native Git Bridge".
- In the plugin settings press "Copy command" and paste the command into Termux. Everything else is set up automatically: the pairing token is imported by the plugin on its own.
If it is useful to you
Stars are the only signal this project gets about whether the work is worth continuing, so if the plugin does something for you, star it on GitHub — it takes a second and it is read.
License
Copyright (C) 2026 maxkalem.
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License, version 3, as published by the Free Software Foundation. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY, without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See LICENSE for the full text.
GPLv3 is a deliberate choice rather than a default: anyone may use, study, change and redistribute this plugin, and anyone who distributes a changed version has to pass the same freedoms on, with source. Closing the source, or folding this code into something proprietary, is what the licence exists to prevent.
No code was copied from obsidian-git or Version History Diff. Both were read as references, to see what users already expect, and some of their interface conventions are followed here: a history panel whose commits expand into changed files, a diff pane, a go-to-file button on rows.
Diff rendering used to bundle diff2html and no longer does. 0.6.2 replaced it with this repository's own parser and DOM builder, which left the plugin with no runtime dependencies at all. What remains from it is presentation: the d2h-* class names and the diff rules in styles.css are adapted from diff2html's MIT-licensed stylesheet, and that MIT notice is kept in styles.css beside them. MIT-licensed material may be redistributed inside a GPLv3 work as long as its notice travels with it, which is why the notice is there.
git itself always runs natively in Termux.