How to Move Claude Code Sessions From Local Terminal Tests to Reliable Scheduled Cron Jobs
Turn your interactive Claude Code CLI workflows into production-grade cron jobs that log, timeout, and recover predictably.
TL;DR: Wrap your claude --print invocation in a Bash script using set -euo pipefail, cap runtime with timeout wrapped to survive strict mode, emit timestamped logs from within the script, and call the script directly from cron without extra redirects. Treat each run as stateless by working in a fresh temp directory.
Build a Stateless Wrapper Script with Strict Error Handling
A stateless wrapper script enforces strict error handling and isolation so every cron invocation starts from a known, clean state. This approach targets the Claude Code CLI running locally—not Managed Agents on the Claude Platform, which are a separate hosted product.
Start with Bash strict mode and a disposable working directory so leftover files, cached credentials, or partial state from earlier runs cannot corrupt the current session:
#!/bin/bash
set -euo pipefail
WORKDIR=$(mktemp -d)
cd "$WORKDIR"
Invoke claude non-interactively with --print and restrict the available tool surface via --allowedTools so the agent cannot perform unintended actions while unattended. Because set -e treats a non-zero exit from timeout as a fatal error, you must wrap the call to capture the real exit code before the script aborts:
LOG_DIR="$HOME/logs/claude-jobs"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/run-$(date +%Y%m%d-%H%M%S).log"
set +e
timeout 300 claude --print "Generate the daily metrics report" \
--allowedTools "Bash,Read" > "$LOG_FILE" 2>&1
rc=$?
set -e
After the command finishes, evaluate rc explicitly. An exit code of 0 signals success, 124 indicates a timeout, and any other value reflects an error raised by Claude Code itself. By handling logs internally with a timestamped file, you keep the crontab entry simple—just point cron at the wrapper—and guarantee every run leaves an isolated, auditable artifact that cannot be overwritten by the next scheduled execution.
Capture Timeouts Without Triggering Immediate Exit
Use a temporary set +e … set -e sandwich around the timeout invocation so its non-zero exit status does not abort the shell before you save $? into a variable. This preserves strict mode for the remainder of the script while still letting you distinguish a clean finish from a timeout or an agent-side failure without rewriting the whole job.
When timeout kills Claude Code after the allotted seconds, it exits with a non-zero status; a common timeout exit code is 124. Under set -e, that non-zero value triggers an immediate shell exit, which means rc=$? is never reached and your logs give no hint that the job hung. Because cron swallows terminal output, an unexplained silent exit looks identical to a success in the system mailbox. Suspend strict mode for exactly that command, capture the return code, then re-enable it so the rest of the script continues to fail fast on unexpected errors:
set +e
timeout 300 claude --print "Generate daily summary" --allowedTools "Bash,Read"
rc=$?
set -e
Branch on rc afterward to emit a clear status for your centralized logs:
if [ "$rc" -eq 0 ]; then
echo "$(date -Iseconds) status=OK"
elif [ "$rc" -eq 124 ]; then
echo "$(date -Iseconds) status=TIMEOUT"
else
echo "$(date -Iseconds) status=ERROR rc=$rc"
fi
If you would rather not toggle strict mode, let a conditional absorb the exit code implicitly:
if timeout 300 claude --print "Generate daily summary" --allowedTools "Bash,Read"; then
rc=0
else
rc=$?
fi
Either pattern prevents the script from terminating early and leaves rc available for downstream alerting or log parsing.
Redirect Output to a Single Timestamped Log File Inside the Script
Let the script capture its own output into a uniquely named log file so cron does not need any redirect operators and every run is preserved separately.
Define the log path with a timestamp at the very top of the script, then use exec to redirect the shell’s stdout and stderr permanently to that file. The 2>&1 merge ensures that Claude Code’s diagnostic messages and any Bash errors land alongside standard output in chronological order. Every subsequent command—including tool calls, API responses, and echo statements—lands in one place without any risk of interleaving from overlapping cron jobs.
LOGFILE="/var/log/claude-code-$(date +%Y%m%d-%H%M%S).log"
exec >>"$LOGFILE" 2>&1
echo "Run started: $(date -Iseconds)"
cd /home/user/project || exit 1
Because the script owns the stream entirely, the crontab entry should contain no trailing redirect operators. This makes the wrapper script the single source of truth for where output lives and eliminates the confusion of a second logging strategy in the scheduler.
0 6 * * * /bin/bash /home/user/agent-scripts/daily-sync.sh
With a unique file per execution, you can inspect or archive a specific run instantly by its filename. If you need to verify the latest result, list the directory and grep the newest log rather than parsing a merged global file. Keeping the redirect inside the script also ensures that local terminal tests and production cron runs produce identical artifacts, so debugging a failed scheduled job uses the exact same workflow as debugging a manual invocation.
Schedule in Cron Without Competing Redirects
Add the job with crontab -e and keep the cron line completely free of any >> /var/log/... redirect. Because the wrapper script already funnels stdout and stderr into its own timestamped log file, a second redirect on the cron line would create a competing sink and fragment the audit trail into two unrelated files.
Cron runs with a minimal environment that usually excludes your interactive PATH and any shell profile exports, so a non-interactive Claude Code session will fail to locate binaries like git or node or authenticate against the API unless you explicitly seed those variables. Rather than embedding secrets directly in the crontab, source a dedicated environment file at the very top of the wrapper script so credentials travel with the script, not with the scheduler.
0 6 * * * /bin/bash /home/user/agent-scripts/daily-sync.sh
Inside the wrapper, load the file before any tool is invoked:
#!/bin/bash
set -euo pipefail
source /home/user/agent-scripts/.env
The .env itself should export exactly what a headless session needs:
export PATH="/usr/local/bin:/usr/bin:/bin"
export CLAUDE_API_KEY="sk-ant-api03-..."
With the script owning the log stream entirely, all output lands in one predictable location. When you need to verify a run, grep the wrapper’s own log directory rather than searching across system logs. This also avoids permission problems that arise when an unprivileged cron job tries to append to /var/log and gets denied or writes a truncated entry.
Verify Runs by Grepping the Unified Log
Inspect the script's self-written logs to confirm behavior and catch timeouts or failures that cron itself will not surface. A single grep across the unified log files shows every start, failure, and exit condition in one chronological view.
Because the wrapper script handles its own timestamped logging internally, the crontab needs no output redirect and every run lands in a predictable path. Search across the rotated files to surface patterns:
grep -E '(START|FAIL|rc=124)' /var/log/claude-code-*.log
An rc=124 entry means the job hit the timeout ceiling and was terminated by timeout(1) before Claude could finish. Treat this as a hard failure: the output is incomplete, partial artifacts may remain in the temporary workspace, and the next run may need a longer limit or a narrower prompt. Normal completions log rc=0; any non-zero code other than 124 indicates Claude returned an application error or the wrapper detected an invalid state. If the grep returns nothing at all, the job never started—check the system mail or cron daemon logs for launch errors rather than Claude-level failures.
When testing changes locally, always invoke the full wrapper script instead of running a bare claude command. Only the wrapper creates the same mktemp working directory, applies the same timeout wrapping, and writes to the same log path that cron uses. Validating against the bare CLI risks hiding path, permission, or environment differences that appear only under the scheduled context.
FAQ
Is this guide about Claude Managed Agents?
No. This covers the Claude Code CLI invoked from cron on your own infrastructure. Managed Agents are general-purpose hosted agents that run on the Claude Platform, which is a separate product.
Why does my cron job exit silently when timeout fires?
If you use set -e, timeout's non-zero exit code triggers an immediate shell exit before your script can log the result. Temporarily disable strict mode around the timeout call with set +e, capture rc=$?, then restore with set -e.
Should I use cron or Claude Code Routines?
Claude Code Routines provide a native scheduled alternative, but they run within the Claude Code ecosystem. If you need to invoke local CLI tools, custom binaries, or filesystem paths on a specific host, a local cron job calling the Claude Code CLI gives you direct machine control.
How do I prevent one failed run from corrupting the next?
Treat every invocation as stateless. Create a fresh temporary working directory at the start of each run—for example, cd "$(mktemp -d)"—and never rely on files left behind by previous executions.
References for further reading
_Sources consulted while researching this guide, included so you can verify the details and go deeper. Listing them is not a claim that every line was independently fact-checked._
- Scheduled Tasks: The Loop Skill and Cron Tools
- Claude Code Routines: Scheduled Cloud Automation Without the DevOps Overhead | AI Magicx Blog | AI Magicx
- Managed Agents and Routines for Set-and-Forget Automation
- Learn The AI Agent Cron Job Inception Strategy (Claude Code)
I packaged the setup above into a ready-to-use kit — Scheduled-Agent Recipe Pack: 15 Automation Blueprints — for anyone who'd rather copy-paste than wire it from scratch: https://unfairhq.gumroad.com/l/oxvwdf.
Last updated: 2026-06-21