Codeman logocodeman GitHub →

Run Claude Code on a schedule with cron or a systemd timer

Updated 2026-10-10

On this pageThe 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 it

Use 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:

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:

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

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.

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.