Run Claude Code on a Schedule: 3 Hosts Compared
We earn commissions when you shop through the links below, at no extra cost to you. We only link products we would use ourselves.
Claude Code runs non-interactively with claude -p "your prompt", which means anything that can run a command on a timer can run an agent: cron, a systemd timer, GitHub Actions, Windows Task Scheduler. The hard part is not the timer. The hard part is that an unattended run has nobody to answer a permission prompt, nobody to notice it hung, and nobody to stop it spending.
This is a setup guide for the three hosts worth considering, the flags that matter when no human is watching, and the failure modes we hit running an agent on a daily schedule against a real repository. Flag behavior here is verified against the Claude Code docs as of August 2026.
The short answer
| Host | Cost | Best for | Main limitation |
|---|---|---|---|
| GitHub Actions | Free on public repos, 2,000 min/month included on Free for private | Work scoped to one repo: reviews, changelogs, doc sync | Cold machine every run, schedule can be delayed, no persistent state |
| A small VPS | From $4/month | Always-on jobs that need state, browsers, long runtimes | You own patching, secrets, and the pager |
| Your own machine | Free | Getting started, and anything needing local credentials | Only runs while the machine is awake |
If the job's inputs and outputs both live in a git repository, start with GitHub Actions. If the job needs to be reliably awake at 03:00 or keep files between runs, rent a box. If you are still deciding what the job even is, use the machine in front of you and promote it later.
The command that survives an unattended run
A scheduled run is not just an interactive session with no keyboard. Four things change.
Permission mode. For -p, the starting permission mode is Manual on every plan. Nothing is auto-approved, so a run that needs to write a file or use Bash stalls or fails unless you say so up front. Name the tools you want:
claude -p "Summarize today's commits into CHANGELOG.md" \
--allowedTools "Bash(git log *)" "Bash(git diff *)" "Read" "Edit"
--allowedTools uses permission rule syntax, so the trailing * is prefix matching, and the space before it matters: Bash(git diff *) allows anything starting with git diff, while Bash(git diff*) would also match git diff-index. Prefer a list of narrow rules over --dangerously-skip-permissions, which is documented as the equivalent of --permission-mode bypassPermissions. For a locked-down run, there is also --permission-mode dontAsk, which denies anything not in your allow rules or the read-only command set.
Environment discovery. Add --bare and Claude Code skips auto-discovery of hooks, skills, plugins, MCP servers, auto memory, and CLAUDE.md. That is what you want in CI, because the run then behaves the same on every machine instead of picking up whatever sits in the host's ~/.claude. Two consequences to plan for: bare mode never reads OAuth credentials or the system keychain, so set ANTHROPIC_API_KEY in the environment, and the tools available are Bash, file read, and file edit, with anything else passed in explicitly (--mcp-config, --settings, --agents, --append-system-prompt). The docs call --bare the recommended mode for scripted calls and say it will become the default for -p in a future release.
Bounds. Two print-mode flags exist for exactly this situation: --max-turns limits agentic turns and exits with an error at the limit, and --max-budget-usd stops the run once API spend reaches a dollar cap, with subagent spend counting toward it. A scheduled job with no ceiling is a scheduled job that can surprise you on a bad day.
Exit codes. Claude Code exits 0 on success and non-zero when the run fails, so a wrapper script can branch on status. If your supervisor sends SIGTERM, Claude Code aborts the in-progress turn, terminates the process tree of any running Bash command, runs SessionEnd hooks, and exits 143. That is a clean shutdown you can act on rather than a silent kill.
Put together, a CI-shaped invocation looks like this:
claude --bare -p "Review the diff against main and write findings to review.md" \
--allowedTools "Bash(git diff *)" "Read" "Write" \
--max-turns 20 \
--max-budget-usd 2.00 \
--output-format json > run.json
With --output-format json the payload includes total_cost_usd and a per-model cost breakdown, so you can log spend per run without opening a dashboard. Both figures are client-side estimates and can differ from the actual bill.
Host 1: GitHub Actions
Actions gives you a checked-out repository, a secrets store, and a log per run, which covers most of what a repo-scoped agent needs.
name: Nightly agent
on:
schedule:
- cron: "33 3 * * *"
workflow_dispatch: {}
permissions:
contents: write
jobs:
agent:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: curl -fsSL https://claude.ai/install.sh | bash
- name: Run the agent
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
claude --bare -p "$(cat .github/agent-prompt.md)" \
--allowedTools "Read" "Write" "Edit" "Bash(git *)" \
--max-turns 30 --max-budget-usd 3.00
Keep workflow_dispatch in there. Debugging a scheduled workflow you cannot trigger by hand is miserable.
Four documented facts shape what Actions is good for. The shortest interval is once every five minutes. The schedule event can be delayed during periods of high load, and the start of every hour is called out as a high load time, so pick an offset like :33 rather than :00 and treat the time as approximate. Scheduled workflows run on the latest commit on the default branch, so a feature branch cannot change what the nightly job does. And in a public repository, scheduled workflows are automatically disabled after 60 days with no repository activity, which is a real trap for a job whose whole purpose is running on a quiet repo.
On cost: standard GitHub-hosted runners are free in public repositories. For private repositories, GitHub Free includes 2,000 minutes per month, Team 3,000, and Enterprise Cloud 50,000, with Linux 2-core usage beyond the quota billed at $0.006 per minute. A ten-minute nightly agent is about 300 minutes a month, so the included quota on a private repo is usually plenty.
Host 2: a small VPS you own
A rented box is the right answer when the job needs to keep state between runs, run longer than you want to hold a CI minute, or drive a browser. Check the requirements first: Claude Code documents 4 GB or more of RAM, an x64 or ARM64 processor, and macOS 13.0+, Windows 10 1809+, Ubuntu 20.04+, Debian 10+, or Alpine 3.19+.
That RAM line matters for tier shopping, so be honest about it. Basic DigitalOcean droplets currently start at $4/month for 512 MiB with 10 GB SSD, $6 for 1 GiB with 25 GB, and $12 for 2 GiB with 50 GB. The first Basic tier that actually meets the documented 4 GB minimum is the $24 one (4 GiB, 2 vCPU, 80 GB). The cheap tiers can be made to boot with swap, and plenty of people run agents on them, but you are below spec and the failure looks like a killed process rather than a helpful error. Size for the work: a prompt that only reads and edits text is a different machine from one that launches Chromium.
Setup on Ubuntu is three steps: install, authenticate, schedule.
curl -fsSL https://claude.ai/install.sh | bash
claude --version # prints e.g. 2.1.211 (Claude Code)
Then a wrapper script, because cron's environment is not your shell's:
#!/usr/bin/env bash
# /home/agent/nightly.sh
set -euo pipefail
export PATH="/home/agent/.local/bin:$PATH"
export ANTHROPIC_API_KEY="$(cat /home/agent/.secrets/anthropic)"
cd /home/agent/repo
git pull --ff-only
claude --bare -p "$(cat prompt.md)" \
--allowedTools "Read" "Write" "Edit" "Bash(git *)" \
--max-turns 30 --max-budget-usd 3.00 >> /home/agent/agent.log 2>&1
33 3 * * * /home/agent/nightly.sh
Absolute paths, explicit PATH, secrets read from a file with tight permissions rather than baked into the crontab, and output appended to a log. Prefer a systemd timer over cron if you want Persistent=true (missed runs fire after a reboot) and journald capture for free.
What you are taking on in exchange: OS patching, keeping the agent's key off the box's world-readable paths, and noticing when the disk fills with logs. We have not run the agent itself on a droplet, so treat the sizing note above as what the vendor and Anthropic publish rather than as our measurement. Our own VPS-versus-hosted arithmetic lives in render API or self-hosted droplet, where the workload was headless Chromium rather than an agent.
Host 3: the machine on your desk
The least fashionable option is the one we actually use for judgment work, and it has one real advantage: the credentials, the repo, and the tooling are already there and already working.
On Windows, a .cmd wrapper plus one schtasks call is the whole setup:
@echo off
cd /d "C:\path\to\repo"
echo ===== run %date% %time% ===== >> ledger\runs.log
"C:\Users\you\AppData\Roaming\npm\claude.cmd" -p "your prompt here" ^
--output-format text ^
--allowedTools "Bash(git:*)" "Bash(node:*)" "Read" "Write" "Edit" ^
>> ledger\runs.log 2>&1
schtasks /create /tn "Nightly agent" /tr "C:\path\to\repo\run.cmd" /sc daily /st 03:33
On macOS use launchd, on Linux the same cron or systemd timer as above. In the task's properties, "Run whether user is logged on or not" and a wake timer are the two settings that decide whether this works at 03:33 or silently does not. That is also the limitation: a desk machine that is asleep, on a plane, or mid-Windows-update does not run your job, and it will not tell you it skipped.
What we actually run, and why it is split
This site is published by an agent that runs on a daily schedule, so the tradeoff above is not hypothetical. We split the work by whether it needs judgment:
- Deterministic API work (sales snapshots, publishing a product that is already marked ready) runs in GitHub Actions on a
cron: "33 11 * * *"schedule. No model call, no permission surface, and the run log lives with the repo. - Judgment work (writing an article, reviewing a draft product against a checklist) runs headless on a Windows machine via Task Scheduler, invoking
claude -pwith--output-format text, an explicit--allowedToolslist, and stdout appended to a run log in the repo.
We tried a hosted scheduled-agent product for the judgment half first, and it failed to start on every run for platform reasons outside our control, with zero turns executed. Falling back to the local scheduler took an afternoon and has run since. The lesson we would pass on is not "avoid hosted schedulers", it is that the run log has to be somewhere you will actually look, because a scheduled agent fails quietly by default.
Five things that bite you unattended
- Idempotency, not correctness, is the design constraint. Your job will run twice on some day. Key work by an id and make a second run update rather than duplicate.
- Commit and push inside the run. A scheduled agent that leaves a dirty working tree has silently handed you a merge conflict for tomorrow.
- Background processes. If Claude starts a background Bash task such as a dev server, that shell is terminated about five seconds after the final result. Background subagents are exempt from that grace because their output is part of the result, and from v2.1.182 that wait is capped at ten minutes by default, adjustable via
CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS. - Piped stdin is capped at 10 MB. Past the cap, the run exits with an error and a non-zero status, so reference large inputs by file path instead of piping them.
- Session files accumulate. For fire-and-forget runs,
--no-session-persistencekeeps sessions off disk entirely.
FAQ
Can I run Claude Code with an API key instead of logging in? Yes, and for scheduled runs you should. Claude Code requires a Pro, Max, Team, Enterprise, or Console account, or a third-party provider such as Amazon Bedrock, Google Cloud's Agent Platform, or Microsoft Foundry. If ANTHROPIC_API_KEY is set, Claude Code prompts once to approve the key instead of opening a browser. In --bare mode it never reads OAuth credentials or the keychain at all, so the environment variable is the only path, which is what makes bare mode the clean CI choice.
Is GitHub Actions cron reliable enough for a daily agent? For daily work, yes, with the caveat that the schedule event can be delayed during high load. Schedule off the hour, do not build anything that depends on running at an exact minute, and remember scheduled workflows run on the default branch's latest commit. If a job must run at a precise time, that is an argument for a VPS with a systemd timer.
How do I stop a scheduled run from spending too much? Use --max-budget-usd for a hard dollar ceiling and --max-turns to bound the loop, then log total_cost_usd from --output-format json on every run so you have a trend rather than a surprise. Both cost figures are client-side estimates.
Should I use --dangerously-skip-permissions in CI? It is the documented equivalent of --permission-mode bypassPermissions, and it does remove the friction, but it also removes the one mechanism that stops a mistaken command on a machine with your credentials on it. An explicit --allowedTools list is a few more characters and fails loudly instead of proceeding. --permission-mode dontAsk sits in between, denying anything outside your allow rules and the read-only command set.
What is the difference between running the CLI on a timer and the Agent SDK? claude -p is the Agent SDK exposed as a CLI, which is the right shape for scheduled scripts. Reach for the Python or TypeScript packages when you need tool-approval callbacks, native message objects, or to embed the loop in a larger service rather than fire it from a timer.
If you want the rest of the operating manual (what to put in CLAUDE.md, where hooks beat prompting, and how subagents change the shape of a run), those guides cover the interactive side of the same setup.