Skip to content

v2.3.0 β€” Silent subtree loss fixed; Endpoint DLP guard

Choose a tag to compare

@Roberdan Roberdan released this 25 Aug 14:16
· 2 commits to master since this release

[2.3.0] - 2026-08-25

Added

  • Endpoint DLP guard β€” on a Mac managed with Microsoft Purview, copying an unlabeled
    Office file to removable media is vetoed by the endpoint agent: copyfile() returns EPERM
    and macOS raises a modal justification dialog. During an unattended hourly backup nobody
    answers it and the file is never copied. DLPGuard now decides before the copy is attempted,
    so the dialog never appears. A file is skipped only when the destination is removable media,
    the file is an Office document, and it carries no MIP sensitivity label (read from
    docProps/custom.xml in the OOXML package).
  • [dlp] config section β€” skip_unlabeled_office and skip_when_label_unknown, both
    defaulting to true. The TOML parser now understands booleans at all, which it previously did
    not.
  • dlp_skipped report category β€” skipped files are counted in status.json
    (files_skipped) and named in errors.json, kept apart from real errors so a skip never
    inflates the error total. A file absent from the backup is always explainable.

Fixed

  • Silent subtree loss in the scanner β€” FileScanner called enumerator.skipDescendants()
    for every entry matching an exclude pattern, files included. skipDescendants() skips the
    subdirectory the enumerator is about to descend into, so an excluded file sitting immediately
    before a real directory swallowed that entire subtree. Nothing failed and nothing was logged:
    the files were simply absent from every snapshot. A single stray .DS_Store was enough β€” on
    this machine it cost FDE_Update/zzArchive/ (6 files) and
    the-standing-egg/docs/archive/exports/ (4 files), and any *.log, *.tmp, *.pyc or
    *.jsonl in the wrong position would do the same. skipDescendants() is now called only for
    directories.
  • Misleading advice on DLP failures β€” these surfaced as permission_denied, whose
    suggested action is "check Full Disk Access". No local setting fixes a Purview policy, so
    the operator was sent chasing a fix that does not exist.
  • files_skipped never reported β€” the field was written to status.json but never
    assigned, so it always read 0 while errors.json listed skipped files.
  • Stale recovery installer on the backup disk β€” install.sh synced the .app and then
    copied "the newest .pkg lying around", which nothing ever rebuilt. The artifact whose
    entire purpose is restoring the app from the backup had sat at 1.0.0 since March while the
    installed app moved on: the one scenario it exists for would have handed back a five-month-old
    build. install.sh now builds the installer it ships (build-pkg.sh, which produces the app
    once and both artifacts from it), pins the copy to the version it just built rather than to a
    glob, and removes any older .pkg from the disk so there are never two with no way to tell
    which matches the .app beside them. build-pkg.sh also derives the version from build.sh
    instead of repeating the literal β€” the same drift, one level down.
  • Concurrency warnings in AppDelegate β€” four capture of 'self' with non-Sendable type
    warnings. AppDelegate touches AppKit and uiState throughout, both main-thread-only, so the
    isolation was already real and simply undeclared; the class is now @MainActor. runDiskutil
    is explicitly nonisolated, since handleEject calls it from a background queue precisely
    because it blocks. Builds clean.

Notes

  • The DLP check runs after the hard-link attempt, not before. DLP vetoes the copy, not the
    link, so a file already present in the previous snapshot is still linked into the new one and
    stays in the backup chain. Enabling the guard never evicts what is already backed up.