Skip to content

zaktrue/safe-crontab

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

safe-crontab

A guarded, drop-in replacement for crontab <file>, built for humans and for unattended automation and AI agents that manage cron jobs.

Plain crontab <file> has no guard rails: it replaces the entire crontab in one shot, takes no backup, and gives no warning if the new file happens to be much smaller than what it is replacing. That is exactly how crontabs get wiped by accident. safe-crontab wraps the same install with a backup, a shrink guard, a syntax check, and a verification step, and refuses to proceed when something looks wrong.

Origin story

This tool exists because of a real incident. An automated agent needed to add a couple of lines to a crontab, so it wrote a small file containing just those new lines and ran crontab <file> on it. The file had never been seeded from crontab -l first, so instead of adding to the existing schedule it replaced the whole thing: every previously scheduled job, 363 lines of them, was gone in one command. There was no warning, no confirmation prompt and no backup taken by crontab itself.

The wipe was recovered from an independent backup that happened to exist for unrelated reasons, but the whole incident should never have been possible. The tool was written the next day with one goal: make that specific mistake, and the general class of mistake it belongs to, get refused before it happens rather than cleaned up after.

How it works

Running safe-crontab <file> performs five steps, in order:

  1. Lock. The whole operation is serialised with flock, so two installs started at the same time cannot race each other.
  2. Backup. The current live crontab is backed up to a timestamped file before anything else happens. On a true first install (no existing crontab for the user) there is nothing to back up, so no backup file is written. Old backups beyond a configurable count are pruned afterwards, oldest first.
  3. Shrink guard. If the new file has fewer lines than a configurable percentage of the current crontab's line count, the install is refused. This is the exact check that would have caught the incident above: a 363-line crontab being replaced by a handful of lines is precisely the shape of an accidental full replacement rather than a deliberate edit. The guard can be bypassed with --force-shrink when a large deliberate removal is genuinely intended. This step needs a reliable current line count to work from, so it is fail-closed: if crontab -l cannot be read for any reason other than "there is no crontab installed for this user yet" (the normal first-install case, where the current count is simply treated as zero), safe-crontab aborts rather than silently skipping the guard. Nothing is installed and the live crontab is left untouched.
  4. Syntax check. Where the local crontab implementation supports a dry-run flag (crontab -n), the new file is checked for syntax errors before anything is installed. Not every cron implementation supports this flag; where it is missing, the step is skipped with a warning rather than failing the run.
  5. Install and verify. The new file is installed with crontab. safe-crontab then attempts to read the crontab back with crontab -l and compares its hash against the source file, but this check is advisory only, not a confirmation that the install matches (see "Verification is advisory" below for why). If an optional post-install hook is configured, it runs at this point (see below).

If step 3 or 4 rejects the install, the live crontab is left completely untouched and, whenever there was a crontab before the run, the backup taken in step 2 is available for reference. If the crontab command itself fails during install (step 5), the script exits with a distinct error code; when a backup was taken it prints the exact command to restore from it, and on a true first install (no prior crontab, so no backup) it says so plainly, since there is nothing to restore to.

Verification is advisory, not a guarantee

Step 5 reads the crontab back after installing it and compares its hash against the source file, but a mismatch is only ever logged as a warning, never treated as a failure, and a match is never treated as proof either. There are two reasons for this:

  • Some cron implementations rewrite whitespace, blank lines or comments when they store a crontab. The install can be entirely correct and the hash can still differ from the source file, so treating a mismatch as an error would produce false alarms on those systems.
  • The readback itself can fail (for example if crontab -l errors out even though the preceding crontab <file> install succeeded). When that happens there is nothing to hash at all, so safe-crontab says so plainly rather than pretending it confirmed anything.

In short: within step 5, the install has already happened by the time the read-back check runs. Verification is a best-effort sanity check on top of the install, not a gate the install has to pass. If you need certainty, run crontab -l yourself after safe-crontab finishes.

Install

Copy the script somewhere on your PATH and make it executable:

cp safe-crontab /usr/local/bin/safe-crontab
chmod +x /usr/local/bin/safe-crontab

No other files are required. The script is a single self-contained bash file; see "Requirements" below for what it expects to find on the system.

Usage

# Always start from the current crontab if you are adding to it, not
# replacing it outright:
crontab -l > my-crontab
$EDITOR my-crontab

safe-crontab my-crontab
safe-crontab <new-crontab-file> [--force-shrink]

--force-shrink bypasses the shrink guard for a deliberate large removal. Everything else about the install (backup, syntax check, verify) still happens.

-h / --help prints usage information and exits.

Configuration

All configuration is via environment variables. Every one of them has a sane default, so safe-crontab <file> works out of the box with no setup.

Variable Default Meaning
SAFE_CRONTAB_DIR ~/.safe-crontab Base directory holding backups/ and the default log file location.
SAFE_CRONTAB_KEEP 50 Number of backups to retain before older ones are pruned.
SAFE_CRONTAB_SHRINK_PCT 80 Shrink guard threshold, as a percentage of the current line count.
SAFE_CRONTAB_LOCK /tmp/safe-crontab.lock Lock file path used to serialise installs.
SAFE_CRONTAB_LOG_FILE $SAFE_CRONTAB_DIR/safe-crontab.log Log file path. May point anywhere; its parent directory is created if missing.
SAFE_CRONTAB_POST_INSTALL_HOOK (unset) Optional command run via bash -c after a successful install; may contain arguments.

The post-install hook

If SAFE_CRONTAB_POST_INSTALL_HOOK is set, its value is executed once via bash -c "$SAFE_CRONTAB_POST_INSTALL_HOOK" immediately after a successful install and verification. Because it goes through bash -c, the value may be a bare executable path or a full command line with arguments, for example:

SAFE_CRONTAB_POST_INSTALL_HOOK="bash /path/to/watchdog.sh rebaseline"

This is the integration point for anything that watches crontab integrity independently of this tool, for example a separate watchdog that hashes the crontab periodically and alerts on unexpected changes: point it at a small script that re-baselines that watchdog's stored hash, and the watchdog will not false-alarm on a change made correctly through safe-crontab.

The hook's exit status is logged but does not affect the exit status of safe-crontab itself; a failing hook (including a hook command that does not exist, which bash -c reports as exit 127) produces a warning, never a rollback and never a failed install (the crontab install has already succeeded and been verified by the time the hook runs).

Migration note (hook contract change): earlier versions required the hook value to be the path of an executable file, which was invoked directly with no arguments; a value that was not an executable file was skipped with a warning and the hook never ran. The value is now a command string run via bash -c, so it may carry arguments. A plain executable script path still works exactly as before, but the not-executable case changed: instead of being skipped, the value is now attempted and a non-zero result (for a missing command, bash -c reports exit 127) is logged as a warning. In both the old and new behaviour the install itself is unaffected either way.

Exit codes

Code Meaning
0 Success.
1 Refused: bad usage, missing file, shrink guard tripped, or a syntax check failure. Nothing was installed.
2 Internal error after the backup step (the crontab install command itself failed). The live crontab state is uncertain. When there was a crontab before the run, restore from the backup path printed in the error message; on a true first install no backup exists and there is nothing to restore.

Requirements

This is a Linux-first tool, built and tested against a GNU userland. It requires:

  • bash
  • flock (from util-linux)
  • sha256sum (from GNU coreutils)
  • GNU xargs, specifically the -r / --no-run-if-empty flag used when pruning old backups
  • A standard crontab command (-l, plain install from a file). crontab -n is used when available but is not required.

These are not exotic packages, they are standard on essentially any Linux distribution, but they are not a universal baseline: macOS and the BSDs do not ship flock by default, and their xargs and sha256sum equivalents differ (macOS has shasum -a 256 instead of sha256sum, and no built-in -r equivalent for xargs is needed there because BSD xargs does not run the command on empty input by default). Running safe-crontab on macOS or a BSD would need those adaptations first; it has not been tested there.

Why not just be careful

"Be careful" is not a control, it is a hope, and it does not scale to automation. A human operator can be careful once and distracted the next time; an AI agent or a script has no notion of caution at all beyond whatever guard rails are actually enforced in the code path it calls. If an agent is going to run crontab on your behalf, unattended, it will eventually do so with a file that was not seeded from the current schedule, under time pressure, in a batch of other changes, or as a result of a subtly wrong prompt. The failure mode described above did not happen because anyone was careless in the ordinary sense: it happened because the tool being used had no way to notice that a full replacement was about to destroy nearly everything.

safe-crontab does not require the caller, human or automated, to remember to be careful. It backs up before touching anything, and it refuses the specific shape of mistake that causes catastrophic, silent data loss, regardless of how the mistake came about. That is the right place to put the safety: in the tool that has the information to check, not in the judgement of whoever happens to be calling it that day.

License

MIT. See LICENSE.

About

A guarded crontab installer: lock, backup, shrink guard, syntax check, verified install.

Resources

License

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages