Nguyễn An Khang49 downloadsA conservative GitHub bridge for Obsidian Mobile - without running Git on the phone.
A conservative GitHub sync bridge for Obsidian mobile and desktop, without running Git on the phone.
1.0.8 (manifest, package, tag, and main agree after release).1.11.4.isDesktopOnly: false).npm run verify PASS on 2026-09-07 (lint, typecheck, test, build).npm run build; npm run verify is the canonical gate.Vault Relay connects one configured Obsidian vault/repository/branch to GitHub over HTTPS. It uses Obsidian's requestUrl() API and GitHub's REST and Git Data APIs; the core sync engine does not depend on native Git, child_process, or isomorphic-git.
The product is intentionally conservative:
force: false..git repository on Desktop, an optional setting writes a declarative signal file (.obsidian/github-vault-relay/git-handoff.json) upon successful sync. Companion helper scripts (scripts/git-handoff.ps1 for Windows, scripts/git-handoff.sh for macOS/Linux) consume this signal outside Obsidian to run git fetch and git reset --mixed <remoteCommitSha>, cleanly advancing local Git metadata without running any child processes inside Obsidian or re-downloading working tree files. This is strictly opt-in, non-blocking, and 100% hidden on mobile.Vault Relay is designed to connect Obsidian directly to GitHub over HTTPS without requiring Git on the device. Understanding how your local filesystem interacts with GitHub avoids unnecessary operational complexity.
flowchart TD
subgraph ModelA["Model A: Standalone Vault (Recommended Best Practice)"]
PhoneA["Mobile (iPhone / Android)"] -->|HTTPS API| GHA["GitHub Repository"]
DesktopA["Desktop Obsidian (Pure Vault, no .git)"] -->|HTTPS API| GHA
GHA -.->|git pull on demand| ExternalRepo["Separate Git Clone Directory (e.g. ~/Developer/vault-backup)"]
end
subgraph ModelB["Model B: Colocated Vault (Vault directory contains .git)"]
DesktopB["Desktop Obsidian"] -->|HTTPS API Push| GHB["GitHub Repository"]
DesktopB -.->|git-handoff.json| Handoff["External Script: git fetch + reset --mixed"]
Handoff -.->|Updates metadata| LocalGit[".git Folder inside Vault"]
end
.git/ directory.git clone your GitHub repository into a separate dedicated directory on your computer (e.g., ~/Developer/my-vault-repo/ or D:\GitHub\notes-repo\). When you want to inspect history or run scripts, just run git pull in that directory. The two locations remain completely isolated and will never interfere with each other.git clone (contains a .git/ directory at the vault root)..git metadata remains at the older commit until a local fetch occurs. Native git status may report unstaged changes.Desktop Git integration) that writes a declarative signal file (.obsidian/github-vault-relay/git-handoff.json). Companion helper scripts (scripts/git-handoff.ps1 for Windows, scripts/git-handoff.sh for macOS/Linux) can consume this signal to run git fetch origin <branch> --quiet and git reset --mixed <remoteCommitSha>, bringing local Git metadata in sync without re-downloading working files or compromising Obsidian's sandbox security.For a beta or mobile install, use BRAT with this repository URL:
https://github.com/ankhang0704/github-vault-relay
For a manual install, copy main.js, manifest.json, and styles.css from the release into <vault-config-dir>/plugins/github-vault-relay/, then reload and enable the plugin. <vault-config-dir> is normally .obsidian, but the runtime configuration directory is not assumed to have that name.
Use a GitHub fine-grained PAT scoped to the single repository. The required repository permission is Contents: Read and write; GitHub may add read-only Metadata automatically.
Vault Relay stores the active token only through Obsidian SecretStorage under github-vault-relay-pat. Plugin data.json and localStorage are not runtime token stores. Legacy token locations are read only for one-time migration when secure storage is available, then purged after verification. Clearing a token requires explicit confirmation.
The classifier compares three inputs: local vault files, the remote Git tree, and the durable state.json baseline. The ten file states are:
| State | Meaning | Normal handling |
|---|---|---|
LOCAL_ONLY |
Local file exists without remote file or baseline | Candidate for push/create |
REMOTE_ONLY |
Remote file exists without local file or baseline | Candidate for pull/create |
LOCAL_CHANGED |
Local SHA differs from baseline; remote still matches baseline | Candidate for push/update |
REMOTE_CHANGED |
Remote SHA differs from baseline; local still matches baseline | Candidate for pull/update |
POTENTIAL_CONFLICT |
Both sides diverged from the baseline | Preserve and request review |
UNCHANGED |
Local and remote canonical SHAs match | No-op |
LOCAL_DELETED |
Baseline file is absent locally but unchanged remotely | Candidate to delete from Git tree |
REMOTE_DELETED |
Baseline file is absent remotely but unchanged locally | Candidate to remove locally |
DELETE_CONFLICT |
One side deleted while the other side modified | Preserve and request explicit resolution |
DELETED |
Baseline file is absent on both sides | Prune obsolete baseline |
OVERSIZED and UNSAFE are scan counters/guards, not additional classifier states.
Safe Pull fetches the remote ref/tree, downloads blobs, verifies raw Git SHA-1 integrity, canonicalizes .md, .txt, and .canvas line endings to LF, and keeps other bytes unchanged. It records recovery evidence before local writes, verifies the result, then advances state.json. A remote deletion uses Obsidian's app.fileManager.trashFile() after its baseline proof and recovery snapshot are in place.
Safe Push reads local bytes, creates Git blobs, constructs a tree from the remote base tree, creates one commit, revalidates local bytes, and updates the branch ref with force: false. It then reads the authoritative ref/tree again. The local baseline advances only after remote verification succeeds. If the PATCH response is ambiguous, the client checks the authoritative ref before deciding whether the commit landed.
Pull and Push are sequential operations. If Pull succeeds and a later Push fails, the successful Pull is retained; the plugin does not pretend it can roll back a distributed GitHub operation.
Plugin-private state is stored under:
${app.vault.configDir}/github-vault-relay/
For a default vault this is commonly .obsidian/github-vault-relay/, but .obsidian is only an example. The directory contains state.json, conflicts_meta.json, conflicts/, pull-recovery/, and delete-recovery/ as needed. Root _vault-relay/ is user content in the current model and is not plugin-owned.
The source computes both the canonical storage path and the live configuration-directory exclusion from app.vault.configDir. Persisted/user rules are retained, while fresh settings, parsed rules, migrations, local scans, remote filtering, and sync engine constructors all ensure the live config directory cannot be synchronized.
.md, .txt, and .canvas.4b825dc642cb6eb9a060e54bf8d69288fbee4904 when the synchronized result contains zero files.DELETE endpoints or PUT /contents.MutationCoordinator lease within the Obsidian app instance.The plugin talks directly to https://api.github.com when settings are configured or a user-triggered sync/connection action runs. It has no relay server, analytics, telemetry, or crash-reporting service.
Vault Relay enumerates vault paths and reads allowed file bytes to compare local content with the selected GitHub repository. Vault API enumeration is supplemented by Obsidian's DataAdapter for user dot-folders such as .agents/ and .vscode/; it does not use Node/Electron filesystem APIs. The live app.vault.configDir, .git/, .trash/, and _fit/ remain explicitly excluded, while user _vault-relay/ content remains syncable. Allowed paths and contents are sent to the selected GitHub repository during sync. Do not put secrets in notes unless you intend to store them in that repository.
PAT redaction is implemented by src/security/redact.ts and used by GitHub, storage, sync, and UI error paths. Production diagnostics pass caught errors through the sanitizer; focused closure tests also guard against direct caught-error logging.
MIT. See LICENSE.