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

291 lines
30 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: Vault Git Sync — Architecture & Scripts
updated: '2026-09-22'
type: tech
tags:
- vault
- git
- sync
- infra
- obsidian
---
# Vault Git Sync
> **✅ STATE 2026-09-26 (поправка Taiga, 09:50 UTC) — «раз в час» было ошибкой чтения: sync идёт каждые 5 минут.**
> Блок ниже («РАСХОЖДЕНИЕ РАСПИСАНИЯ», «задержка до 60 минут») построен на поле `schedule_display: "0 * * * *"`
> в `/opt/data/cron/jobs.json` — это **устаревшее поле**, диспетчер его не использует. Факты:
> - `jobs.json` → `schedule.expr = "*/5 * * * *"`, `display = "*/5 * * * *"` (устарело только `schedule_display`);
> - `executions.db`, job `29851c413903`, 1001 тик за 09-22 22:25 → 09-26 09:40 UTC: интервалы **985 × 5 мин**,
> 14 × 4 мин, 1 × 1 мин → **288,6 тика/сутки**, а не 24. Тик «раз в час» дал бы 98 записей за это окно;
> - `/tmp/vault-sync.log` — timestamped `Pushed ✓` каждые 5 минут.
>
> `hermes cron list` печатает то же устаревшее поле (`Schedule: 0 * * * *`), поэтому каденцию проверять
> по `executions.db` или `jobs.json:schedule.expr`, а не по выводу CLI. Задержка «до 60 минут» бумажная;
> реальная доставка до телефона — в пределах тика. Перезапись `schedule_display` через
> `hermes cron edit 29851c413903 --schedule '*/5 * * * *'` — на решение Алексея (косметика, на исполнение не влияет).
> **🔴 STATE 2026-09-26 (later) — `obsidian-syncthing` НЕ получает обновления: root-owned objects + битый remote.**
> Нота `1-needs-decision.md` была запушена с Mac (`66d4d62..f2501dc`), но в клон
> `/obsidian-syncthing` **не доехала**. Два независимых дефекта:
> **(1)** `origin` в клоне был **`/vault.git`** (потерян префикс пути) → `git fetch` падал
> `does not appear to be a git repository`. Переписан на полный путь `git remote set-url`.
> **(2)** **4051 из 4055 объектов принадлежат `root`** — контейнер `hermes-taiga` работает от
> root (`Config.User` пусто, `id` → `uid=0`), он и делает pull → новые объекты root-owned,
> git под uid 950 не может писать (`insufficient permission for adding an object`).
> **ACL на `obsidian-syncthing` НЕ СТОИТ** — `nfs4xdr_getfacl` показывает `trivial_acl: true`
> (только `owner@`/`group@`/`everyone@`, ни одной именованной записи). Рекурсивный ACL с группой
> ставился ранее (2026-09-23), но на **`/mnt/RED_2TB/Edu`**, не на эту папку.
> 🔴 **Урок: «ACL уже ставили» ≠ «ACL стоит на этой папке» — проверять фактом.**
> Разовый `chown` лечит только уже существующие объекты; следующий pull от root возвращает дефект.
> Фиксы НЕ применены. Подробно: `family/documents/vault-sync/2026-09-26-syncthing-clone-root-objects-acl-confusion.md`.
>
> **🔴 STATE 2026-09-26 — РАСХОЖДЕНИЕ РАСПИСАНИЯ: Taiga-сервер синкается РАЗ В ЧАС, не 5 минут.**
> Job `sync-vault` внутри контейнера `hermes-taiga` (Hermes cron, id `29851c413903`) расписан
> **`0 * * * *`** — то есть раз в час, **хотя таблица ниже (и `family/how-to/obsidian-sync`)
> заявляет «*/5»**. Обнаружено при проверке «доехал ли док до телефона»: ноты, залитые с Mac
> в 14:32, в клоне `obsidian-syncthing` отсутствовали.
>
> **Следствие:** изменения с Eagle/Kraken доезжают до телефона с задержкой **до 60 минут**.
> Цепочка `bare repo → git-клон obsidian-syncthing → Syncthing → телефон` рвётся на **втором**
> шаге (`Syncthing здоров` ≠ `док доехал`).
>
> **Ручной триггер (проверено):**
> ```bash
> docker exec hermes-taiga sh -c 'cd /opt/data && HERMES_HOME=/opt/data \
> /opt/hermes/.venv/bin/hermes cron run 29851c413903'
> # → Triggered job: sync-vault … Ran now: succeeded.
> ```
> После триггера: клон обновлён, Syncthing `needFiles: 0`, телефон `completion: 100`.
>
> **Открытый вопрос к Alex: менять расписание на `*/5 * * * *` или оставить раз в час?**
>
> ⚠️ **Диагностика синка — только изнутри контейнера Syncthing:** порт `8384` слушает лишь
> в контейнере, `curl` с хоста TrueNAS даёт пустоту.
> ```bash
> docker exec syncthing sh -c 'K=$(grep -oE "<apikey>[^<]+" /var/syncthing/config/config.xml | sed "s|<apikey>||"); \
> curl -s -H "X-API-Key: $K" "http://localhost:8384/rest/db/status?folder=k5vyy-gzdgj"'
> ```
> Папка Syncthing: id `k5vyy-gzdgj` «Obsidian Vault», устройства `SM-S931B` (телефон) и `syncthing-taiga`.
> Готовый скрипт: `~/Developer/personal-os/tools/music-tags/check_syncthing2.sh`.
> **✅ STATE 2026-09-22 — рецидив ACL закрыт перманентно.** Дефект вернулся (35 root-каталогов),
> push с Mac падал. Починено: **236/236 каталогов `INHERIT` + `WRITE`** через
> `midclt filesystem.setacl` (read-modify-write + ожидание job). **Наследование подтверждено
> ФАКТОМ** — `mkdir objects/zzz` → новый ACL унаследован (`INHERITED:True`), `touch` внутрь OK.
> End-to-end push: `ba3780c..8b831a8 main -> main` ✅. Поставлен **cron-сторож** (каждые 2 ч):
> `~/scripts/vault-acl-watchdog.py` на TrueNAS — SILENT когда всё ок, чинит + докладывает когда
> сломано. Скрипты: `/Users/admin/Developer/vault-tools/`.
> 🔴 **Корень НЕ устранён:** контейнер `hermes-taiga` работает от root
> (`HERMES_ALLOW_ROOT_GATEWAY=1`, `User:` пусто) → `mcpvault /vault` пишет root-owned объекты.
> Перманентный фикс — `user: "950:950"` в `taiga` service
> `/mnt/RED_2TB/docker/hermes/docker-compose.yml` (риск: образ может требовать root для `/opt/data`).
> Сторож пока закрывает последствия. Подробно:
> `family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md`.
>
> **STATE 2026-09-21 — bare repo push починен (наследуемый ACL).** `git push nas` падал `Permission denied` на `objects/XX`: 28 каталогов были root-owned (создавал `mcpvault` **второго Hermes на TrueNAS, от root**), 13 каталогов без owner-`x`. Фикс: **полный dacl-REPLACE с `FILE_INHERIT`+`DIRECTORY_INHERIT`** через `midclt filesystem.setacl` — 291 каталог, 0 ошибок, без root-пароля. Push прошёл дважды, Mac=`nas`=`3b923d3`. ⚠️ Прямой write-тест `touch` в `objects/` **не выполнялся** → наследование подтвердит только следующий push root-инстанса. Подробно: `family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md`.
>
> **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`.
>
> **STATE 2026-09-26 — `pipefail` + `grep -q` молча отменял коммиты в `sync-vault-lib.sh`.** При большом
> числе изменений (492 файла ≈ 23 КБ вывода) `git status --porcelain | grep -q .` падал по `SIGPIPE`
> (grep выходил после первого совпадения, git дописывал в закрытый pipe), `set -o pipefail` объявлял
> pipeline упавшим (exit 141) → ветка `else` → **коммит не создавался вообще**, при этом тик печатал
> `Pushed ✓` ×3 и exit 0. Так застряли 491 mode-flip и правка телефона (`1-needs-decision.md`, 4 чекбокса
> `[x]`). Фикс: условие через `STATUS="$(git status --porcelain)"` + `[ -n "$STATUS" ]`, сообщение
> коммита — одним `awk` (первые 25 путей, дальше `(... +N more)`). ⚠️ `awk | head -25` в подстановке
> даёт тот же SIGPIPE → тик падает с exit 141, так делать нельзя. Проверено: коммит `12a7566e` (492 файла),
> три SHA равны, `git status` пуст. Разбор: `family/documents/vault-sync/2026-09-26-commit-skip-pipefail.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)
```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 | **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 * * * *`** — job `29851c413903`. ⚠️ `hermes cron list` и поле `schedule_display` в `jobs.json` показывают устаревшее `0 * * * *` (см. поправку в STATE 2026-09-26 выше); фактические тики — каждые 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`).
`/obsidian-syncthing` is also a clone with `remote = file:///vault.git`, но **его `.git/` сам синкается
Syncthing'ом с других устройств**, и `remote.origin.url` периодически перезаписывается чужим адресом —
хост-путём `/mnt/RED_2TB/storage/git/obsidian-vault.git` (2026-09-26 09:36) или Gitea-URL
`https://git.mallexxx.duckdns.org/git_admin/obsidian-vault.git` (версия с телефона). Внутри контейнера
не резолвится ни один: `git fetch origin main` → `fatal: … does not appear to be a git repository`,
`sync-vault-lib.sh` печатает `Fetch failed (origin offline?) — skipping merge/push` и выходит **с exit 0**,
поэтому фазы 1 и 3 **молча не выполняются** (правки телефона не доезжают до bare, правки агента — до телефона),
а cron рапортует «успех». С 2026-09-26 09:42 `sync-vault.sh` в начале каждого тика принудительно ставит
`origin = file:///vault.git` и добавляет `url."file:///vault.git".insteadOf` для обоих чужих адресов.
Разбор: `family/documents/vault-sync/2026-09-26-syncthing-clone-origin-url-flip.md`.
## 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
- **`nfs4xdr_getfacl`, а НЕ `getfacl`, если проверяешь наличие расширенного ACL** (2026-09-26). POSIX-вид (`getfacl`, `ls -l`) показывает `user::rwx group::rwx` и **скрывает** отсутствие именованных ACE. Только `nfs4xdr_getfacl` печатает `trivial_acl: true` — что значит «кастомного ACL нет». Все диагностики прав на TrueNAS начинать с него.
- **`chgrp` / `chown` от `truenas_admin` → `Operation not permitted`** (2026-09-26). В NFSv4 `group@` — это POSIX-gid файла, а не ACE: без смены gid группа не получает доступа. Менять gid/uid только через `midclt filesystem.setacl` (root-контекст). TrueNAS `sudo` требует пароль (в отличие от Kraken, где `sudo -n` работает).
- **Разовый `chown` — лечение симптома, не фикс** (2026-09-26). Если root-процесс продолжает писать в папку, root-owned объекты вернутся на следующем же запуске. Перманентно лечит только `ACL`+`INHERIT` (наследование новыми объектами) либо запуск процесса **не от root**.
- **«Этот фикс уже делали» — проверить, что он на ТОЙ САМОЙ папке** (2026-09-26). Рекурсивный ACL с группой `nas_users` ставился 2026-09-23, но на `/mnt/RED_2TB/Edu`. На `obsidian-syncthing` ACL не было никогда. Каждый «известный фикс» верифицировать фактом на целевом объекте, а не памятью. Искать прошлые сессии: `sqlite3 ~/.hermes/hermes-whale/state.db "SELECT session_id, substr(content,1,300) FROM messages WHERE content LIKE '%setfacl%' ORDER BY id DESC LIMIT 40;"`.
- **Клон может иметь битый `origin`** (2026-09-26). `obsidian-syncthing` имел `origin = /vault.git` (потерян префикс пути) → `fatal: does not appear to be a git repository`. Проверять `git remote -v` в клоне до диагностики прав — это независимый дефект, который выглядит как проблема доступа.
- **`sync-vault-lib.sh` глушит ошибки (`2>/dev/null` на fetch/merge)** → «Pushed ✓» / `exit 0` при фактически упавшем pull. Это уже второй раз, когда поломка синка была не видна в логе. Диагностировать по `git status`/`git log` в клоне, не по exit-code.
- `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
- **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 = **full dacl REPLACE** via `filesystem.setacl` (chmod/chown do NOT remove DENY ACEs). Tell-tale: POSIX file mode `40` (`r--------`) on loose objects vs normal `750`.
> ⚠️ **Don't reach for `stripacl`** (2026-09-21): it tears the ACL down to POSIX and does **not** remove a DENY in all cases. Always pass a complete `dacl` array — `setacl` treats it as a full replace.
- **`Permission denied` writing `objects/XX/` (NOT the corrupt-object case)** — see History 2026-09-21. Two defects: (a) dirs with owner `rw-` and **no `x`** (`drw-r-x---`), (b) `objects/XX/` dirs **owned by `root`** with `group@:r-x` — git runs as uid 950 → cannot write. Root cause is `mcpvault` (a **second Hermes on TrueNAS, `/opt/hermes`**, vault `/vault`) running **as root** and creating root-owned objects in the shared bare repo. Requires `midclt` (see below) — `chown`/`chmod` as `truenas_admin` give `Operation not permitted` because uid 950 isn't the owner, and TrueNAS `sudo` **needs a password** (unlike Kraken, where `sudo -n` works). Fix must include **`FILE_INHERIT`+`DIRECTORY_INHERIT`** or the defect returns on the next push by the root instance. Recipe: `personal/tech/truenas-nfs4-acl-and-arrmultiuser.md` §«Durable fix with INHERITANCE».
## ACL repair via `midclt` (no root password needed)
`truenas_admin` is FULL_ADMIN in midclt → can read/write ACLs directly.
```bash
# READ — path as a BARE STRING
midclt call filesystem.getacl /mnt/RED_2TB/storage/git/obsidian-vault.git
midclt call filesystem.listdir /mnt/RED_2TB/storage/git/obsidian-vault.git/objects
# WRITE — ONE json object with "path" INSIDE it (opposite convention!)
midclt call filesystem.setacl '{"path":"/x","acltype":"NFS4","dacl":[…],
"options":{"recursive":false,"traverse":true,"stripacl":false}}'
```
Pitfalls: `setacl`/`setperm` are **async** (return a job id) — poll `core.get_jobs` until `SUCCESS`/`FAILED`, or the immediate `WAITING` reads as a failure. `filesystem.setperm` refuses when a non-trivial ACL is present (`[EINVAL] … Option "stripacl" required`).
## History
- **2026-09-26**: клон `/obsidian-syncthing` потерял рабочий remote — Syncthing переписал `.git/config` (mtime 09:36:19, owner uid 950 ≠ наши root-записи) на хост-путь `/mnt/RED_2TB/storage/git/obsidian-vault.git`. Первый за всю историю лога `Fetch failed` (тик 09:40:31) → фазы 1 и 3 молча скипались при `exit 0`; `/obsidian-syncthing` отстал на 1 коммит Eagle (`66d4d626` vs bare `5c939ed8`). Фикс: `sync-vault.sh` форсит `origin = file:///vault.git` каждый тик + `insteadOf` для хост-пути и Gitea-URL; after-fix три SHA равны `3edbddf5`, статусы пусты, новых `Fetch failed` нет. Корень (`.git` клона синкается Syncthing'ом — отсюда же `.sync-conflict-*` внутри `.git`) — на решение владельца. Подробно: `family/documents/vault-sync/2026-09-26-syncthing-clone-origin-url-flip.md`.
- **2026-09-26 (поправка каденции)**: снято ложное «Taiga синкается раз в час». `hermes cron list` и `jobs.json:schedule_display` показывают `0 * * * *`, но `schedule.expr = "*/5 * * * *"` и `executions.db` дают 985 интервалов по 5 мин из 1001 тика (288,6/сутки). Диагностировать каденцию — только по `executions.db`/`schedule.expr`.
- **2026-09-21**: bare repo push `Permission denied` on `objects/XX`. 28 dirs root-owned (created by `mcpvault` of a **second Hermes on TrueNAS** running as root) + 13 dirs with owner `rw-` and no `x`. `chown`/`chmod` as `truenas_admin` → `Operation not permitted` (TrueNAS sudo needs a password, unlike Kraken). Fixed with a **full dacl REPLACE + `FILE_INHERIT`/`DIRECTORY_INHERIT`** via `midclt filesystem.setacl` — **291 dirs, 0 errors**, no root password. Two clean pushes after; Mac = `nas` = `3b923d3`. ⚠️ Direct write-test in `objects/` was NOT run. Full log: `family/documents/vault-sync/2026-09-21-bare-repo-push-permission-denied.md`.
- **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.