Skip to content

ADR 0002 — chflags uchg only on data/, not on subdirectories

  • Status: Accepted
  • Date: 2026-04-16 (consolidated from scripts/protect-data.sh header + 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

  • uchg on 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 immutable data/).
    • git clean -fdx → blocked (can't remove data/).
    • mv data/ elsewhere/ → blocked.
    • Apps writing inside data/immich/upload/… → allowed.
    • Apps creating data/loki/tsdb-shipper-cache/allowed (because data/loki/ is not flagged, only data/ 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 lockchflags uchg data/.
  • scripts/protect-data.sh unlockchflags nouchg data/.
  • make init runs lock automatically after first-time setup.
  • make protect / make unprotect / make data-status are the day-to-day knobs.

Consequences

  • macOS only. Linux has chattr +i with similar semantics, but this script is BSD-flavoured (chflags). When migrating to WSL2 Linux, rewrite with chattr or pick a different mechanism (read-only bind mount, snapshots).
  • CI workaround needed. e2e-tests.yml patches the script to no-op chflags on Ubuntu. Tracked in known_issues.md #6.
  • .gitignore + .gitignore inside data/ + make clean (only prunes Docker) remain as defense-in-depth on top of this.

MIT License