Headless Notes

Practical guides to running AI automations that keep working while you sleep.

Claude Code Cron Job Setup: Run Automations Overnight

Claude Code Cron Job Setup: Run Automations Overnight

2026-10-15 · 12 min read

#claude code #automation #headless

To set up a Claude Code cron job, you need two pieces. The first is a wrapper script that sets PATH, loads a token, cds into your project and runs claude -p with explicit permissions and limits. The second is one crontab line that runs that script under a lock and sends its output to a log file. Most "silent" overnight failures come from what cron does not give you: your shell's environment, your login, your project directory and a place to see errors. This guide covers each of those gaps, plus the scheduling options Anthropic offers if you don't want to manage cron yourself.

Should you use cron, or a built-in Claude Code scheduler?

Use cron when the job needs your machine's local files and tools and you want full control. Use Anthropic's built-in options when you'd rather not maintain a server. Claude Code now has three of its own scheduling options, and the official comparison shows how they differ:

OptionRuns onMachine must be onLocal filesMinimum intervalMain limitation
System cron + claude -pYour machine/serverYesYes1 minute (cron)You handle auth, env, logs, locking
Routines (cloud)Anthropic cloudNoNo (fresh clone)1 hourResearch preview; GitHub repos only
Desktop scheduled tasksYour machineYes, app open and awakeYes1 minuteSkipped while the computer sleeps
/loopYour open sessionYesYes1 minuteSession-scoped, expires after 7 days

How to choose:

The rest of this guide is about the cron route.

Trap 1: authentication that works in your terminal but not in cron

Your cron job needs a credential passed in explicitly through an environment variable, because the login in your interactive terminal may not reach it. Claude Code's authentication docs list two common options for scripts:

Two details often cause trouble:

  1. Precedence. ANTHROPIC_API_KEY ranks above CLAUDE_CODE_OAUTH_TOKEN (source). If both exist, you are billed through the API key, which may not be what you intended.
  2. --bare ignores the OAuth token. Bare mode is recommended for scripts, but it "does not read CLAUDE_CODE_OAUTH_TOKEN". With --bare, use ANTHROPIC_API_KEY or an apiKeyHelper instead (source).

Why this fails silently: when a failure happens inside the run, "such as missing authentication, Claude Code prints the failure as the result on stdout" (source). If you throw stdout away, the error disappears with it. Check the exit code too: Claude Code exits with a non-zero code when a run fails.

Trap 2: PATH and the minimal cron environment

Cron runs your command in a much smaller environment than your terminal, so set every variable the job needs inside the script itself. The crontab(5) man page says cron sets SHELL to /bin/sh and takes LOGNAME and HOME from /etc/passwd. Anything your ~/.zshrc or ~/.bashrc adds is not guaranteed to be there.

That matters for Claude Code because the native installer puts the launcher at ~/.local/bin/claude (source), and that directory is often added to PATH only by your shell profile. The tools Claude calls during the run also need to be found: node, git, gh, npm and so on.

Practical fixes:

Trap 3: the working directory decides what Claude loads

Always cd into the project explicitly, because the folder a -p run starts in decides which configuration and instructions it loads. Without --bare, claude -p "loads the same context an interactive session would, including anything configured in the working directory or ~/.claude". That includes CLAUDE.md, the hooks in .claude/settings.json and the MCP servers in .mcp.json (source).

Two consequences:

If you want the same result on every run no matter what's in ~/.claude, use --bare and pass context explicitly with --append-system-prompt-file, --settings and --mcp-config (source). The trade-off is that you then need an API key (see Trap 1).

Trap 4: permissions in a run nobody is watching

Set the permission mode and the allowed tools explicitly, because no one is awake to answer a prompt. The docs warn that a run with no permission mode set takes a built-in starting mode, "which can be auto, so pass the one you want" (source).

Options from the headless docs and the CLI reference:

Avoid --dangerously-skip-permissions for overnight jobs unless the machine is a disposable sandbox.

Putting it together: script, crontab, lock and log rotation

The setup has four parts: a wrapper script, a crontab line with flock, structured logs, and rotation so the logs don't fill your disk. Everything below is an example. Change the paths, prompt and tool list to fit your project.

Step 1: the wrapper script

#!/usr/bin/env bash
# ~/bin/claude-nightly.sh  (example)
set -uo pipefail

export PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin"
set -a; source "$HOME/.config/claude-cron/env"; set +a   # holds CLAUDE_CODE_OAUTH_TOKEN=...
unset ANTHROPIC_API_KEY   # avoid accidental precedence over the token

cd "$HOME/projects/myapp" || { echo "cd failed"; exit 1; }

LOG_DIR="$HOME/logs/claude-cron"
mkdir -p "$LOG_DIR"
STAMP=$(date +%Y-%m-%dT%H%M)
OUT="$LOG_DIR/run-$STAMP.json"

claude -p "$(cat .claude/nightly-prompt.md)" \
  --permission-mode dontAsk \
  --permission-prompts none \
  --allowedTools "Read,Edit,Bash(npm test *),Bash(git status *),Bash(git diff *)" \
  --max-turns 30 \
  --max-budget-usd 3 \
  --output-format json > "$OUT" 2> "$LOG_DIR/run-$STAMP.err"
STATUS=$?

echo "$(date -Is) exit=$STATUS cost=$(jq -r '.total_cost_usd // "n/a"' "$OUT" 2>/dev/null)"
[ "$STATUS" -eq 0 ] || echo "FAILED: $(jq -r '.result // empty' "$OUT" 2>/dev/null | head -c 500)"
exit "$STATUS"

Why these choices: --output-format json gives you result, session_id and total_cost_usd in a form a script can parse (source). Branching on the exit code catches failed runs. Keeping the prompt in a file means you can version-control it.

Step 2: the crontab line

Run crontab -e:

SHELL=/bin/bash
MAILTO=""
7 2 * * * /usr/bin/flock -n /tmp/claude-nightly.lock $HOME/bin/claude-nightly.sh >> $HOME/logs/claude-cron/cron.log 2>&1

flock -n makes the job "fail rather than wait if the lock cannot be immediately acquired" (flock(1)). If last night's run is still going, a second copy won't start on top of it. Cron mails command output to the crontab owner or to MAILTO (cron(8)). Since this setup sends output to a file, set MAILTO="" so cron doesn't also try to send mail.

Step 3: log rotation

Rotate the summary log with logrotate, and prune the per-run files with find. An example /etc/logrotate.d/claude-cron:

/home/you/logs/claude-cron/cron.log {
    weekly
    rotate 8
    compress
    missingok
    notifempty
    copytruncate
}

copytruncate truncates the file in place after copying it, so the >> in your crontab keeps writing to the same file. missingok and notifempty skip missing or empty logs (logrotate(8)). For the per-run JSON files, add a second cron line such as 0 4 0 find $HOME/logs/claude-cron -name 'run-*' -mtime +30 -delete.

Claude Code also saves session transcripts on disk. If you never resume these runs, --no-session-persistence stops them from being saved (source).

Step 4: test it under cron conditions

Before you trust the schedule, run the script in an environment as bare as cron's: env -i HOME="$HOME" /bin/sh -c '$HOME/bin/claude-nightly.sh'. Then schedule it a few minutes ahead and read cron.log and the JSON file. Also run claude auth status inside the script once: it reports which authMethod is active and exits 1 if you're not logged in (source).

Keeping overnight jobs healthy over time

Plan for updates, token expiry and alerting from the start, because these cause failures weeks after setup rather than on day one.

FAQ

Can Claude Code run on a schedule without my computer on?

Yes, through Routines, which run on Anthropic-managed cloud infrastructure on hourly, nightly or weekly schedules (source). They work from a fresh clone of your GitHub repositories, so they can't see local files. The minimum interval is one hour, and the feature is in research preview.

Why does my Claude Code cron job work manually but not from cron?

Usually cron's environment is missing something your terminal has: ~/.local/bin on PATH, an exported token, or the project directory. Use absolute paths, load credentials from a file, and cd explicitly. Capture stdout, because Claude Code prints failures such as missing authentication there (source).

Should I use CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY for cron?

Use CLAUDE_CODE_OAUTH_TOKEN (from claude setup-token) if you want runs to count against your Claude subscription. Use ANTHROPIC_API_KEY if you want API billing, or if you run with --bare, which ignores the OAuth token (source). Don't set both, because the API key takes precedence.

How do I stop a scheduled Claude run from costing too much?

Combine --max-turns with --max-budget-usd, and log total_cost_usd from the JSON output. The budget cap uses a client-side estimate and spend can go past it, so set it below your real limit (source).

How do I prevent overlapping Claude cron runs?

Wrap the command in flock -n with a lock file. A new run then exits right away if the previous one still holds the lock (flock(1)).

Sources