Research date: 2026-08-29 Scope: the cron renderer, managed jobs, runit integration, and realistic scheduler alternatives.
Keep cron for now. Do not replace it merely to solve XDG environment initialization.
If “different tool” means a small, suckless-style Unix tool, the serious candidate is snooze, used under the repository’s existing runit supervision. It is not an official suckless.org project, but it is a small public-domain C tool explicitly designed to replace cron with runit. The official suckless tools list has no scheduler, while suckless.org lists snooze-like projects only as external “other projects.”
For this repository, I would still fix the environment boundary first and keep cron. The proper fix is one explicit boundary owned by the renderer: add a small shared launcher that sources ~/.config/sh/env, then make merge-crontab.sh route every generated job through it. This changes neither crontab-cmd nor crontab-pi, and adds no manually maintained XDG variables.
A generated crontab environment block is also valid and is the smallest patch, but the launcher is the better long-term boundary: it always reads the canonical environment at execution time and covers XDG, PATH, PYTHONPATH, and future variables together.
The cron contract supplies SHELL, HOME, and LOGNAME (and implementation-specific defaults such as PATH). It does not promise XDG_*, nor does it source .profile or .config/sh/env.
Both Vixie/Debian cron and Cronie support environment assignments in a crontab, so this is legal:
XDG_CONFIG_HOME=/home/user/.config
XDG_DATA_HOME=/home/user/.local/share
XDG_STATE_HOME=/home/user/.local/state
XDG_CACHE_HOME=/home/user/.cache
The XDG specification requires these paths to be absolute and defines the same $HOME-relative defaults when the variables are absent or empty. The repository’s shell consumers currently assume the variables are already present instead of applying those defaults themselves.
The affected path is broader than firmware_updates.sh:
firmware_updates.sh constructs $XDG_STATE_HOME/firmware-updates.
alert-on-fail.sh constructs $XDG_STATE_HOME/cron-logs.
mail.sh looks under $XDG_CONFIG_HOME/mail.
notify.sh constructs $XDG_DATA_HOME/sx/xauthority.
backup.sh uses all of XDG_CONFIG_HOME, XDG_DATA_HOME, and XDG_STATE_HOME.
run-job.py stores its database under XDG_DATA_HOME.
xdg.py, which already has correct $HOME fallbacks.
The current generator also relies on a nested /bin/sh -lc to obtain shell startup configuration. That is an implicit dependency, not a cron guarantee, and it is fragile when the profile is absent, changed, or the job is invoked through another path.
-PThe package manifest installs Debian’s cron, while setup/runit/cron/run starts /usr/sbin/cron -f -P.
-P is documented by Cronie, but Debian’s cron synopsis does not include it. On a normal Ubuntu/Debian installation this can make the runit cron service exit immediately. The service command and package choice must be made consistent before relying on any environment fix:
cron: use cron -f (and Debian-supported options only).
cron under that name.
merge-crontab.sh can source .config/sh/env, emit four absolute assignments at the top of the generated crontab, and remove/rewrite its own previous block on every merge.
Advantages:
Costs:
.config/sh/env; PATH and future variables remain a separate concern.
This is acceptable if the objective is the fewest changed lines.
Systemd timers would provide useful operational features:
.timer schedules a matching .service.
systemctl --user exposes status and enables manual runs.
Persistent=true can catch up one missed calendar run after downtime.
They do not automatically solve this problem. A systemd user manager does not generally source .profile; environment must still be supplied via Environment=, EnvironmentFile=, or ~/.config/environment.d/*.conf. The official environment.d documentation also says those files affect environments passed to managed services, not the manager’s own environment.
A migration would additionally need user-manager lifecycle decisions. To run while logged out, user lingering is normally required. GUI notifications need a graphical-session dependency and current display/authentication variables. The repository currently installs Debian cron and supervises system services with runit, so timers would add a second service model, schedule conversion, unit generation, and a careful cutover to avoid duplicate runs.
Systemd is worth reconsidering if journaled per-job status, missed-run persistence, dependencies, or resource controls become requirements. It is not the simplest fix for this P1.
snoozeAnacron is designed for daily, weekly, and monthly jobs and records whether a day-level job was missed. It cannot express the repository’s every-five-minute, every-minute, hourly, weekday-at-14:47, and twice-daily schedules as cleanly.
snooze is the best small-tool alternative. Its README explicitly describes using it with runit to replace cron. It has mnemonic time flags, no central daemon, no overlapping runs within one supervised service, time-file support, and no dependencies beyond a C99 toolchain at build time. This aligns much better with the suckless preference than systemd timers.
However, migrating this repository would require:
snooze flags;
snooze after each command;
sxrc session starts;
snooze; and
The last point is important: snooze changes scheduling and supervision, not process environment semantics. It does not remove the need to source .config/sh/env. It is a good future replacement if removing cron and central scheduling are goals, but it is not the smallest P1 fix.
setup/runit/cron/run so the installed Debian cron is started with supported options.
cron-env launcher under the repository’s .local/bin tree.
merge-crontab.sh’s generated command templates to prefix both job types with the absolute launcher path.
watch-crontab command with the same launcher.
crontab-cmd and crontab-pi as the declarative source of truth.
/bin/sh -lc only if its shell parsing is still needed; it should no longer be responsible for environment setup.
env -i HOME="$HOME" PATH=/usr/bin:/bin \
"$HOME/.local/bin/cron-env" sh -c \
'printf "%s\\n" "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_STATE_HOME" "$XDG_CACHE_HOME"'
Then inspect the rendered crontab and run one harmless generated command manually. Ensure no path becomes /cron-logs, /firmware-updates, /mail, or /sx/xauthority.
As defense in depth, standalone shell scripts that are intended to work outside the scheduler should eventually use the XDG defaults themselves. That is a separate hardening pass; it is not necessary to edit every managed job for this incident.
/etc/environment does not configure task environments.
OnCalendar, timer behavior, accuracy, and Persistent.
snooze.
The report’s repository-specific conclusions are derived from the checked-out files listed in the request and the related cron, environment, runit, and helper files.