--- title: "Scheduled Tasks Overview" description: "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." type: context domain: scheduled-tasks tags: [claude-code, systemd, timers, automation, headless, runner, issue-poller, pr-reviewer, backlog-triage, sync-kb, sync-config] --- # 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// # 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@.timer → claude-scheduled@.service → runner.sh reads tasks//{prompt.md,settings.json,mcp.json} invokes: claude -p --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`) ```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.sh` unsets `CLAUDECODE` to allow nested sessions (running from within Claude Code) - MCP servers configured per-task via `mcp.json` + `--strict-mcp-config` ## Monitoring ```bash # 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 ``` ## Cost Safety - Per-task `max_budget_usd` cap — runner.sh detects `error_max_budget_usd` and warns - `--allowedTools` restricts what Claude can do (read-only tasks can't Edit/Write/Bash) - `timeout_seconds` kills hung sessions - Global kill switch: `touch ~/.config/claude-scheduled/disabled` - Typical cost: $0.15–0.30 per Sonnet run with MCP tools