--- title: Vault Git Sync — Architecture & Scripts updated: '2026-06-20' type: tech tags: - vault - git - sync - infra - obsidian --- # Vault Git Sync Obsidian vault is a bare git repo on TrueNAS: `/mnt/RED_2TB/storage/git/obsidian-vault.git` Also mirrored in Gitea: `https://git.mallexxx.duckdns.org/git_admin/obsidian-vault` Four clients sync to it: Eagle, Kraken, Taiga (vault), Taiga (syncthing/phone). ## Architecture ``` bare repo (obsidian-vault.git) — source of truth ↑↓ ↑↓ ↑↓ ↑↓ Eagle Kraken Taiga /vault Taiga /obsidian-syncthing full clone sparse clone sparse clone full clone ~/obsidian ~/obsidian personal/family/ phone ↔ Syncthing personal/family/ .obsidian/ .obsidian/ ``` Taiga runs 3-phase sync (order matters): 1. **syncthing** → phone changes land in bare repo first 2. **vault** → agent (mcpvault) changes land in bare repo 3. **syncthing** → agent changes propagate to phone ## Sync Algorithm (same for all clients) ```bash git add -A && git commit # commit local changes git fetch origin main # get remote state git merge origin/main --no-edit # 3-way merge (conflict → commit markers) git push origin main # push back ``` No stash. No GIT_DIR/GIT_WORK_TREE workarounds. ## Scripts | Host | Script | Type | Trigger | |--------|-------------------------------|---------------|----------------------| | Eagle | `~/scripts/sync-vault.sh` | full clone | Hermes cron */5 | | Kraken | `~/scripts/sync-vault.sh` | sparse clone | host crontab */5 | | Taiga | `/opt/data/sync-vault.sh` | 3-phase orch. | Hermes cron */5 | All clients share a common library: `sync-vault-lib.sh` (same dir as sync-vault.sh). Source of truth for scripts: `~/Developer/vault-sync-test/scripts/` on Eagle. ## Taiga Container Mounts | Container path | Host path | Purpose | |----------------------|----------------------------------------------------|--------------------------| | `/vault` | `/mnt/RED_2TB/storage/obsidian` | sparse clone (agent rw) | | `/vault.git` | `/mnt/RED_2TB/storage/git/obsidian-vault.git` | bare repo (git remote) | | `/obsidian-syncthing`| `/mnt/RED_2TB/storage/obsidian-syncthing` | full clone (phone sync) | | `/opt/data` | `/mnt/RED_2TB/docker/hermes/config` | Hermes config + scripts | `/vault` is a sparse clone with `remote = file:///vault.git`. `/vault.git` is hidden from the agent (mcpvault points to `/vault`). ## Sparse Checkout Paths Taiga and Kraken check out only: ``` personal/ family/ .obsidian/ ``` `work/` and `wiki/` are never checked out on sparse clients — they never touch those paths. ## Deploy a Script Fix ```bash # Edit on Eagle: ~/Developer/vault-sync-test/scripts/sync-vault-lib.sh # common algorithm ~/Developer/vault-sync-test/scripts/sync-vault.sh # Taiga 3-phase orchestrator # Deploy to Taiga: scp ~/Developer/vault-sync-test/scripts/sync-vault-lib.sh \ ~/Developer/vault-sync-test/scripts/sync-vault.sh \ truenas_admin@mallexxx.duckdns.org:/mnt/RED_2TB/docker/hermes/config/ # Deploy to Kraken (via sudo): cat ~/scripts/sync-vault-lib.sh | ssh kraken "sudo tee ~/scripts/sync-vault-lib.sh > /dev/null" cat ~/scripts/sync-vault.sh | ssh kraken "sudo tee ~/scripts/sync-vault.sh > /dev/null" ``` ## .gitignore Plugin binaries excluded to avoid cross-device conflicts: ``` .obsidian/plugins/obsidian-git/main.js .obsidian/plugins/obsidian-git/styles.css .obsidian/plugins/obsidian-git/manifest.json ``` `data.json` (plugin config) is tracked — shared settings. ## Regression Tests `~/Developer/vault-sync-test/test-sync.sh` — 16 scenarios (A–P): covers Cyrillic filenames, moves, deletes, conflicts, work/ phantom-delete, 3-phase sync. ```bash cd ~/Developer/vault-sync-test && bash test-sync.sh ``` ## Pitfalls - `git add pathspec` fails with `fatal: pathspec did not match any files` when a directory doesn't exist on disk — use `git add -A` instead - `git add -A` in sparse worktree still stages deletions of out-of-cone files tracked in index — need to unstage them (fixed in old script; irrelevant with proper clone) - `core.quotePath=true` (default) escapes Cyrillic paths in `git ls-files` output — use `-c core.quotePath=false` - ZFS on TrueNAS blocks `chmod` — `git init` fails from host; must run from inside Docker container or create `.git` structure manually ## History - **2026-06-20**: Kraken ACL fix. mcpvault (Hermes контейнер, uid 10000) не мог писать в существующие файлы vault из-за прав (EACCES). Подробно: **Проблема 1 — права доступа:** файлы в vault создаются от uid 1000 (kraken через git на хосте), а mcpvault в контейнере работает от uid 10000. `patch_note` падал: `EACCES: permission denied`. `delete_note` + `write_note` срабатывали, но mcpvault при write перезаписывает файл целиком, переформатируя YAML фронтматер (убирает кавычки, меняет стиль) — шум в git-diff. **Проблема 2 — экранирование backtick в patch_note:** mcpvault `patch_note` не может найти строку, содержащую тройные обратные кавычки (`` ``` ``). Пришлось якориться на соседнюю строку без backtick — например, на `\n\n## Заголовок` вместо `\`\`\`\n\n## Заголовок`. **Решение — ACL ext4 (setfacl):** ```bash sudo setfacl -R -m u:10000:rwx /srv/.../obsidian # mcpvault — write sudo setfacl -R -d -m u:10000:rwx /srv/.../obsidian # default для новых файлов sudo setfacl -R -m g:kraken:rwx /srv/.../obsidian # kraken (uid 1000) — write sudo setfacl -R -d -m g:kraken:rwx /srv/.../obsidian # default для новых файлов ``` **Итоговая совместимость:** uid 10000 (mcpvault) и uid 1000 (kraken/git) могут читать и писать одни и те же файлы. `patch_note` работает без EACCES. **Ограничение (mcpvault patch_note + backtick):** `patch_note` не может найти строку, содержащую тройные обратные кавычки (`` ``` ``). Пример: нужно было вставить новый раздел между closing code fence `\`\`\`` и заголовком `## Исправления (v0.0.92...`. Варианты `oldString`, которые **НЕ сработали**: - `"}\n\n\`\`\`\n\n## Исправления (v0.0.92..."` — не найдено - `"\`\`\`\n\n\n## Исправления (v0.0.92..."` — не найдено - Любая комбинация пустых строк между `\`\`\`` и заголовком — не найдено **Что сработало:** якориться на соседнюю строку **без** обратных кавычек. Вместо `\`\`\`\n\n## Исправления...` использовать `\n\n## Исправления (v0.0.92...` — замена пустой строки + заголовка, куда перед заголовком вставляется новый контент, а сам заголовок остаётся. `patch_note(oldString="\n\n## Исправления (v0.0.92...")` — найдено и отработало. - **2026-05-23**: Taiga deleted 32+ wiki files. Root cause: `git add -A` before merge staged deletions of files outside sparse cone. Fixed with scoped `git add`. - **2026-06-01**: "Керамика" lost from gift-ideas. Root cause: `git add $SPARSE_PATHS` failed silently when `.obsidian/` absent — patch+move not committed. - **2026-06-02**: Full architecture migration. Taiga `/vault` converted from `GIT_DIR=bare GIT_WORK_TREE` workaround to proper sparse clone. All clients unified on same `commit→fetch→merge→push` algorithm. Sync interval: every 5 min.