ADR 0002 — chflags uchg only on data/, not on subdirectories
- Status: Accepted
- Date: 2026-04-16 (consolidated from
scripts/protect-data.shheader +docs/architecture_and_data_safety.md)
Context
All runtime state lives in data/ (photos, databases, certs, logs). A misplaced rm -rf, git clean -fdx, or mv can destroy years of data in seconds. Docker volume protection alone is not enough — the host filesystem is vulnerable.
We wanted a lightweight, reversible mechanism that:
- Blocks deletion of the data directory and its top-level subdirectories.
- Does not break Docker containers that need to create runtime files inside those subdirectories (Loki's
tsdb-shipper-cache/, Grafana's state, Immich's thumbnail tree, etc.). - Doesn't require root, third-party tools, or filesystem-level snapshots.
Decision
Set the BSD immutable flag (chflags uchg) on only the top-level data/ directory. Do not recurse into subdirectories.
Reasoning
uchgon a directory means "you cannot add/remove entries in this directory." It does not stop writes inside files or subdirectories.- Flagging only
data/gives:rm -rf data/→ blocked (can't delete an immutable directory).rm -rf data/immich→ blocked (can't remove an entry from an immutabledata/).git clean -fdx→ blocked (can't removedata/).mv data/ elsewhere/→ blocked.- Apps writing inside
data/immich/upload/…→ allowed. - Apps creating
data/loki/tsdb-shipper-cache/→ allowed (becausedata/loki/is not flagged, onlydata/is).
- If we flagged every subdirectory, services like Loki and Grafana would crashloop because they can't create new runtime directories.
Implementation
scripts/protect-data.sh lock—chflags uchg data/.scripts/protect-data.sh unlock—chflags nouchg data/.make initrunslockautomatically after first-time setup.make protect/make unprotect/make data-statusare the day-to-day knobs.
Consequences
- macOS only. Linux has
chattr +iwith similar semantics, but this script is BSD-flavoured (chflags). When migrating to WSL2 Linux, rewrite withchattror pick a different mechanism (read-only bind mount, snapshots). - CI workaround needed.
e2e-tests.ymlpatches the script to no-opchflagson Ubuntu. Tracked in known_issues.md #6. .gitignore+.gitignoreinsidedata/+make clean(only prunes Docker) remain as defense-in-depth on top of this.