From daily-cron-agent
How to run Claude Code CLI as a daily autonomous agent via cron. Battle-tested pattern for scheduled tasks including parallel execution, structured logging, error handling, and optional Telegram notifications.
How this skill is triggered — by the user, by Claude, or both
Slash command
/daily-cron-agent:daily-cron-agentThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
How to run Claude Code CLI as a daily autonomous agent via cron. Battle-tested pattern for scheduled tasks including parallel execution, structured logging, error handling, and optional Telegram notifications.
How to run Claude Code CLI as a daily autonomous agent via cron. Battle-tested pattern for scheduled tasks including parallel execution, structured logging, error handling, and optional Telegram notifications.
Claude Code CLI can be invoked non-interactively with -p (prompt) flag, making it suitable for cron-scheduled autonomous tasks. The pattern:
claude -p "...") do the actual workANTHROPIC_API_KEY environment variable (set in crontab or .env)crontab -e on the target machineclaude -p "Read project/.claude/CLAUDE.md. Then execute the skill at \
project/.claude/skills/{skill-name}/SKILL.md for date $DATE. \
Save output to project/.tmp/$DATE/{output_file}" \
--max-turns 15 \
--allowedTools "WebSearch,WebFetch,Read,Write,Glob,Grep,Bash"
| Flag | Purpose | Typical Value |
|---|---|---|
-p "..." | Non-interactive prompt | Full instructions as a string |
--max-turns N | Limit conversation turns | 10-35 depending on task complexity |
--allowedTools "..." | Restrict available tools | Comma-separated list (security + cost control) |
| Task Type | Recommended | Rationale |
|---|---|---|
| Simple research (single topic) | 10-15 | Focused, bounded scope |
| Multi-source research track | 15-20 | Needs several search + fetch cycles |
| Synthesis / report writing | 20-35 | Reads multiple inputs, writes structured output |
| Full pipeline (research + write) | 25-35 | Complex multi-phase task |
Conservative is better. If an agent hits max-turns, it gracefully stops. If it runs away with unlimited turns, it burns API credits.
Restrict tools to what the task actually needs:
| Tool | When to Allow |
|---|---|
WebSearch | Research tasks that need web search |
WebFetch | Tasks that need to read specific URLs |
Read | Always (reads project files, rules, skills) |
Write | Tasks that produce output files |
Edit | Tasks that modify existing files |
Glob | Tasks that need to find files |
Grep | Tasks that need to search file contents |
Bash | Tasks that run scripts (Python, etc.) |
Do NOT allow tools the task does not need. This prevents unexpected behavior and reduces cost.
The shell script is the orchestration layer. It handles everything that should be deterministic: path resolution, environment setup, logging, parallel execution, and error handling.
#!/bin/bash
set -euo pipefail
# ===== 1. Path Resolution =====
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")" # adjust based on script location
OPS_DIR="$(dirname "$PROJECT_DIR")"
cd "$OPS_DIR" # all paths relative to workspace root
# ===== 2. Environment Setup =====
source ~/.bashrc 2>/dev/null || true
export PATH="/usr/local/bin:/usr/bin:$HOME/.local/bin:$PATH"
source "$OPS_DIR/.venv/bin/activate" 2>/dev/null || true
# ===== 3. Date-Based Logging =====
DATE=$(date +%Y%m%d)
LOG_DIR="$PROJECT_DIR/.tmp/$DATE"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/pipeline.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# ===== 4. Optional Notification Helper =====
notify() {
python3 "$PROJECT_DIR/.claude/skills/{skill}/scripts/send_notification.py" \
--message "$1" 2>&1 | tee -a "$LOG_FILE" || true
}
# ===== 5. Pipeline Phases =====
log "=== Pipeline started ==="
# ... (phases go here)
log "=== Pipeline complete ==="
set -euo pipefail: Exit on error, treat unset variables as errors, fail on pipe errors. This prevents silent failures.$(cd ... && pwd): Resolves to absolute paths regardless of where the script is called from.cd "$OPS_DIR": Set working directory explicitly. Cron does not inherit your shell's working directory..bashrc, activate venv, and set PATH.|| true on non-critical commands: Prevents the script from dying on optional steps (notifications, logging).For multi-track research or any task that can be split into independent subtasks:
# ===== Phase 1: Parallel Execution =====
log "=== Phase 1: Parallel Tasks ==="
# Task A
claude -p "..." --max-turns 15 --allowedTools "..." \
2>&1 | tee -a "$LOG_DIR/task_a.log" &
PID_A=$!
# Task B
claude -p "..." --max-turns 15 --allowedTools "..." \
2>&1 | tee -a "$LOG_DIR/task_b.log" &
PID_B=$!
# Wait for all parallel tasks
wait $PID_A || log "WARNING: Task A failed"
wait $PID_B || log "WARNING: Task B failed"
log "Phase 1 complete"
# ===== Phase 2: Sequential Processing =====
log "=== Phase 2: Synthesis ==="
claude -p "Read outputs from Phase 1 and synthesize..." \
--max-turns 20 --allowedTools "..." \
2>&1 | tee -a "$LOG_FILE"
PHASE2_EXIT=$?
if [ $PHASE2_EXIT -ne 0 ]; then
log "ERROR: Phase 2 failed with exit code $PHASE2_EXIT"
notify "Pipeline FAILED at Phase 2. Check logs."
exit 1
fi
log "Phase 2 complete"
# ===== Phase 3: Notification =====
log "=== Phase 3: Notification ==="
notify "Pipeline complete for $DATE"
&: Each claude -p invocation runs in the background.$!: Used to wait for specific processes.wait $PID || log "WARNING: ...": Wait for each process, log failure but continue.wait to ensure all parallel tasks complete before starting the next phase.# Daily research pipeline at 4:00 AM JST
0 4 * * * /home/user/project/automation/run_nightly.sh >> /home/user/project/.tmp/cron.log 2>&1
If the API key is not in the shell environment when cron runs:
# Set API key for Claude Code CLI
ANTHROPIC_API_KEY=sk-ant-...
# Daily pipeline at 4:00 AM
0 4 * * * /home/user/project/automation/run_nightly.sh >> /home/user/project/.tmp/cron.log 2>&1
Cron uses the system timezone. If your server is UTC but you want JST scheduling:
# JST is UTC+9. For 4:00 AM JST, use 19:00 UTC (previous day)
0 19 * * * /home/user/project/automation/run_nightly.sh >> /home/user/project/.tmp/cron.log 2>&1
Or set the timezone in crontab:
TZ=Asia/Tokyo
0 4 * * * /home/user/project/automation/run_nightly.sh >> /home/user/project/.tmp/cron.log 2>&1
If a pipeline might run longer than the cron interval:
# At the top of the shell script
LOCKFILE="$PROJECT_DIR/.tmp/pipeline.lock"
if [ -f "$LOCKFILE" ]; then
log "ERROR: Pipeline already running (lockfile exists). Exiting."
exit 1
fi
trap "rm -f $LOCKFILE" EXIT
touch "$LOCKFILE"
# Every day at 6:00 AM
0 6 * * * /path/to/run.sh
# Every day at 1:30 AM
30 1 * * * /path/to/run.sh
# Monday and Thursday at 2:30 AM
30 2 * * 1,4 /path/to/run.sh
# Every Monday at 11:00 PM
0 23 * * 1 /path/to/run.sh
# Every 6 hours
0 */6 * * * /path/to/run.sh
When writing the -p prompt for claude -p, structure it as a complete instruction set:
"Read {project}/.claude/CLAUDE.md for project context. \
Read {project}/.claude/rules/{relevant_rule}.md for domain knowledge. \
Then read and execute {project}/.claude/skills/{skill}/SKILL.md for date $DATE. \
Save all outputs to {project}/.tmp/$DATE/. \
If any step fails, log the error and continue with remaining steps."
$DATE from the shell script. This ensures consistent date-based file organization.project/
├── automation/
│ └── run_nightly.sh # Cron-triggered shell script
├── .claude/
│ ├── CLAUDE.md # Project context
│ ├── rules/ # Domain knowledge
│ ├── skills/ # Workflow definitions
│ │ └── {skill}/
│ │ ├── SKILL.md # Workflow steps
│ │ └── scripts/ # Deterministic scripts
│ └── agents/ # Agent definitions (optional)
│ └── {agent}/
│ └── AGENT.md
└── .tmp/
├── {YYYYMMDD}/ # Daily output
│ ├── pipeline.log
│ ├── track_a.json
│ ├── track_b.json
│ ├── synthesis.json
│ └── report.md
├── registry/ # Persistent data
│ └── history.json
└── cron.log # Cron-level log
tail -50 /path/to/project/.tmp/cron.log
cat /path/to/project/.tmp/20260312/pipeline.log
cat /path/to/project/.tmp/20260312/track_a.log
# Run from the same directory cron would use
cd /path/to/workspace && bash /path/to/project/automation/run_nightly.sh
| Symptom | Cause | Fix |
|---|---|---|
| "command not found: claude" | PATH not set in cron | Add explicit PATH in shell script |
| Empty output files | Agent ran out of turns | Increase --max-turns |
| Permission denied | Script not executable | chmod +x run_nightly.sh |
| API key errors | ANTHROPIC_API_KEY not set | Set in crontab or .env file |
| "Pipeline already running" | Previous run didn't finish | Check for stale lockfile, investigate slow run |
Pattern based on 15+ production cron agents running daily on VPS infrastructure.
Provides a checklist for code reviews covering functionality, security, performance, maintainability, tests, and quality. Use for pull requests, audits, team standards, and developer training.
npx claudepluginhub mtkana/claude-code-plugins --plugin daily-cron-agent