How Do I Schedule Claude Code to Run Maintenance Jobs Overnight?

Claude Code is most useful when you're watching it. It's most valuable when you're not. A scheduled overnight job can triage the inbox of bugs, regenerate the changelog from yesterday's commits, and file three draft PRs for lint errors before you sit down with coffee. Here's the minimum-viable setup that runs Claude Code unattended, reliably, starting tonight.
Why this matters
Most developers use Claude Code in a tight feedback loop — type, watch, correct, repeat. That's the right mode for building. But it leaves the long tail of maintenance work on your plate: weekly dep bumps, stale issue triage, refactoring a pattern across the codebase, regenerating documentation from the source.
That work is perfect for a scheduled job. No human latency, no context switch cost, and Claude doesn't get bored on iteration #47. The constraint is that Claude has to behave without a human in the loop, which means you need three things: a deterministic prompt, strict quality gates, and a way to catch failures. This guide sets up all three.
Before you start
You need:
- Claude Code installed and authenticated on the machine that'll run the schedule. A laptop works for a week; for real ongoing use, a dedicated VM or your CI runner is better.
- An API-plan or subscription that permits headless runs. Check your plan's current terms — scheduled automation is usually fine but you want to read before you run.
- A concrete overnight task. "Triage any new issue labeled
bugopened since yesterday and suggest a reproduction" is a great first one. - A working CLAUDE.md and hooks setup in the target repo — see Write a CLAUDE.md That Works and Claude Code Hooks. These are your safety rails when you're asleep.
Step 1: Write the job as a single headless command
Claude Code supports non-interactive runs with --print (or -p), which takes a prompt, runs to completion, and exits. The shape:
claude --print --dangerously-skip-permissions "$(cat <<'PROMPT'
You are running unattended.
Task: Look at all GitHub issues in this repo opened or updated
in the last 24 hours that are labeled "bug" and have no
assignee. For each one:
1. Read the issue body.
2. Check the relevant code path.
3. Append a comment with:
- a one-line summary of the likely root cause
- a minimal reproduction if you can produce one
- which file and line to start debugging
Do NOT close issues. Do NOT make code changes. Do NOT assign
yourself. Only post comments.
When done, write a one-line summary to STDOUT.
PROMPT
)"Two critical things:
- Explicit constraints. "Do NOT close issues." etc. Claude doesn't have a human to check with, so ambiguity becomes errors.
- Hard exit criteria. "When done, write a one-line summary." No dangling "and anything else you think of."
Save this as scripts/nightly-triage.sh.
--dangerously-skip-permissions bypasses Claude's interactive approval prompts. Only use it for prompts where you've reviewed and trust every action path. Consider a narrower permissions config file instead — see the Claude Code docs for per-tool allow/deny rules.
Step 2: Wrap the script with logs and error handling
scripts/run-nightly.sh:
#!/usr/bin/env bash
set -uo pipefail
cd /path/to/your/repo
TIMESTAMP=$(date -u +%Y%m%dT%H%M%SZ)
LOG_DIR="$HOME/claude-nightly-logs"
mkdir -p "$LOG_DIR"
LOG="$LOG_DIR/$TIMESTAMP.log"
# Pull latest so Claude reasons against current code
git fetch --all --quiet
git checkout main --quiet
git pull --ff-only --quiet >> "$LOG" 2>&1
# Run the job, tee output to the log
./scripts/nightly-triage.sh 2>&1 | tee -a "$LOG"
EXIT_CODE=${PIPESTATUS[0]}
if [ $EXIT_CODE -ne 0 ]; then
# Optional: notify a webhook, email, or Slack on failure
curl -s -X POST "$NIGHTLY_FAILURE_WEBHOOK" \
-d "Nightly triage failed. Log: $LOG" || true
fi
exit $EXIT_CODELogs in a known location, non-zero exit on failure, an optional failure notification. That's the minimum for "I'm asleep when this runs."
Step 3: Schedule it
On macOS or Linux, cron:
crontab -eAdd a line:
0 3 * * * /path/to/scripts/run-nightly.sh > /dev/null 2>&1Runs 3am every day. Redirect stdout/stderr to null because run-nightly.sh already handles logging.
For server deployments, prefer systemd timers or your platform's scheduler (GitHub Actions schedule, Railway cron, Fly machines, etc.) over host cron — they're easier to debug and version-control.
Step 4: Add quality gates in the job
Remember: nobody's watching. If Claude hallucinates a file edit that breaks main, you find out when the next human tries to commit.
For any job that modifies code (vs. just commenting on issues), gate behind a full test pass:
npm test -- --run
TEST_EXIT=$?
if [ $TEST_EXIT -ne 0 ]; then
echo "Tests failed — reverting Claude's changes"
git reset --hard HEAD
exit 1
fiCombined with your Claude Code hooks from the hooks guide, this gives you: hooks block bad commits in the Claude session, and the wrapper script blocks bad branches from being pushed.
For jobs that push changes, open a PR instead of pushing to main:
BRANCH="nightly/$(date -u +%Y%m%d)"
git checkout -b "$BRANCH"
# Claude's work happens here
git push origin "$BRANCH"
gh pr create --title "Nightly automated triage" --body "Generated by scheduled Claude run on $(date -u)"You review and merge in the morning, or not — the job ran harmlessly either way.
Step 5: First run — kick it off manually
Do not wait for 3am to find out it's broken. Run it now:
./scripts/run-nightly.shWatch the log. Iterate on the prompt until:
- Claude completes the task.
- Exit code is 0.
- Output looks right.
Then let the schedule take over.
Verify it worked
1. Log file exists for the most recent scheduled run. Check $HOME/claude-nightly-logs/ — each run should produce a timestamped file.
2. Artifacts landed. If the job posts GitHub comments, they should be there with yesterday's timestamp. If the job opens PRs, they should exist.
3. Failures notify you. Deliberately break the prompt (e.g., give Claude an impossible instruction), run manually, confirm you get the failure notification.
Where this breaks
- API rate limits or subscription caps. A chatty scheduled run can exhaust your daily limit. Track token usage in the log, and if you're bumping against limits, switch to the Batch API for jobs that aren't time-sensitive — it's half the cost.
- Silent prompt drift. A prompt that worked in January may misbehave in April because dependencies changed, the codebase shape shifted, or Claude's default behavior updated. Review logs weekly and re-test manually once a month.
- Authentication expiring. Your Claude Code session auth can expire. If the schedule runs for 30 days without you logging in, it may suddenly start failing with auth errors. Use an API-key-based setup for production jobs, not session auth.
- The machine isn't on at 3am. If cron is on your laptop and the lid is closed, nothing runs. For real production use, schedule on an always-on machine — GitHub Actions, a cloud VM, or your company's CI runner.
--dangerously-skip-permissionsdoing something dangerous. That flag grants Claude every permission. A prompt with ambiguous scope ("clean up the repo") can become "delete everything." Always pair the flag with very explicit prompts and hard quality gates.
What to try next
- How Do I Set Up Claude Code Hooks for Auto-Quality? — the safety rails that make unattended Claude possible.
- How Do I Cut My Anthropic Bill in Half Using the Batch API? — when your scheduled workload gets big, switch to Batch.
- How Do I Install Claude in Chrome and Schedule a Recurring Browser Task? — the browser-side equivalent for non-code work.
Let's talk about your AI + SEO stack
If you'd rather skip the how-to and have it shipped for you, that's what I do. Start a conversation and we'll figure out the fastest path to results.
Let's Talk