Troubleshooting
Start with doctor
safekeep doctorChecks: fswatch presence, version and the monitor for your OS (fsevents_monitor on macOS, inotify_monitor on Linux), global config and every .sync syntax, no destination inside a source (copy loop), python version, permission errors in the log from the last 24h (0.3.1), TCC + launchd plist lint (macOS), the running daemon's executable still on disk (0.5.1, macOS), inotify watch limit + systemd user unit + linger (Linux), WSL detection (0.4.0, best-effort), leftover .safekeep.tmp.* files. Exit 1 on a fatal failure; every check described below is non-fatal.
Full Disk Access (FDA)
FDA (Full Disk Access) is the TCC permission that opens the whole disk. Without it, safekeep cannot read protected folders.
Grant it to both:
- the Python interpreter running safekeep (
/usr/bin/python3or your Homebrew python) — System Settings → Privacy & Security → Full Disk Access; fswatch, when it runs as a separate binary.
Granting it to the terminal is not enough for the launchd daemon: the job inherits the login context, not the terminal's per-session consents.
Symptoms: EACCES / Operation not permitted in the log, projects missing in auto-discovery mode.
After changing permissions, rebuild the agent — a loaded launchd job keeps its old context:
safekeep daemon stop && safekeep daemon start
# (≡ launchctl bootout + bootstrap on the rendered plist)Daemon runs from a deleted executable (Homebrew upgrade)
Root cause of the ~45h EPERM incident (03/10/2026), fixed in 0.5.1. Until 0.5.0 install.sh rendered the agent's Python as /opt/homebrew/bin/python3, which Homebrew resolves into its versioned Cellar folder:
03/10 18:54 launchd starts the daemon (exe = Cellar/python@3.14/3.14.6/…)
03/10 21:20 brew upgrade: python 3.14.6 → 3.14.8 DELETES the old Cellar
folder → the executable under the live process is gone
hours later TCC can no longer identify the process
(proc_pidpath_audittoken() failed) → removable volumes denied
→ EPERM on EVERY copy, 5.251 identical ERROR log lines
recovery re-grant permissions + launchctl kickstart -kSymptoms: doctor's "recent permission errors" exploding, every copy failing with Operation not permitted while the permissions themselves look fine, fswatch still delivering events normally.
Detection (0.5.1): doctor reads the live process (launchctl print + ps -o comm=) and warns when that executable is gone — it works even with an old, fragile plist still installed:
✗ exe del daemon: /opt/homebrew/…/Python NON esiste su disco — il processo
gira su un eseguibile cancellato … → launchctl kickstart -k
gui/$(id -u)/com.safekeep.agent — warning non fataleFix: re-run the installer (idempotent) and reload the job.
safekeep daemon install # re-render the unit (idempotent; does not start)
safekeep daemon stop # bootout / disable (exit 1 if it was not loaded — fine)
safekeep daemon start # bootstrap + start
./install.sh --trend # same treatment for the hourly trend agentSince 0.7.0 the main plist has no interpreter at all: ProgramArguments is the safekeep shim → run, so this incident class no longer applies to the main agent (verify with ps -p <pid> -o comm=: it must print an executable still on disk). The --trend agent still renders __PYTHON__ = /usr/bin/python3 — the Apple-managed system shim no Homebrew upgrade can delete. launchd does not re-read a plist on its own: without bootout+bootstrap (or a reboot) the old rendering keeps running. If Full Disk Access was granted to a Homebrew python, grant it to /usr/bin/python3 too — a one-time step, that path never changes again. A shell wrapper that re-resolves python3 at every start was considered and rejected: it would only resolve at start, recreating the same incident on the next upgrade while the daemon keeps running (SPEC §8.4).
Recent permission errors in the log
Since 0.3.1 doctor counts permission errors in the current log (~/.local/state/safekeep/safekeep.log) from the last 24 hours — the follow-up to the incident where a volume lost its permissions and 223 copies started failing with EPERM:
✗ errori di permesso recenti: 223 nelle ultime 24h (ultimo: 2026-10-04 18:57:29)
— log: ~/.local/state/safekeep/safekeep.log — warning non fatale: …Matched lines carry [Errno 1] Operation not permitted or [Errno 13] Permission denied. The check is non-fatal (doctor still exits 0): it is a recent symptom, not a system state. What to do:
- what is still waiting:
safekeep sync-once --dry-run; - re-grant the permissions — the destination volume on macOS, or FDA for protected folders, or the stable interpreter if the daemon runs from a deleted executable;
- the log path is printed with the warning.
Since 0.5.1 repeated failures for the same (path, errno) are throttled: one line per 300-second window, later lines suffixed — ripetuto N volte, instead of one line per failure (the incident wrote 5.251 identical lines in two days). The count stays honest — doctor adds N + 1 for every suffixed line, so count_24h is still the true number of failures, just with far fewer lines on disk.
Limits, on purpose: only the current log is read (rotations .log.1 … .log.5 are not), a line whose timestamp cannot be parsed is not counted (so the number is a minimum), and a missing log skips the check.
To follow the trend instead of looking at a single run, use safekeep doctor --json: the same data in structured form (permission_errors.count_24h and permission_errors.last) — and bin/safekeep-trend.sh already appends one line per run to ~/.local/state/safekeep/permission-trend.csv (hourly agent included), so the trend is a CSV away — see Commands.
Linux: inotify watch limit
On Linux fswatch watches trees through inotify (FSEvents on macOS). One watch per directory, capped per user:
sysctl fs.inotify.max_user_watchesdoctor warns below 16384 (non-fatal): a home tree of ~25k files runs out, the extra watches are silently dropped and new events stop arriving — files already backed up stay safe, and the 24h reconcile still catches up. Raise it:
sudo sysctl -w fs.inotify.max_user_watches=524288 # this boot
echo 'fs.inotify.max_user_watches=524288' \
| sudo tee /etc/sysctl.d/50-safekeep.conf # and at every bootsystemd linger
The systemd user unit starts with the login session only: without linger it runs after the first login, not at boot.
safekeep daemon install --enable-linger # runs it for you (best-effort)
loginctl enable-linger $USER # or by hand, no root needed
loginctl show-user $USER -p Linger # yes / noBy default daemon install only prints the hint; with --enable-linger it runs loginctl enable-linger $USER itself (a failure there is a warning, not an error).
Since 0.3.1 doctor checks linger whenever ~/.config/systemd/user/safekeep.service is installed, and warns (non-fatal) when it is off:
✗ linger: NON abilitato — l'agent non partirà al boot senza login
→ loginctl enable-linger $USER — warning non fataleIt reads /var/lib/systemd/linger/ directly — the same truth as loginctl, without root. No systemd at all → the check is skipped; no unit installed → it is not shown. See Agent.
Windows: WSL (best-effort)
There is no native Windows build: run safekeep inside WSL (Windows Subsystem for Linux), where it is a plain Linux program. Honesty first: not yet tested on a real WSL machine — 0.4.0 ships detection and graceful degradation only; the Linux runtime it inherits (systemd user unit, inotify, fswatch) is the 0.3.0 one, E2E-tested on real Linux.
doctor prints these lines when it detects WSL (WSL_DISTRO_NAME set, or a microsoft kernel) — all three are non-fatal:
✔ WSL rilevato: Ubuntu (supporto best-effort, SPEC §17)no systemd user manager (WSL1, or WSL2 with systemd off) → a warning with the fix — enable it and restart WSL:
ini# /etc/wsl.conf (from Windows: wsl --shutdown, then reopen the distro) [boot] systemd=true…or skip the agent and run the daemon by hand:
safekeep run.sources/destinations under
/mnt/…→ a drvfs note: that filesystem is slow (9p translation) and case-insensitive by default — prefer a native ext4 destination inside the distro (e.g.~/backup) over/mnt/c/....
safekeep daemon install follows a stricter rule since 0.7.0: without systemd the preflight fails (exit 1) with the fix above and the manual-start alternative (safekeep run) — no unit is written. (install.sh, pre-0.7.0, used to install nothing and exit 0; the wrapper now delegates to the new command.) safekeep doctor stays exit 0 on WSL without systemd: all three WSL checks are informational.
safekeep daemon install fails (preflight)
daemon install writes nothing and exits 1 when one of these is missing — each line comes with its fix:
fswatchmissing — install it for your platform (the error prints the right command:brew/pacman/apt/dnf). The unit'sPATHcan only find binaries, never install them.shim safekeep not found on PATH—ExecStartis the absolute path of thesafekeepshim resolved onPATHat install time. Withpipx install safekeepit is~/.local/bin/safekeep; withpip install --user safekeepmake sure~/.local/binis on thePATHof both your login shell and the service environment (put it in~/.profile/~/.zprofile, then open a new session). A shim that moves after install only breaks at the nextdaemon start— re-rundaemon installto re-resolve.- WSL without systemd — see Windows: WSL.
- python < 3.9 / unsupported OS — upgrade python; safekeep needs macOS or Linux.
Nothing was written when this happens: fix the reported item and re-run safekeep daemon install. Use --dry-run to check the preflight without touching anything.
Nothing is copied
Is there a
.syncin the project root? No.sync→ not followed.Does any rule match the file? It is an allow-list: no matching include → not copied. Check with:
bashsafekeep sync-once --dry-run --project ~/Code/myappIs a parent directory excluded? An excluded directory prunes its subtree.
Is
dest:set in~/.safekeep? At least onedest:ors3_bucket:is mandatory.
Logs
tail -f ~/.local/state/safekeep/safekeep.log # both platforms (rotating, 5 x 5 MB)
journalctl --user -u safekeep -f # Linux: what the unit printsRaise verbosity with safekeep run -v (foreground) or log_level: debug in ~/.safekeep.
Destination shows pending
The destination volume is not mounted. safekeep retries the mountpoint stat with exponential backoff (1s → 60s cap) and reconciles that project as soon as the volume is back. Check with safekeep status.
Leftover .safekeep.tmp.* files
A crash during a copy. They are removed on the next copy to that destination; doctor lists any that remain, and they are safe to delete manually.