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.
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.
Running safe-crontab <file> performs five steps, in order:
- Lock. The whole operation is serialised with
flock, so two installs started at the same time cannot race each other. - 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.
- 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-shrinkwhen a large deliberate removal is genuinely intended. This step needs a reliable current line count to work from, so it is fail-closed: ifcrontab -lcannot 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. - Syntax check. Where the local
crontabimplementation 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. - Install and verify. The new file is installed with
crontab. safe-crontab then attempts to read the crontab back withcrontab -land 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.
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 -lerrors out even though the precedingcrontab <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.
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-crontabNo 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.
# 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-crontabsafe-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.
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. |
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.
| 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. |
This is a Linux-first tool, built and tested against a GNU userland. It requires:
bashflock(from util-linux)sha256sum(from GNU coreutils)- GNU
xargs, specifically the-r/--no-run-if-emptyflag used when pruning old backups - A standard
crontabcommand (-l, plain install from a file).crontab -nis 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.
"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.
MIT. See LICENSE.