Run Claude Code on a schedule with cron or a systemd timer
On this page
The command: claude -p with explicit permissionsA wrapper script for cronAuthentication for unattended runssystemd user timers (Linux)Usage limits and scheduled jobsHosted alternatives from AnthropicHow Codeman handles itUse headless mode: claude -p "<prompt>" runs one task and exits, so cron or a systemd timer can start it like any script. Three things decide whether it works at 3am: permissions that never wait for an answer, an absolute path to claude plus credentials the job can read (a scheduler's environment is not your shell's), and a lock so a slow run cannot overlap the next.
The command: claude -p with explicit permissions
-p (--print) is Claude Code's non-interactive mode. It exits with code 0 on success and non-zero when the run fails, and a failure inside the run, such as missing authentication, is printed as the result on stdout. The flags that matter for a job nobody watches, all from the CLI reference:
| Flag | Why it matters unattended |
|---|---|
--permission-mode dontAsk |
Denies every call that would otherwise prompt. Reads and read-only commands still run, plus whatever --allowedTools lists. |
--allowedTools "Read,Edit,Bash(npm test *)" |
The exact tools and commands the job may use without asking. Bash(npm test *) matches npm test with or without arguments. |
--permission-prompts none |
Tells Claude that nobody can approve a request, so it does not retry denied calls. Requires v2.1.259 or later. |
--max-turns 40 |
Caps agentic turns; the run exits with an error at the limit. There is no limit by default. |
--output-format json |
One JSON object with result, session_id and total_cost_usd, easy to log and parse with jq. |
--bare |
Skips CLAUDE.md, hooks, skills, plugins and MCP discovery. Never reads your subscription login or CLAUDE_CODE_OAUTH_TOKEN, so it needs ANTHROPIC_API_KEY or an apiKeyHelper. |
Always pass a permission mode. With none, a -p run usually starts in Manual mode (the permission modes page lists the exceptions), where anything that would prompt is denied and the job quietly does less than you asked. acceptEdits and auto are the alternatives; --dangerously-skip-permissions is documented for isolated containers and VMs only (see Run Claude Code in a Docker sandbox).
Workspace trust catches scheduled jobs too. A -p run never shows the trust dialog, so permissions.allow rules in the project's .claude/settings.json are ignored in a folder you never trusted, with a warning on stderr: put the rules on the command line or in ~/.claude/settings.json, or run claude in that repository once and accept the dialog. Without --bare, a -p run also executes the repository's hooks and connects its .mcp.json servers without asking.
A wrapper script for cron
Keep the job in a script, so the crontab line stays short and you can test it by hand:
#!/usr/bin/env bash
# ~/bin/claude-deps.sh: one unattended Claude Code run, output on stdout/stderr
set -u
export PATH="$HOME/.local/bin:/usr/local/bin:/usr/bin:/bin" # include the dir of `command -v claude`
if [ -r "$HOME/.config/claude-cron.env" ]; then # CLAUDE_CODE_OAUTH_TOKEN=... (mode 600)
set -a; . "$HOME/.config/claude-cron.env"; set +a
fi
cd "$HOME/src/myapp" || exit 1
echo "== start $(date -Is)"
claude -p "Update minor and patch dependencies, run npm test, and commit on a new branch deps/$(date +%F) only if the tests pass. Do not push." \
--permission-mode dontAsk \
--allowedTools "Read,Edit,Bash(npm outdated *),Bash(npm install *),Bash(npm test *),Bash(git switch -c *),Bash(git add *),Bash(git commit *)" \
--max-turns 40 --output-format json < /dev/null
status=$?
echo "== exit $status $(date -Is)"
exit $status
Then crontab -e:
# m h dom mon dow command
17 3 * * 1-5 flock -n $HOME/.claude-deps.lock $HOME/bin/claude-deps.sh >> $HOME/claude-deps.log 2>&1
What each piece is for:
- PATH. cron starts jobs with a short default PATH (
/usr/bin:/binon Debian and Ubuntu). The native installer puts the launcher at~/.local/bin/claude; Homebrew installs under/opt/homebrewon Apple Silicon. An npm install under nvm sits in nvm's per-versionbindirectory, and nvm is a shell function cron never loads; the npm package'sclaudeis a native binary that does not invoke Node, so its absolute path is enough. Runcommand -v claudein your shell and use that directory. - HOME. cron sets
HOMEfrom/etc/passwdfor the crontab's owner, so a user crontab finds~/.claude. A root crontab or/etc/crontabentry runs as root, with none of your credentials. - Logging. Redirect both streams, because a missing login shows up on stdout as the run's result, not on stderr. Keep
date +%Fout of the crontab itself: an unescaped%there becomes a newline. (< /dev/nullin the script gives-p, which also reads stdin, an empty one.) - Overlap.
flock -n(util-linux) exits immediately if the previous run still holds the lock, so a slow Tuesday run cannot collide with Wednesday's.
On macOS, Apple describes cron as deprecated in favor of launchd. A cron job does not run while the Mac sleeps; a launchd job with StartCalendarInterval runs on wake.
Authentication for unattended runs
| Credential | How the job gets it | Notes |
|---|---|---|
Subscription login from /login |
~/.claude/.credentials.json on Linux, the Keychain on macOS |
Works when the job runs as you. Fails with Login expired · Please run /login once the login can no longer be renewed. |
CLAUDE_CODE_OAUTH_TOKEN |
Generated by claude setup-token, valid one year |
Uses your Pro, Max, Team or Enterprise plan. Model requests only. Not read with --bare. |
ANTHROPIC_API_KEY |
A Claude Console key | Metered API billing, no usage-limit resets. |
Mind the precedence order: ANTHROPIC_API_KEY beats CLAUDE_CODE_OAUTH_TOKEN, which beats the /login credential, and in -p mode an API key in the environment is always used. A stray key exported in a profile silently moves the job to API billing. claude auth status exits 0 when logged in and 1 when not, and its JSON authMethod field names the credential in use; run it once from the job to see what the scheduler sees.
systemd user timers (Linux)
A timer replaces both the crontab line and the lock. The service runs the same script:
# ~/.config/systemd/user/claude-deps.service
[Unit]
Description=Weeknight dependency bump with Claude Code
[Service]
Type=oneshot
ExecStart=%h/bin/claude-deps.sh
TimeoutStartSec=2h
# ~/.config/systemd/user/claude-deps.timer
[Unit]
Description=Run claude-deps on weeknights
[Timer]
OnCalendar=Mon..Fri 03:17
Persistent=true
[Install]
WantedBy=timers.target
systemctl --user daemon-reload
systemctl --user enable --now claude-deps.timer
systemctl --user list-timers # next and last run
journalctl --user -u claude-deps.service # the job's output
loginctl enable-linger "$USER" # run while you are logged out
Why this beats cron on Linux:
- Missed runs. With
Persistent=true, a run missed while the machine was off is triggered as soon as the timer is active again. - No overlap. A timer never spawns a second instance of its service; a run still going when the timer elapses is left running.
- Logs. stdout and stderr go to the journal without any redirection.
- A ceiling.
Type=oneshothas no start timeout by default;TimeoutStartSec=2hstops a runaway job with SIGTERM, which makesclaude -pexit with code 143.
The user manager is normally started at your first login, not at boot, which is what enable-linger changes. Its PATH is distribution-defined, so keep the export PATH line in the script. Test a calendar expression with systemd-analyze calendar "Mon..Fri 03:17".
Usage limits and scheduled jobs
A job signed in with your subscription draws on the same session and weekly allowances as your interactive work, and usage counts against both at once. When a limit is hit, Claude Code blocks further requests until the reset time in the message, and the built-in wait-and-continue is not offered to -p runs, so nothing resumes the job. Keep heavy jobs away from your working hours, cap them with --max-turns, and read the log rather than assuming a clean exit. With an API key there is no reset; --max-budget-usd caps a -p run's estimated spend instead.
Hosted alternatives from Anthropic
- GitHub Actions. The official
anthropics/claude-code-action@v1runs on anon: schedulecron trigger when you give it aprompt, authenticating withANTHROPIC_API_KEYorCLAUDE_CODE_OAUTH_TOKEN; tools are granted with--allowedToolsinclaude_args. Runs use GitHub Actions minutes, schedules run only from the default branch, and public repositories lose the schedule after 60 days without activity. - Routines. Routines (research preview; Pro, Max, Team and Enterprise) run a saved prompt against fresh clones of your GitHub repositories on Anthropic's cloud, so your computer can be off. Create them at claude.ai/code/routines or with
/schedule. The minimum interval is one hour, local files are out of reach, and runs draw on your subscription usage without stopping for permission prompts. - In-app options. Anthropic's scheduling comparison also lists Desktop scheduled tasks and
/loop./looplives inside an open session and its recurring tasks expire after seven days, so it is not a cron replacement.
How Codeman handles it
Codeman has its own scheduler, Cron Jobs. Instead of claude -p, each run starts the interactive CLI in a tmux session, so a scheduled run is a normal session you can open in the browser, watch and take over.
- Where it lives. A Cron button in the bottom toolbar, hidden by default; switch it on under App Settings → Header & Panels → Scheduling. A job has a name, an agent type (Claude, Terminal/Shell, OpenCode, Codex, Gemini, Antigravity, Pi, Grok, DeepSeek or OMP), a working directory, a prompt and a schedule.
- Schedules.
once,interval(1 minute to a year),dailyorweekly, in the server's local timezone. Codeman checks every 30 seconds and advances the schedule before launching, so a slow start cannot fire the same slot twice. A job that came due while the server was down fires once when it is back. - The prompt. Codeman waits for the CLI to be ready, then sends the prompt and presses Enter. Prompts must be one line; for longer instructions, point it at a file (
read TASKS.md and work through it). - Overlap and clutter. An optional policy skips a scheduled run while another live session of the same agent type exists (the job's own earlier sessions do not count), and recurring jobs close the previous run's session by default.
- Run history records whether the session started and the prompt was sent, or why a run failed or was skipped, not whether the task succeeded.
- Permissions. Claude jobs use the Startup Mode from App Settings → Agents & CLIs, which defaults to
--dangerously-skip-permissions;auto, normal prompting and an explicit allowed-tools list are the alternatives.
Because the run is an interactive session, Claude Code's own wait-and-continue after a usage limit applies to it (v2.1.234 and later, with a subscription login); see Auto-continue Claude Code when your usage limit resets. Codeman has to be running at the scheduled time (Running As A Service), and Notifications And Approvals covers hearing about a run that stopped on a question. Jobs can also be created with POST /api/cron/jobs.