- runner.sh: opt-in session persistence via session_resumable and resume_last_session settings; fix read_setting to normalize booleans - issue-poller.sh: capture and log session_id from worker invocations, include in result JSON - pr-reviewer-dispatcher.sh: capture and log session_id from reviews - n8n workflow: add --append-system-prompt to initial SSH node, add Follow Up Diagnostics node using --resume for deeper investigation, update Discord Alert with remediation details - Add Agent SDK evaluation doc (CLI vs Python/TS SDK comparison) - Update CONTEXT.md with session resumption documentation Closes #3 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
7.1 KiB
| title | description | type | domain | tags | |||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Scheduled Tasks Overview | Headless Claude Code sessions triggered by systemd timers using runner.sh template framework and custom dispatcher scripts. Covers task layout, settings reference, cost safety, and monitoring. | context | scheduled-tasks |
|
Scheduled Tasks — Headless Claude Sessions on a Timer
Headless Claude Code sessions triggered by systemd timers. Runs claude -p with task-specific prompts, tools, and cost limits — no TTY required.
For a guide on creating new tasks, see: ~/.claude/skills/create-scheduled-task/SKILL.md
File Layout
~/.config/claude-scheduled/
├── runner.sh # Universal task runner (template-based tasks)
├── disabled # Touch to globally pause all template tasks
├── tasks/
│ ├── backlog-triage/ # Example template task
│ │ ├── prompt.md # What Claude should do
│ │ ├── settings.json # Model, budget, tools, working dir
│ │ └── mcp.json # MCP servers (optional)
│ ├── issue-poller/ # Used by issue-poller.sh (not runner.sh)
│ ├── issue-worker/ # Agent instructions + settings
│ └── pr-reviewer/ # Agent instructions + settings
├── issue-poller.sh # Custom dispatcher for issue scanning
├── issue-dispatcher.sh # Dispatches AI agent per issue
├── pr-reviewer-dispatcher.sh # Dispatches AI reviewer per open PR
└── repos.json # Gitea repos to scan
~/.config/systemd/user/
├── claude-scheduled@.service # Template service (used by runner.sh tasks)
├── claude-scheduled@backlog-triage.timer
├── claude-scheduled@sync-config.timer # Daily 02:00
├── claude-issue-poller.service/timer # Every 30 min
├── claude-pr-reviewer.service/timer
├── claude-issue-dispatcher.service
├── claude-daily-report.service/timer
├── sync-kb.service/timer # Every 2 hours (plain bash, no Claude)
~/.local/share/claude-scheduled/
└── logs/<task-name>/ # Timestamped .log files (last 30 kept)
Source files are in ~/dotfiles/claude-scheduled/ and symlinked into place.
Three Execution Patterns
1. runner.sh (Template Framework)
Used by tasks with only a prompt and settings — no custom logic needed.
claude-scheduled@<task>.timer
→ claude-scheduled@.service
→ runner.sh <task>
reads tasks/<task>/{prompt.md,settings.json,mcp.json}
invokes: claude -p <prompt> --model --effort --max-budget-usd
--allowedTools --output-format json
optionally POSTs to Discord webhook
Kill switch: touch ~/.config/claude-scheduled/disabled pauses all runner.sh tasks.
2. Custom Dispatcher Scripts
Used for tasks that need conditional dispatch, parallel agents, or complex logic.
claude-issue-poller.timer (every 30 min)
→ issue-poller.sh
→ scans repos.json for new open issues
→ calls issue-dispatcher.sh per issue
→ spawns claude -p (issue-worker agent)
claude-pr-reviewer.timer
→ pr-reviewer-dispatcher.sh
→ scans open PRs needing review
→ spawns claude -p (pr-reviewer agent) per PR
claude-daily-report.timer
→ daily-report.py
→ generates summary, posts to Discord
3. Plain Bash Scripts
Used for tasks that don't need AI — just shell commands with their own systemd units.
sync-kb.timer (every 2 hours)
→ sync-kb.service
→ tasks/sync-kb/sync-kb.sh
→ git add/commit/push .md files in claude-home
→ push triggers kb-rag webhook reindex via Gitea Actions
Logs go to ~/.local/share/claude-scheduled/logs/sync-kb/ (last 30 kept).
Active Tasks
| Task | Trigger | Type | Description |
|---|---|---|---|
backlog-triage |
Weekdays 09:15 | runner.sh | Scan Gitea issues, prioritize, suggest focus |
sync-config |
Daily 02:00 | runner.sh | Commit and push ~/.claude and ~/dotfiles to Gitea |
issue-poller |
Every 30 min | custom | Find new issues, dispatch AI workers |
issue-dispatcher |
On-demand | custom | Fix a single issue, open a PR |
pr-reviewer |
On timer | custom | Review open PRs, post formal reviews |
daily-report |
Daily | custom | Summarize activity, post to Discord |
sync-kb |
Every 2 hours | bash script | Commit and push claude-home KB changes to Gitea (triggers kb-rag reindex) |
Settings Reference (settings.json)
{
"model": "sonnet",
"effort": "medium",
"max_budget_usd": 0.75,
"allowed_tools": "Read(*) Glob(*) Grep(*)",
"working_dir": "/mnt/NV2/Development/claude-home",
"timeout_seconds": 300,
"notify_webhook": "https://discord.com/api/webhooks/..."
}
| Field | Default | Notes |
|---|---|---|
model |
sonnet |
Model alias. Use sonnet for cost efficiency. |
effort |
medium |
low, medium, high — controls reasoning depth |
max_budget_usd |
0.25 |
Hard cost ceiling per session |
allowed_tools |
Read(*) Glob(*) Grep(*) |
Principle of least privilege |
working_dir |
claude-home |
cd here before running — loads that project's CLAUDE.md |
timeout_seconds |
300 |
Hard timeout via timeout(1) |
notify_webhook |
— | Discord webhook URL for result posting (optional) |
Auth and Environment
- Uses Cal's Claude Max subscription via OAuth (no API key needed)
runner.shunsetsCLAUDECODEto allow nested sessions (running from within Claude Code)- MCP servers configured per-task via
mcp.json+--strict-mcp-config
Monitoring
# List all active timers
systemctl --user list-timers 'claude*'
# Check logs for a task
journalctl --user -u claude-scheduled@backlog-triage.service --since today
# Latest log file for runner.sh task
ls -t ~/.local/share/claude-scheduled/logs/backlog-triage/ | head -1
# Manual test run
~/.config/claude-scheduled/runner.sh backlog-triage
Session Resumption
Tasks can opt into session persistence for multi-step workflows:
{
"session_resumable": true,
"resume_last_session": true
}
When session_resumable is true, runner.sh saves the session_id to $LOG_DIR/last_session_id after each run. When resume_last_session is also true, the next run resumes that session with --resume.
Issue-poller and PR-reviewer capture session_id in logs and result JSON for manual follow-up.
See also: Agent SDK Evaluation for CLI vs SDK comparison.
Cost Safety
- Per-task
max_budget_usdcap — runner.sh detectserror_max_budget_usdand warns --allowedToolsrestricts what Claude can do (read-only tasks can't Edit/Write/Bash)timeout_secondskills hung sessions- Global kill switch:
touch ~/.config/claude-scheduled/disabled - Typical cost: $0.15–0.30 per Sonnet run with MCP tools