Cron jobs and the repository XDG environment

Research date: 2026-08-29 Scope: the cron renderer, managed jobs, runit integration, and realistic scheduler alternatives.

Decision

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.

Findings

Cron does not provide the login environment

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.

Repository impact

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.
  • Several Python jobs use the repository’s 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.

Independent P1 issue: Debian cron and -P

The 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:

  • Debian/Ubuntu cron: use cron -f (and Debian-supported options only).
  • Cronie: install and configure Cronie deliberately, rather than installing Debian cron under that name.

Options compared

1. Generated global XDG assignments — valid minimal patch

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:

  • No changes to job definitions or job scripts.
  • Supported by the targeted Debian/Vixie cron implementation.
  • Directly fixes all entries in this user crontab.

Costs:

  • The values are a snapshot taken at merge time; changing the environment file requires another merge.
  • The renderer must handle ordering, deduplication, and quoted absolute paths correctly.
  • It duplicates only part of .config/sh/env; PATH and future variables remain a separate concern.
  • The block affects unmanaged entries in the same crontab as well.

This is acceptable if the objective is the fewest changed lines.

3. Systemd user timers — a scheduler improvement, not an XDG fix

Systemd timers would provide useful operational features:

  • .timer schedules a matching .service.
  • systemctl --user exposes status and enables manual runs.
  • Service output can go to the journal.
  • Persistent=true can catch up one missed calendar run after downtime.
  • Service units give explicit dependencies, timeouts, and resource controls.

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.

4. Anacron or runit plus snooze

Anacron 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:

  • translating every cron expression into snooze flags;
  • generating one runit service per job, with runit restarting snooze after each command;
  • deciding whether the user supervisor must run at boot or only after the graphical sxrc session starts;
  • adding/installing snooze; and
  • retaining the same shared environment launcher.

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.

Sources

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.