Can Claude Code Run on Cron?
Yes. Claude Code ships with a non-interactive headless mode designed for scripts and CI. Invoke it withclaude -p "prompt here"and it executes the prompt, prints the final response to stdout, and exits with a normal status code. That is all a scheduler needs.
# Minimal headless invocation
claude -p "check git status and summarise uncommitted changes"
# Scoped to a specific working directory
cd ~/projects/kaiships.com && claude -p "draft one reddit reply about claude code hooks"
# With explicit permission mode for unattended use
claude -p --permission-mode plan "audit today's deploy log and flag regressions"What you get
Full tool access, MCP servers, skills, and subagents -- the same Claude Code you use interactively, just without the TUI and without a human keystroke in the loop.
What you lose
Mid-run permission prompts. If a tool is not pre-authorised in.claude/settings.jsonthe run fails instead of pausing for input.
Why Schedule Claude Code in the First Place
An interactive agent is reactive. A scheduled agent is proactive. The shift from "open Claude Code when I need something" to "Claude Code runs on its own cadence" is the difference between a tool and a teammate. These are the real jobs I run daily.
Daily SEO blog writer
One cron job kicks Claude Code at 09:00 with a prompt to pick an uncovered target keyword, research the SERP, write an article, commit, and push. The post you are reading was produced by that exact job.
Heartbeat / check-in loop
Every 30 minutes Claude Code reads recent logs, memory files, and Discord messages, decides whether anything requires action, and either takes it or writes a note. Background awareness without prompts.
Reddit / community scanner
Twice a day, scan r/ClaudeAI and r/LocalLLaMA for fresh questions where we have genuine expertise. Draft replies, queue the best one for review in Discord.
Job application pipeline
A job-scout cron pulls new postings on Lever, Ashby, and Greenhouse. A job-applier cron fills forms via Playwright. 10+ applications per day, zero interactive input.
Nightly memory compaction
At 02:00, Claude Code reads the day's memory files, merges duplicates, archives stale entries, and updates the MEMORY.md index. Context stays lean without manual curation.
The complete playbook
Get every cron recipe, every hook, and the exact heartbeat loop that ships blog posts while I sleep -- in the KaiShips Guide to Claude Code.
This post is the cron chapter. The full guide covers memory, skills, subagents, hooks, MCP, and the end-to-end automation layer I run at KaiShips today.
Get the KaiShips Guide to Claude Code -- $29The Headless Mode Primitives
Before wiring anything into a scheduler, learn the three flags you will reach for on every cron job. Everything else is a variation on these.
| Flag | What it does | When to use it |
|---|---|---|
| -p "<prompt>" | Runs Claude Code non-interactively with the given prompt and exits. | Every scheduled invocation. This is the core flag. |
| --permission-mode | Overrides the interactive permission mode. Values include default, plan, acceptEdits, bypassPermissions. | Use plan for analysis jobs, acceptEdits for scripted writes you already trust. |
| --output-format | Controls stdout format. json is machine-readable and safe to pipe into downstream tools. | When the next cron step needs to parse Claude's output programmatically. |
| --model | Pins the run to a specific model (opus, sonnet, haiku). | Cost routing. Use haiku for lightweight cron jobs, sonnet for writing, opus only when needed. |
| --resume | Resumes a previous session by ID. Useful for long-running multi-step workflows. | Only when you actually need continuity. Most cron jobs should be stateless. |
Prefer stateless jobs. A cron job that starts fresh every run is easier to debug, cheaper (no context carryover), and more resilient to yesterday's mistakes.
Setting Up Your First Claude Code Cron Job
The pattern is always the same: wrapper script, schedule entry, log file, permission allowlist. Build each piece once, reuse for every future job.
Write a wrapper script
Never invokeclaudedirectly from a crontab line. Wrap it in a bash script so the environment, PATH, and logging are explicit.
#!/bin/bash
# ~/crons/seo-blog-writer.sh
set -euo pipefail
LOG=~/crons/logs/seo-blog.log
echo "[$(date -u +%FT%TZ)] starting seo-blog-writer" >> "$LOG"
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
export ANTHROPIC_API_KEY="$(cat ~/.claude/keys/anthropic)"
cd ~/projects/kaiships.com
claude -p --permission-mode acceptEdits --model sonnet \
"Write one new SEO blog post targeting an uncovered claude code keyword. \
Use the /seo-2026 skill. Commit and push when done." \
>> "$LOG" 2>&1
echo "[$(date -u +%FT%TZ)] seo-blog-writer exit=$?" >> "$LOG"Schedule it (macOS, launchd)
Drop a plist in~/Library/LaunchAgentsand load it. launchd survives sleep/wake cycles correctly, unlike cron on macOS.
<!-- ~/Library/LaunchAgents/com.kaiships.seo-blog.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.kaiships.seo-blog</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/kai/crons/seo-blog-writer.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer></dict>
<key>StandardOutPath</key><string>/Users/kai/crons/logs/seo-blog.out</string>
<key>StandardErrorPath</key><string>/Users/kai/crons/logs/seo-blog.err</string>
</dict>
</plist>launchctl load ~/Library/LaunchAgents/com.kaiships.seo-blog.plist
launchctl list | grep com.kaishipsSchedule it (Linux, crontab)
On a Linux VPS, use the same wrapper and register a single crontab line. Never put the prompt inline -- keep it in the script.
# crontab -e
# Daily at 09:00 UTC, write one SEO blog post
0 9 * * * /home/kai/crons/seo-blog-writer.shLock down permissions for unattended runs
Interactive Claude Code can prompt you. Headless Claude Code cannot. Add a.claude/settings.jsonto the working directory with an explicit allowlist of the tools this job legitimately needs.
{
"permissions": {
"allow": [
"Bash(git *)",
"Bash(npm run build)",
"Edit",
"Write",
"Read",
"Grep",
"Glob",
"WebSearch",
"WebFetch"
],
"deny": [
"Bash(rm -rf *)",
"Bash(curl * | sh)"
]
}
}Writing Cron-Friendly Prompts
A prompt that works interactively can fail badly on cron. The interactive version asks clarifying questions. The cron version has to decide and act alone. Six patterns make the difference.
- ●State the decision authority. "Pick the single best option and proceed" beats "which option do you want?" every time.
- ●Pre-answer every ambiguity. File paths, author name, commit message style, output format -- specify all of it in the prompt so Claude never stops to ask.
- ●Pin the success criterion. "Task is done when npm run build passes, the commit is pushed, and a Discord message is sent" -- explicit, testable, terminal.
- ●Name the skill. "Use the /seo-2026 skill" forces Claude into a deterministic workflow instead of improvising every run.
- ●Cap the scope. "Write exactly one blog post" prevents a runaway session that burns $20 on 12 drafts you will never review.
- ●Include a fallback. "If no suitable keyword remains, log the reason and exit cleanly" is how you avoid a cron job that spins its wheels forever on a saturated backlog.
Bad prompt (interactive thinking)
write a blog post for the siteGood prompt (cron-ready)
Write ONE new SEO blog post on kaiships.com using the /seo-2026 skill.
Repo: /Users/kai/projects/kaiships.com
Target keywords (pick one not already in app/routes.ts):
- claude code cron jobs
- best claude code configuration
- claude code plan mode
After writing:
1. npm run build (must pass)
2. git add + commit + push
3. Post to Discord channel 1484907941193580655
If all target keywords are already covered, log the reason and exit 0.The complete playbook
Get every cron recipe, every hook, and the exact heartbeat loop that ships blog posts while I sleep -- in the KaiShips Guide to Claude Code.
This post is the cron chapter. The full guide covers memory, skills, subagents, hooks, MCP, and the end-to-end automation layer I run at KaiShips today.
Get the KaiShips Guide to Claude Code -- $29Authentication and Environment Variables
Most broken cron jobs die here. launchd and cron do not source your shell profile. If it works in your terminal but fails silently at 09:00, the environment is almost certainly the reason.
OAuth (claude login)
Works for any cron that runs as the same macOS user. Claude Code reads~/.clauderegardless of who invokes it. No extra setup.
API key (ANTHROPIC_API_KEY)
Preferred for servers, CI, and jobs that might run as a different user. Store the key in a file with mode 600, read it from the wrapper, and never commit it.
The env-var checklist
- ●Set
PATHexplicitly. Homebrew paths are not in launchd's default PATH. - ●Set
HOMEif you invoke anything that reads per-user config and the job runs under systemd or a different account. - ●Never put trailing newlines in env var values. They break shell parsing and, on Vercel, fail deploys silently.
- ●Log
envon the very first run to capture the baseline. When things break later you can diff against it.
Logging and Debugging
Silent cron jobs are worse than failed cron jobs. A job that errors at least tells you. A job that silently hangs for a week costs you a week. Instrument every run.
Timestamp every line
Echo start and end timestamps in the wrapper. Without them you cannot tell a hung job from a slow one.
Separate stdout and stderr
launchd supportsStandardOutPathandStandardErrorPathnatively. Use them. Grep errors only when something looks wrong.
Add a watchdog
Wrap the claude call intimeout 15mso a hung session terminates before the next schedule window. A scheduled job that overruns its own interval is a ticking pileup.
Ping Discord on non-zero exit
The cheapest monitoring you will ever build. A single curl to a Discord webhook at the end of the wrapper tells you immediately if something broke.
STATUS=$?
if [ "$STATUS" -ne 0 ]; then
curl -s -X POST "$DISCORD_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"content\":\"cron seo-blog failed (exit=$STATUS)\"}"
fiCost Control for Scheduled Claude Code
Cron is where API bills escalate quickly. A job that costs $0.05 interactively costs $36 per month at 24 runs per day. Four levers keep the total predictable.
| Lever | Typical impact | How |
|---|---|---|
| Model routing | 60 to 95% | haiku for file ops, sonnet for writing, opus only when reasoning is the bottleneck. |
| Interval tuning | 50 to 75% | Every minute you add between runs halves the monthly bill. 30 minutes is usually plenty. |
| Active windows | 33 to 50% | Skip overnight runs. No one needs a heartbeat at 03:00. |
| Prompt size | 20 to 60% | Keep CLAUDE.md lean, reference external files on demand, trim boilerplate in the prompt. |
According to Anthropic's pricing documentation, Haiku 4.5 costs $0.25 per million input tokens vs $3 for Sonnet 4.6 and $15 for Opus 4.6. A hourly cron job using Haiku for a 4KB prompt and light tool use runs under $2 per month. The same job on Opus runs around $120.
The 7 Cron Jobs I Actually Run at KaiShips
This is my live production schedule on an Apple Silicon Mac running 24/7. Every job routes through the same wrapper pattern described above. All of them use launchd.
| Job | Cadence | Model | What it does |
|---|---|---|---|
| heartbeat | every 30 min | sonnet | Scan memory, Discord, logs. Decide on next action. |
| seo-blog-writer | daily 09:00 | sonnet | Write one new SEO blog post, commit, push. |
| reddit-scout | 6 hours | sonnet | Scan r/ClaudeAI, r/LocalLLaMA for reply opportunities. |
| job-scout | daily 07:00 | haiku | Pull new postings from Lever, Ashby, Greenhouse. |
| job-applier | daily 10:00 | sonnet | Fill and submit applications via Playwright. |
| memory-backup | daily 02:00 | haiku | Dedupe, archive stale entries, refresh MEMORY.md index. |
| stripe-monitor | every 15 min | haiku | Check Stripe and Gumroad for new sales, alert Discord. |
Total incremental API cost after subtracting Claude Max usage is roughly $22 per month. The same cadence on Opus would run more than $500. Model routing is load-bearing.
Memory system guide
How the memory-backup and heartbeat crons keep CLAUDE.md lean and MEMORY.md honest.
Hooks guide
Use hooks for inside-session automation; use cron for between-session automation. The combo is powerful.
Subagents tutorial
Parallelise work inside a cron run by dispatching subagents for independent subtasks.
CLAUDE.md setup
Cron jobs inherit your CLAUDE.md. Keep it lean or pay for it on every run.
The 5-Minute Quick Start
If you want one cron job shipped before lunch, do exactly this. You can optimise later.
Pick one repeatable task you already do manually
Don't start with something novel. Pick a task you have executed interactively at least five times and know the success criterion for.
Write the wrapper script
Copy the Step 1 wrapper from this guide. Replace the prompt. Run it manually. Iterate until one successful run lands.
Schedule at a conservative cadence
Start daily, not hourly. You need log evidence that the job is reliable before you increase frequency.
Wire up a Discord webhook for failures
Five extra lines in the wrapper. Saves you from silent failures, which is the #1 way cron jobs rot.
Total time: ~30 minutes. Ongoing effort: zero.
Once one job is running and logging cleanly, every subsequent cron job is a 5-minute copy of the same pattern.
The complete playbook
Get every cron recipe, every hook, and the exact heartbeat loop that ships blog posts while I sleep -- in the KaiShips Guide to Claude Code.
This post is the cron chapter. The full guide covers memory, skills, subagents, hooks, MCP, and the end-to-end automation layer I run at KaiShips today.
Get the KaiShips Guide to Claude Code -- $29Running Claude Code on a Schedule Without Your Own Machine
If you don't want to keep a Mac online 24/7, GitHub Actions works as a zero-infrastructure scheduler. Runs free for public repos, cheap for private, and survives reboots.
# .github/workflows/daily-claude.yml
name: Daily Claude Code Job
on:
schedule:
- cron: "0 9 * * *" # 09:00 UTC daily
workflow_dispatch:
jobs:
run:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm install -g @anthropic-ai/claude-code
- name: Run Claude Code
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude -p --permission-mode acceptEdits --model sonnet \
"Run the /seo-2026 skill. Write one blog post. Commit + push."Same wrapper logic, just expressed in YAML. The prompt patterns, permission scoping, and cost levers all carry over verbatim.
Common Mistakes That Kill Cron Jobs
I have made each of these. Save yourself the debugging hours.
- ●Trusting $PATH. launchd has a minimal PATH. Export one in the wrapper or the
claudebinary will not resolve. - ●Running without a permission allowlist.Headless Claude Code cannot prompt you. A missing allow entry turns into a silent tool-denied error.
- ●Skipping timeouts. A hung session will quietly consume the next scheduled slot. Always wrap with
timeout 15mor similar. - ●Using Opus for everything. Opus is 60x the per-token price of Haiku. Route routine jobs to cheaper models or the bill will surprise you.
- ●Forgetting the cwd. Without an explicit cd, cron runs Claude Code from /, and your repo-relative prompts break.
- ●Letting jobs rot silently. Review logs weekly. A cron that stopped working 10 days ago is worse than never setting it up.
Frequently Asked Questions
Can Claude Code run on a cron schedule?
Yes. Claude Code supports a non-interactive headless mode via the `claude -p "<prompt>"` command, which makes it safe to wire into launchd, crontab, GitHub Actions, or any other scheduler. The process runs, executes the prompt with the permissions you allow, writes output to stdout, and exits. On macOS the standard pattern is a launchd plist calling a shell wrapper; on Linux it is a crontab line calling the same wrapper.
How do I authenticate Claude Code in a cron job?
Claude Code reads credentials from the same config directory it uses interactively (~/.claude on macOS and Linux). As long as you have logged in once with `claude login` under the user account that runs the cron, the scheduled invocation picks up the same OAuth token. For unattended servers, set ANTHROPIC_API_KEY in the wrapper script rather than exporting it from your shell profile, because launchd and cron do not source ~/.zshrc.
What is the difference between launchd and cron for Claude Code?
launchd is the macOS-native scheduler and is what Apple recommends over cron on Darwin. It uses an XML plist per job and survives sleep/wake cycles cleanly. cron is the traditional Unix scheduler, still present on macOS and default on Linux, configured via `crontab -e`. Both work for Claude Code. On a Mac running 24/7 I use launchd; on a Linux VPS I use cron. The prompt, wrapper script, and cost profile are identical.
How much does it cost to run Claude Code on cron?
Cost depends on model choice, prompt size, and run frequency. A Claude Sonnet 4.6 cron job with a 4KB prompt and a few tool calls typically costs $0.02 to $0.08 per run. At 24 runs per day (hourly) with Sonnet that is roughly $18 to $55 per month. Switching hourly heartbeat-style jobs to Claude Haiku 4.5 drops the per-run cost to around $0.002 and the monthly total to under $2. Model routing is the biggest lever.
How do I debug a Claude Code cron job that is failing silently?
Redirect both stdout and stderr to a log file in the wrapper script (`>> /path/to/job.log 2>&1`), and always add a timestamp at the start of each run. Run the wrapper manually in your shell first to confirm auth and paths work. For launchd, check /var/log/system.log or `launchctl list | grep <label>` for exit status. Most silent failures come from PATH not including the claude binary, missing ANTHROPIC_API_KEY, or the working directory defaulting to /.
Can I run Claude Code cron jobs on a server without a GUI?
Yes. Claude Code's headless mode does not require a display. Install the CLI on your server, run `claude login` once via SSH (it will print a URL to open on another device), and schedule the command with cron or systemd timers. Using an ANTHROPIC_API_KEY environment variable is cleaner for servers because it avoids OAuth token refresh edge cases.
Bottom Line
Claude Code on cron is the difference between a tool you open and an agent that runs the business while you sleep. The primitives are small: a wrapper script, a schedule entry, a permission allowlist, a log file, a webhook.
The hard part is writing prompts that do not stop to ask questions, and picking the right model for each cadence. Do both well and you get a compounding system -- every job you ship keeps paying off every day, with no ongoing effort.
Start with one job. Get it reliable. Then copy the pattern. Seven jobs in, you will notice the agent has a schedule of its own.
The complete playbook
Get every cron recipe, every hook, and the exact heartbeat loop that ships blog posts while I sleep -- in the KaiShips Guide to Claude Code.
This post is the cron chapter. The full guide covers memory, skills, subagents, hooks, MCP, and the end-to-end automation layer I run at KaiShips today.
Get the KaiShips Guide to Claude Code -- $29