Crontab Command Guide: Edit, List, and Manage Cron Jobs Safely
Contents
The crontab command installs, lists, edits, and removes one user's table of cron jobs: crontab -e opens yours in an editor, crontab -l prints it, crontab -r deletes it without asking, and crontab file replaces all of it with the file's contents. It manages only per-user crontabs. System-wide schedules live in /etc/crontab, /etc/cron.d/, and directories like /etc/cron.daily, which root edits without the crontab command. For what cron is and how a job runs, see what is a cron job.
Behavior differs by implementation. This guide was checked against cronie 1.7.2 (Fedora, Arch; RHEL 10 ships 1.7.0, RHEL 9 ships 1.5.7), Debian's cron 3.0pl1-197 (Debian 13; Ubuntu packages the same cron), and BusyBox 1.37.0 (Alpine 3.21 through 3.24), with package versions checked October 24, 2026.
The crontab Options at a Glance#
| Command | What it does | Where |
|---|---|---|
crontab -e |
Edit your crontab; it's installed when the editor exits | All |
crontab -l |
Print your crontab | All |
crontab -r |
Delete your crontab immediately | All |
crontab -i -r |
Ask for confirmation, then delete | cronie, Debian |
crontab file, crontab - |
Replace your crontab with a file or standard input | All |
crontab -u alice -l |
Act on alice's crontab instead of yours | Root |
crontab -T file |
Check syntax without installing | cronie 1.5.6+ |
crontab -n file |
Check syntax without installing | Debian cron 3.0pl1-160+ (Debian 12+, Ubuntu 24.04+) |
In cronie, -n means something else: it sets which host in a cluster runs the shared crontabs, needs root, and checks nothing (cronie crontab(1)). Both checkers validate the user-crontab format, so neither can tell you that an /etc/cron.d line is missing its user field.
Editing With crontab -e#
Which Editor Opens#
All three use $VISUAL, then $EDITOR. With neither set, cronie and BusyBox open vi. Debian's crontab hands off to sensible-editor, which checks two more variables and, the first time you run it from a terminal, starts select-editor, which asks you to choose and saves the answer in ~/.selected_editor (source). Ubuntu 24.04 behaves the same way. Run select-editor to change that choice, or pick an editor for one run:
VISUAL=nano crontab -eSyntax Is Checked When You Save#
cronie and Debian check the file when the editor exits. If a line is invalid, they print the error, keep the old crontab, and ask Do you want to retry the same edit?. Answer y to reopen your changes. Answer n and crontab tells you where it left them, in a /tmp/crontab.XXXXXX file. BusyBox installs whatever you saved without checking it. Crontab error messages explains each message this check can print, from bad minute to premature EOF, and how to fix it.
The check catches malformed lines, not wrong ones. A bad path, a job in the wrong user's crontab, or an unescaped % that cron will turn into a newline (cron expression syntax explains that one) all install cleanly.
Where Crontabs Are Stored#
| Implementation | Distributions | User crontab |
|---|---|---|
| cronie | Fedora, RHEL, Arch | /var/spool/cron/<user> |
| Debian cron | Debian, Ubuntu | /var/spool/cron/crontabs/<user> |
| BusyBox crond | Alpine | /etc/crontabs/<user>, also reachable as /var/spool/cron/crontabs/ |
Read these files as root if you need to, but don't write them. That skips the syntax check, and Debian's cron rereads user crontabs only when the spool directory's modification time changes, which crontab updates and an in-place edit doesn't (cron(8)). It also ignores any spool file whose mode isn't exactly 0600 (database.c). BusyBox crond rescans only when its directory changes or once an hour; crontab notifies it at once (crond.c).
Two Commands That Wipe a Crontab#
crontab -r Deletes Without Asking#
On a QWERTY keyboard, r sits next to e, and crontab -r removes the whole crontab with no prompt. cronie and Debian accept -i, which makes -r ask first and does nothing with -e or -l, so an alias in ~/.bashrc is safe:
alias crontab='crontab -i'BusyBox's crontab has no -i, so on Alpine the alias makes every crontab command fail with a usage message.
crontab file Replaces Everything#
crontab file and crontab - don't merge. Whatever you pass becomes the entire crontab, so this common one-liner deletes every other job:
echo '30 2 * * * /opt/app/backup.sh' | crontab - # replaces the whole crontabTo add a job from a script, read the current crontab, append, and install the result:
#!/bin/sh
# add-cron-job.sh: append one entry to your crontab and keep every existing entry.
# Usage: ./add-cron-job.sh '30 2 * * * /opt/app/backup.sh'
set -eu
entry=$1
current=$(mktemp)
trap 'rm -f "$current"' EXIT
if ! err=$(crontab -l 2>&1 > "$current"); then
case $err in
"no crontab for "* | *"No such file or directory") ;; # no crontab yet
*) printf '%s\n' "$err" >&2; exit 1 ;; # anything else: change nothing
esac
fi
if grep -qxF -- "$entry" "$current"; then
echo "already installed"
exit 0
fi
# Make sure the last existing line ends with a newline before appending.
if [ -s "$current" ] && [ -n "$(tail -c 1 "$current")" ]; then echo >> "$current"; fi
{ cat "$current"; printf '%s\n' "$entry"; } | crontab -If the current crontab can't be read, the script stops, so a permission error can't become an empty crontab. Running it twice doesn't add a duplicate.
Back Up Before You Change Anything#
Save your own crontab:
crontab -l > ~/crontab-$(date +%F).txtSave every user's crontab and the system files, as root. The archive can contain credentials set in crontabs, so keep it private:
umask 077
set --
for p in /var/spool/cron /etc/crontabs /etc/crontab /etc/cron.d /etc/anacrontab \
/etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly /etc/periodic; do
[ -e "$p" ] && set -- "$@" "$p"
done
tar -czf "/root/cron-backup-$(date +%F).tar.gz" "$@"cronie 1.7.0 and later also keep a copy of the previous crontab whenever crontab -e, crontab file, or crontab -r changes it, in ~/.cache/crontab/crontab.bak (or under $XDG_CACHE_HOME), named crontab.<user>.bak when you change someone else's with -u (crontab(1), NEWS). That covers Fedora, RHEL 10, and Arch, but not RHEL 9. It holds one version, so copy it elsewhere before your next change. To keep every version, crontab version control covers deploying crontabs from a git repository and catching drift. Look for the line Backup of alice's previous crontab saved to ...: upstream 1.7.0 through 1.7.2 skip the backup when ~/.cache doesn't exist yet, and crontab -r deletes anyway (fix). Fedora 43 and later and RHEL 10 ship the fix; Arch's 1.7.2 package doesn't. Debian's cron and BusyBox keep no copy.
Restore a Crontab#
crontab ~/crontab-2026-10-24.txt # your own
sudo crontab -u alice /root/restore/alice.txt # another user'sRestore through crontab, not by copying a file into the spool directory: it checks the syntax, sets the owner and mode cron requires, and tells the daemon. From the archive, extract the user's file and install it the same way. Debian's spool files begin with three header comments, starting # DO NOT EDIT THIS FILE; delete them first or they stay in the crontab as comments.
With no backup at all, cron's log still records every command it started, as (alice) CMD (/opt/app/backup.sh), with a timestamp:
journalctl -u cron --since "30 days ago" | grep ' CMD ' # use -u crond on cronieFrom the run times you can work out each schedule. Jobs that didn't run in that window, and variable lines, aren't in the log.
A wiped crontab raises no error; its jobs just stop. Catching that takes a check from outside the host, where each job reports in and a monitor alerts when an expected report doesn't arrive. CronRadar works this way. It has no SDK or auto-discovery for plain cron, so each job sends its own HTTP check-in; how to know if a cron job ran compares that with cron's log and exit codes.
System Crontabs: /etc/crontab and /etc/cron.d#
crontab -e |
/etc/crontab, /etc/cron.d/* |
|
|---|---|---|
| Line format | Five time fields, command | Five time fields, user, command |
| Runs as | The crontab's owner | The user named on each line |
| How you change it | crontab, checked when you save |
Any editor, as root; nothing checks it |
| Who can change it | That user, if allowed, and root | Root |
| Suited to | A person's or service account's job | Jobs owned by a package or a deploy process |
cron picks up changes to these files without a restart. Debian's cron checks each /etc/cron.d file's modification time every minute (database.c), and cronie watches with inotify or checks modification times each minute (cronie cron(8)). On Debian, /etc/cron.d files don't inherit variables set in /etc/crontab; cron environment variables covers what each job sees.
For deploy automation, give each application its own /etc/cron.d file and replace the whole file rather than editing a user's crontab. It can't disturb anyone else's jobs, and the file can live in version control:
tmp=$(mktemp)
cat > "$tmp" <<'EOF'
# Installed by the deploy script; local edits are overwritten.
30 2 * * * appuser /opt/app/backup.sh
EOF
sudo install -o root -g root -m 0644 "$tmp" /etc/cron.d/nightly-backup
rm -f "$tmp"That name, owner, and mode pass both daemons' checks:
| Rule | Debian cron 3.0pl1 | cronie 1.7 |
|---|---|---|
| File name | Letters, digits, _, and - only; a name with a dot is skipped without a log line |
Skipped if it starts with . or # or ends in ~, .rpmsave, .rpmorig, or .rpmnew |
| Owner and mode | Root-owned; not group- or other-writable | Root-owned; not group- or other-writable; not executable |
| One invalid line | The whole file is ignored | That line is skipped |
Sources: Debian cron(8), database.c, and user.c; cronie database.c and user.c. BusyBox crond reads neither /etc/crontab nor /etc/cron.d. On Alpine, system jobs go in root's crontab, /etc/crontabs/root.
The Periodic Directories and run-parts#
/etc/cron.hourly, /etc/cron.daily, /etc/cron.weekly, and /etc/cron.monthly hold executable scripts, not crontab lines. A crontab entry or anacron runs run-parts on each directory, which runs its scripts in name order. What triggers them, and when, depends on the distribution:
| System | Hourly | Daily, weekly, monthly |
|---|---|---|
| Debian 13, Ubuntu 24.04, no anacron | /etc/crontab, :17 past each hour |
/etc/crontab: 06:25 daily, 06:47 Sundays, 06:52 on the 1st |
| Debian 13, Ubuntu 24.04, anacron installed | Same | anacron, started by anacron.timer at :30 past each hour from 07:30 to 23:30 |
| Fedora, RHEL, Arch | /etc/cron.d/0hourly, :01 past each hour |
anacron, started by /etc/cron.hourly/0anacron, between 03:00 and 22:00 after a random delay |
| Alpine | /etc/crontabs/root, on the hour |
/etc/periodic/ scripts at 02:00 daily, 03:00 Saturdays, 05:00 on the 1st, plus a 15min folder |
On Debian and Ubuntu, the daily, weekly, and monthly lines in /etc/crontab begin with test -x /usr/sbin/anacron ||, so once anacron is installed they do nothing (crontab.main). On a systemd host, anacron's timer fires at 07:30 plus up to five random minutes, and anacron starts the daily scripts five minutes later (anacron.timer, anacrontab), so on a server that stays up they run around 07:35 to 07:40, not 06:25. On cronie systems, anacron adds up to 45 random minutes to each job's fixed delay (anacrontab); with the cronie-noanacron package instead, dailyjobs runs them at 04:02 daily, 04:22 on Sundays, and 04:42 on the 1st.
File names decide which scripts run, and run-parts skips the others silently. Debian's run-parts, which Arch also packages, runs only executable files named with letters, digits, underscores, and hyphens, so backup.sh in /etc/cron.daily never runs (run-parts(8)). Fedora's run-parts allows dots and skips names ending in ~, ,, .rpmsave, .rpmorig, .rpmnew, .swp, or .cfsaved. BusyBox's allows dots anywhere except the first character. All three can show what they would run without running it:
run-parts --test /etc/cron.dailyWho May Use crontab: cron.allow and cron.deny#
cronie and Debian apply the same rules:
- If
/etc/cron.allowexists, only users listed in it, one per line, can runcrontab.cron.denyis then ignored. - Otherwise, if
/etc/cron.denyexists, every user not listed in it can. - If neither exists, cronie allows only root and Debian allows everyone.
Root is always allowed. Fedora, RHEL, and Arch install a cron.deny that names no one, so every user can run crontab there (Fedora spec, Arch cron-deny). On Debian, the files must be world-readable or readable by the crontab group, or every user is denied (crontab(1)).
These files control only the crontab command. A user added to cron.deny keeps the crontab they already have, and cron keeps running it (cronie crontab(1)). Back it up and remove it with sudo crontab -u alice -r if those jobs should stop. BusyBox's crontab doesn't read either file.
List Every Cron Job on a Machine#
crontab -l shows one user. A full inventory reads every user's crontab, the system crontabs, the periodic directories, anacron, and systemd timers, since on systemd hosts some scheduled work, Debian's anacron among it, starts from a timer. This read-only script covers cronie, Debian, and BusyBox systems; run it as root:
#!/bin/sh
# list-cron-jobs.sh: print every cron schedule on this host. Run as root; changes nothing.
show() {
[ -f "$1" ] || return 0
printf '\n== %s\n' "$1"
grep -v -E '^[[:space:]]*(#|$)' "$1"
}
# User crontabs: Debian, Ubuntu, and Alpine use /var/spool/cron/crontabs (on Alpine,
# a link to /etc/crontabs); cronie uses /var/spool/cron. List each real directory once.
seen=
for dir in /var/spool/cron/crontabs /var/spool/cron /etc/crontabs; do
[ -d "$dir" ] || continue
real=$(cd "$dir" && pwd -P)
case " $seen " in *" $real "*) continue ;; esac
seen="$seen $real"
for f in "$dir"/*; do
case ${f##*/} in \#* | cron.update) continue ;; esac # temporary files
show "$f"
done
done
# System crontabs and anacron.
show /etc/crontab
for f in /etc/cron.d/*; do show "$f"; done
show /etc/anacrontab
# Periodic scripts that run-parts would actually run.
for d in /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly \
/etc/cron.yearly /etc/periodic/*; do
[ -d "$d" ] || continue
printf '\n== %s (run-parts --test)\n' "$d"
run-parts --test "$d"
done
# systemd timers, including inactive ones.
if command -v systemctl > /dev/null 2>&1; then
printf '\n== systemd timers\n'
systemctl list-timers --all --no-pager
fiReading the output:
- A user crontab named after an account that no longer exists is an orphan. cron keeps the file but doesn't run it.
- On Debian and Ubuntu, an
/etc/cron.dfile with a dot in its name appears here but never runs. - A periodic script missing from the
run-parts --testoutput won't run. systemctl list-timerscovers system timers; a user's own timers show up only insystemctl --user list-timers, run as that user.
If an entry is listed and its job still doesn't run, work through why a cron job isn't running. To list scheduled jobs across many hosts and schedulers, see scheduled job inventory.
Comments, Blank Lines, and the Final Newline#
| cronie 1.7.2 | Debian cron 3.0pl1 | BusyBox 1.37.0 | |
|---|---|---|---|
Blank lines and # lines |
Ignored | Ignored | Ignored |
# after a job or variable |
Part of the command or value | Part of the command or value | Starts a comment anywhere, even inside a command |
| No newline after last line | crontab refuses the file; crond skips that line and logs it |
crontab refuses the file; cron ignores the whole file and logs it |
Read normally |
Sources: cronie crontab(5) and crontab.c; Debian crontab(5) and user.c; BusyBox parse_config.c, which crond calls with end-of-line comments enabled.
In practice:
- Put comments on their own line. In cronie and Debian,
[email protected] # team inboxsetsMAILTOto the whole string, comment included. - On Alpine, a
#inside a command, such as in a URL fragment or$#, cuts the command short. Move such commands into a script. - End every file with a newline; a file written by a template or by
printfwithout a trailing\ncan lack one. Debian's whole-file rule matters most for/etc/crontaband/etc/cron.d, because nothing checks those files before cron reads them. This prints nothing when the newline is missing:
[ -z "$(tail -c 1 /etc/cron.d/nightly-backup)" ] && echo "ends with a newline"Frequently Asked Questions#
How do I list all cron jobs on Linux?#
crontab -l lists your own jobs and sudo crontab -u alice -l another user's, but neither covers the machine. As root, also read every file in the spool directory, /etc/crontab, /etc/cron.d/, the /etc/cron.daily-style directories, and /etc/anacrontab, then run systemctl list-timers --all. The script above does all of it read-only.
What's the difference between crontab -e and /etc/crontab?#
crontab -e edits one user's crontab: its jobs run as that user, its lines have no user field, and it's checked when you save. /etc/crontab and the files in /etc/cron.d/ are system crontabs that root edits directly. Each line names the user to run as, and nothing validates the file before cron reads it.
How do I restore a crontab after crontab -r?#
Install your latest backup with crontab file, or sudo crontab -u alice file for another user. On cronie 1.7 or later, check ~/.cache/crontab/crontab.bak, which crontab -r writes before deleting when the backup succeeds; Debian, Ubuntu, and Alpine keep no copy. Without a backup, cron's log lists each command it ran and when, enough to rebuild entries that ran recently.
Where is the crontab file stored?#
In /var/spool/cron/crontabs/<user> on Debian and Ubuntu, /var/spool/cron/<user> on Fedora, RHEL, and Arch, and /etc/crontabs/<user> on Alpine. Change it with crontab, which checks the syntax, sets the permissions cron requires, and tells cron to reload.
Why is my /etc/cron.d file ignored?#
On Debian and Ubuntu, check the name first: a dot anywhere in it, as in backup.cron or app.conf, makes cron skip the file without logging anything. Then check that it's owned by root, isn't group- or other-writable, ends with a newline, and has no invalid lines, since Debian ignores the whole file over one. cronie also rejects an executable file, logging BAD FILE MODE. Alpine's BusyBox crond doesn't read /etc/cron.d at all.
Put this guide to work
Know when the next run is missing
Connect one scheduled job, confirm a real completion, then verify the alert destination. Keep your existing scheduler in charge of running the work.