Claude Code Headless: An Always-On Local Schedule
Run Claude Code headless with -p on an OS-cron schedule so a job survives restarts.
Run this workflow
CI-verified, 4/4 fixtures passing.
Intended Use
Running Claude Code unattended on your own machine on an OS-cron schedule, so a job survives restarts as long as the machine is awake.
Not for
- Machines that may be asleep when cron fires, use the cloud schedule instead
- Interactive runs, this is headless -p
- Anything you have not locked down with permission rules first
The Stack
Tested Against
claude-code@2.1.72bash@5.xnode@20.xSide effects & data flow
- Network
- none, local only
- Writes
- ./overnight-summary.sh, ./checkheadless.mjs
- Credentials
- none required
Steps
- 1
Write the headless script and the cron line
claude -p runs one prompt non-interactively. --permission-mode dontAsk auto-denies anything not pre-approved instead of hanging on a prompt no one will answer. Schedule it with OS cron.
cat > overnight-summary.sh <<'EOF' #!/usr/bin/env bash cd ~/code/myrepo claude -p "Summarize the commits pushed since yesterday and flag anything risky." --permission-mode dontAsk EOF echo "wrote overnight-summary.sh; cron line: 0 7 * * 1-5 ~/bin/overnight-summary.sh" - 2
What CI checks: the cron is well-formed and the call is non-interactive
CI confirms the cron line is a valid 5-field expression and the script carries a `claude -p` call in a non-interactive permission mode (dontAsk, a real mode per code.claude.com/docs). No model runs.
cat > checkheadless.mjs <<'EOF' import { readFileSync } from 'node:fs'; const sh = readFileSync('overnight-summary.sh','utf8'); const fields = (process.argv[2] || '').trim().split(' ').filter(Boolean); let ok = true; function check(label, cond){ console.log(label + ': ' + (cond ? 'yes' : 'NO')); if(!cond) ok=false; } check('script has claude -p', sh.includes('claude -p')); check('non-interactive (dontAsk)', sh.includes('--permission-mode dontAsk')); check('cron is 5-field', fields.length === 5); if(!ok){ console.log('headless schedule check FAILED'); process.exit(1); } console.log('headless schedule check OK'); EOF node checkheadless.mjs "0 7 * * 1-5" - 3
Let cron run it (the model step, not checked by CI)
When cron fires the script, claude -p invokes the model to write the summary. That output is non-deterministic and fenced: CI verifies the schedule and the command shape, never the summary.
Eval, 4 fixtures
Last passed: verified todaynon-interactivecontainstimeout 30s · max $0Expected:
non-interactive (dontAsk): yescron-5fieldcontainstimeout 30s · max $0Expected:
cron is 5-field: yescheck-okcontainstimeout 30s · max $0Expected:
headless schedule check OKclean-exitexit_codetimeout 30s · max $0Expected:
0
Results
Survives closing the terminal; the only catch is the machine has to be awake when cron fires.
Did this work for you?
Our CI checks the setup runs. You tell us if the whole thing worked. Tell us straight.
Liked this workflow?
Get new verified workflows in WebAfterAI, three issues a week (Tue, Thu, Sat).