# Crontab Command Guide: Edit, List, and Manage Cron Jobs Safely

By Dan Ochoa · Published 2026-10-25 · Updated 2026-10-25
Canonical: https://cronradar.com/blog/crontab-guide

> How the crontab command edits, lists, and removes cron jobs, how /etc/crontab and /etc/cron.d differ, and how to back up and list every job on a server.

**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](https://cronradar.com/blog/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+](https://sources.debian.org/src/cron/3.0pl1-197/debian/changelog/) (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)](https://man7.org/linux/man-pages/man1/crontab.1.html)). 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](https://sources.debian.org/src/busybox/1:1.37.0-6/miscutils/crontab.c/) open `vi`. Debian's crontab hands off to [sensible-editor](https://manpages.debian.org/trixie/sensible-utils/sensible-editor.1.en.html), which checks two more variables and, the first time you run it from a terminal, starts [select-editor](https://manpages.debian.org/trixie/sensible-utils/select-editor.1.en.html), which asks you to choose and saves the answer in `~/.selected_editor` ([source](https://sources.debian.org/src/sensible-utils/0.0.25/sensible-editor/)). Ubuntu 24.04 behaves the same way. Run `select-editor` to change that choice, or pick an editor for one run:

```bash
VISUAL=nano crontab -e
```

### Syntax 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](https://cronradar.com/blog/crontab-errors) 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](https://cronradar.com/blog/cron-expression) 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)](https://manpages.debian.org/trixie/cron/cron.8.en.html)). It also ignores any spool file whose mode isn't exactly `0600` ([database.c](https://sources.debian.org/src/cron/3.0pl1-197/database.c/)). BusyBox crond rescans only when its directory changes or once an hour; `crontab` notifies it at once ([crond.c](https://sources.debian.org/src/busybox/1:1.37.0-6/miscutils/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:

```bash
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:

```bash
echo '30 2 * * * /opt/app/backup.sh' | crontab -    # replaces the whole crontab
```

To add a job from a script, read the current crontab, append, and install the result:

```bash
#!/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:

```bash
crontab -l > ~/crontab-$(date +%F).txt
```

Save every user's crontab and the system files, as root. The archive can contain credentials set in crontabs, so keep it private:

```bash
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)](https://man7.org/linux/man-pages/man1/crontab.1.html), [NEWS](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/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](https://cronradar.com/blog/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](https://github.com/cronie-crond/cronie/pull/206)). 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

```bash
crontab ~/crontab-2026-10-24.txt                 # your own
sudo crontab -u alice /root/restore/alice.txt    # another user's
```

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

```bash
journalctl -u cron --since "30 days ago" | grep ' CMD '    # use -u crond on cronie
```

From 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](https://cronradar.com/blog/how-to-know-if-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](https://sources.debian.org/src/cron/3.0pl1-197/database.c/)), and cronie watches with inotify or checks modification times each minute ([cronie cron(8)](https://man7.org/linux/man-pages/man8/cron.8.html)). On Debian, `/etc/cron.d` files don't inherit variables set in `/etc/crontab`; [cron environment variables](https://cronradar.com/blog/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:

```bash
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)](https://manpages.debian.org/trixie/cron/cron.8.en.html), [database.c](https://sources.debian.org/src/cron/3.0pl1-197/database.c/), and [user.c](https://sources.debian.org/src/cron/3.0pl1-197/user.c/); cronie [database.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/database.c) and [user.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/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](https://sources.debian.org/src/cron/3.0pl1-197/debian/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](https://sources.debian.org/src/anacron/2.3-43/debian/anacron.timer/), [anacrontab](https://sources.debian.org/src/anacron/2.3-43/debian/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](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/contrib/anacrontab)); with the `cronie-noanacron` package instead, [dailyjobs](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/contrib/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)](https://manpages.debian.org/trixie/debianutils/run-parts.8.en.html)). Fedora's [run-parts](https://github.com/cronie-crond/crontabs/blob/crontabs-20190603/run-parts) allows dots and skips names ending in `~`, `,`, `.rpmsave`, `.rpmorig`, `.rpmnew`, `.swp`, or `.cfsaved`. [BusyBox's](https://sources.debian.org/src/busybox/1:1.37.0-6/debianutils/run_parts.c/) allows dots anywhere except the first character. All three can show what they would run without running it:

```bash
run-parts --test /etc/cron.daily
```

## Who May Use crontab: cron.allow and cron.deny

cronie and Debian apply the same rules:

1. If `/etc/cron.allow` exists, only users listed in it, one per line, can run `crontab`. `cron.deny` is then ignored.
2. Otherwise, if `/etc/cron.deny` exists, every user not listed in it can.
3. 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](https://src.fedoraproject.org/rpms/cronie/blob/rawhide/f/cronie.spec), [Arch cron-deny](https://gitlab.archlinux.org/archlinux/packaging/packages/cronie/-/blob/main/cron-deny)). On Debian, the files must be world-readable or readable by the `crontab` group, or every user is denied ([crontab(1)](https://manpages.debian.org/trixie/cron/crontab.1.en.html)).

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)](https://man7.org/linux/man-pages/man1/crontab.1.html)). 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:

```bash
#!/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
fi
```

Reading 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.d` file with a dot in its name appears here but never runs.
- A periodic script missing from the `run-parts --test` output won't run.
- [`systemctl list-timers`](https://www.freedesktop.org/software/systemd/man/latest/systemctl.html) covers system timers; a user's own timers show up only in `systemctl --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](https://cronradar.com/blog/cron-job-not-running). To list scheduled jobs across many hosts and schedulers, see [scheduled job inventory](https://cronradar.com/blog/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)](https://man7.org/linux/man-pages/man5/crontab.5.html) and [crontab.c](https://github.com/cronie-crond/cronie/blob/cronie-1.7.2/src/crontab.c); Debian [crontab(5)](https://manpages.debian.org/trixie/cron/crontab.5.en.html) and [user.c](https://sources.debian.org/src/cron/3.0pl1-197/user.c/); BusyBox [parse_config.c](https://sources.debian.org/src/busybox/1:1.37.0-6/libbb/parse_config.c/), which crond calls with end-of-line comments enabled.

In practice:

- Put comments on their own line. In cronie and Debian, `MAILTO=ops@example.com # team inbox` sets `MAILTO` to 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 `printf` without a trailing `\n` can lack one. Debian's whole-file rule matters most for `/etc/crontab` and `/etc/cron.d`, because nothing checks those files before cron reads them. This prints nothing when the newline is missing:

```bash
[ -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.
