Files
obsidian-vault/family/how-to/vault-git-sync.md
T

12 KiB
Raw Blame History

title, updated, type, tags
title updated type tags
Vault Git Sync — Architecture & Scripts 2026-09-02 tech
vault
git
sync
infra
obsidian

Vault Git Sync

STATE 2026-09-02 (позднее обновление): Eagle/bare repo снова синхронны (ahead=0/behind=0, 2fab879). Ранее (см. ниже) git-sync стоял из-за битых прав storage/gitпочинено полной dacl-заменой (family/documents/vault-sync/2026-09-02-restore-privilege-scope.md §RESOLVED).

STATE 2026-09-04: починена потеря git identity в контейнере Taiga (слетела при рекреации после crashloop 09-02) — коммиты молча не проходили ~1.5 суток, изменения телефона не доезжали до bare. Фикс: sync-vault.sh теперь идемпотентно ставит user.name "Taiga" / user.email "taiga@hermes" при каждом запуске. Накопленное закоммичено (2b45264). Подробно: family/documents/vault-sync/2026-09-04-git-identity-loss-silent-commit-failure.md.

НОВАЯ проблема 2026-09-02 (не решена): оба Taiga-клиента (/storage/obsidian и /storage/obsidian-syncthing) отстали на месяц — HEAD ceea848 (2026-08-01) vs bare repo 2fab879 (2026-09-02). Причина: контейнер hermes-taiga в crash-loop (16339 рестартов) — uv run hermes gateway run не может докачать депы (pytz/markdown-it-py) с PyPI, т.к. весь HTTP идёт через мёртвый socks5://vless-proxy:1080, а UV_CACHE_DIR=/tmp/uv-cache пустеет на каждом рестарте. Taiga 3-phase sync (крон внутри агента) не выполняется => телефон видит устаревший vault. Подробно: family/documents/vault-sync/2026-09-02-hermes-taiga-crashloop.md. Фикс НЕ применён (ждёт Alex).

Триггер cron на маке (Eagle) — это launchd, НЕ Hermes cron: ~/Library/LaunchAgents/com.sync-vault.plist/Users/admin/scripts/sync-vault.sh, StartInterval=300 (5 мин). Скрипт sync-vault-lib.sh намеренно глушит dead remote (git fetch fail → exit 0), поэтому launchd показывает last exit code = 0 даже при фактически стоящем sync. Системный crontab на маке пуст.

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)

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 launchd com.sync-vault (⚠️ НЕ Hermes cron) StartInterval=300
Kraken ~/scripts/sync-vault.sh sparse clone host crontab */5
Taiga /opt/data/sync-vault.sh 3-phase orch. Hermes cron */5

УТОЧНЕНО 2026-09-02 (Eagle/мак sync-механизм): автосинка на маке запускается НЕ Hermes cron, а launchd-агентом ~/Library/LaunchAgents/com.sync-vault.plist:

  • Программа: /Users/admin/scripts/sync-vault.sh, StartInterval = 300 сек (5 мин)
  • launchctl list → активен, runs = 3762, last exit code = 0
  • ⚠️ sync-vault-lib.sh умышленно глушит недоступность remote: если git fetch падает → логирует Fetch failed (...) и exit 0. Поэтому launchd показывает exit code 0, даже когда sync фактически сломан (см. случай ahead 55 при сломанном bare repo после restore). Это не отсутствие cron и не сбой — диагностировать нужно по git status (ahead count), а не по exit-code агента.
  • Системный crontab и /etc/crontab на маке отсутствуют.

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

# 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 (AP): covers Cyrillic filenames, moves, deletes, conflicts, work/ phantom-delete, 3-phase sync.

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 chmodgit init fails from host; must run from inside Docker container or create .git structure manually
  • NFS4 owner@ DENY READ_DATA ACL breaks push (fake "corrupt" objects) — see History 2026-09-02. Key: after a TrueNAS pool restore, a subset of loose git objects in the bare repo can carry an inverted NFS4 ACL owner@ type=DENY READ_DATA=True. In NFSv4 a DENY overlays ALLOW, so the owning uid (e.g. 950) can't mmap-read those objects → on receive, git-receive-pack/index-pack reports loose object ... is corrupt and rejects push, even though data is intact (git fsck --full under root is clean). Fix = filesystem.setacl <repo> ... {stripacl:true} (chmod/chown do NOT remove DENY ACEs). Tell-tale: POSIX file mode 40 (r--------) on loose objects vs normal 750.

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):

    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.