From e5c9bc66136b905fb1ea913026d0b148092713eeb324bca95c18338ddf6da00a Mon Sep 17 00:00:00 2001 From: Claude Code Date: Thu, 25 Jun 2026 07:28:23 +0000 Subject: [PATCH] Add saiop-git-sync-sidecar skill: reusable pattern from the Obsidian vault sync --- .../saiop-git-sync-sidecar/SKILL.md | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 skills/saiop-infrastructure/saiop-git-sync-sidecar/SKILL.md diff --git a/skills/saiop-infrastructure/saiop-git-sync-sidecar/SKILL.md b/skills/saiop-infrastructure/saiop-git-sync-sidecar/SKILL.md new file mode 100644 index 0000000..ffee660 --- /dev/null +++ b/skills/saiop-infrastructure/saiop-git-sync-sidecar/SKILL.md @@ -0,0 +1,51 @@ +--- +name: saiop-git-sync-sidecar +description: "Pattern for keeping a container's data volume continuously synced to a Gitea repo via a small sidecar container — used for the Obsidian vault, reusable elsewhere." +version: 1.0.0 +author: Claude Code (SAIOP ops session) +license: MIT +platforms: [linux] +prerequisites: + env_vars: [] + commands: [docker, git] +metadata: + hermes: + tags: [SAIOP, git, sync, Gitea, pattern, sidecar] +--- + +# Git-sync sidecar pattern + +## When to use + +Any time an app's data volume should be continuously backed by a Gitea repo +(used for the Obsidian vault → `atu/saiop-knowledge`). Generalizes to any +app that just reads/writes files in a directory without needing to know +git exists. + +## The pattern + +A second, tiny container (`alpine/git`) mounts the **same volume** as the +main app and loops: `git pull` → check `git status --porcelain` → if dirty, +`add`/`commit`/`push` → `sleep`. The main app never needs to know about +git at all. + +## Gotchas hit building this + +1. **`rm -rf` on the mount point itself fails** ("Resource busy") — you can't + remove a bind-mounted directory, only its contents. Use + `find "$DIR" -mindepth 1 -delete`, or `git clone .` directly into + the already-existing empty directory instead of removing-then-cloning. +2. **UID mismatch silently breaks the main app's write access.** If the + sidecar runs as root (the `alpine/git` default) and the main app runs as + a non-root `PUID`/`PGID` (common for LinuxServer.io images), every file + the sidecar creates is unwritable by the main app. Set `user: "UID:GID"` + on the sidecar to match the main app's user — don't just chown once + after the fact, the mismatch comes back on every commit. +3. **Add a `.gitignore` for the app's own volatile state files** (Obsidian: + `workspace.json`, `cache/`) before the first commit, or every sync cycle + produces a noise commit even with zero real content changes. +4. **Don't hardcode a container's internal IP for inter-container access** — + IPs churn (confirmed twice on this stack already). Use the container + **name** if both containers share a network (Docker's embedded DNS + resolves it), which is why the sync sidecar talks to `gitea:3000` + directly rather than an IP.