--- name: create description: Create, manage, or debug headless Claude scheduled tasks on systemd timers --- # Create Scheduled Task ## When to Activate This Skill - "Create a scheduled task for X" - "Schedule Claude to do X every day" - "Add a new automated task" - "Debug a scheduled task" - "List scheduled tasks" - "Manage scheduled tasks" ## System Overview Headless Claude Code sessions triggered by systemd timers. Each task has its own prompt, MCP config, settings, and timer. ``` ~/.config/claude-scheduled/ ├── runner.sh # Universal task runner (DO NOT MODIFY) ├── disabled # Touch this file to globally disable all tasks ├── logs -> ~/.local/share/... # Symlink to log directory └── tasks/ └── / ├── prompt.md # What Claude should do ├── settings.json # Model, budget, tools, working dir └── mcp.json # MCP servers this task needs (optional) ~/.config/systemd/user/ ├── claude-scheduled@.service # Template unit (DO NOT MODIFY) └── claude-scheduled@.timer # One per task ``` ## Creating a New Task ### Step 1: Create the task directory ```bash mkdir -p ~/.config/claude-scheduled/tasks/ mkdir -p ~/.local/share/claude-scheduled/logs/ ``` ### Step 2: Write the prompt (`prompt.md`) Write a clear, structured prompt that tells Claude exactly what to do. Include: - Specific instructions (repos to check, files to read, etc.) - Desired output format (structured text or JSON) - Any cognitive-memory operations (recall context, store results) **Guidelines:** - Be explicit — headless Claude has no user to ask for clarification - Specify output format so results are parseable in logs - Keep prompts focused on a single concern ### Step 3: Write settings (`settings.json`) ```json { "model": "sonnet", "effort": "medium", "max_budget_usd": 0.75, "allowed_tools": "", "graph": "default", "working_dir": "/path/to/your/project", "timeout_seconds": 300 } ``` **Settings reference:** | Field | Default | Description | |-------|---------|-------------| | `model` | `sonnet` | Model alias or full ID. Use `sonnet` for cost efficiency. | | `effort` | `medium` | `low`, `medium`, or `high`. Controls reasoning depth. | | `max_budget_usd` | `0.25` | Per-session cost ceiling. Typical triage run: ~$0.20. | | `allowed_tools` | `Read(*) Glob(*) Grep(*)` | Space-separated tool allowlist. Principle of least privilege. | | `graph` | `default` | Cognitive-memory graph for storing results. | | `working_dir` | (your project root) | `cd` here before running. Loads that project's CLAUDE.md. | | `timeout_seconds` | `300` | Hard timeout. 300s (5 min) is usually sufficient. | **Common tool allowlists by task type:** Read-only triage (Gitea + memory): ``` mcp__gitea-mcp__list_repo_issues mcp__gitea-mcp__get_issue_by_index mcp__gitea-mcp__list_repo_labels mcp__gitea-mcp__list_repo_pull_requests mcp__cognitive-memory__memory_recall mcp__cognitive-memory__memory_search mcp__cognitive-memory__memory_store mcp__cognitive-memory__memory_episode ``` Code analysis (read-only): ``` Read(*) Glob(*) Grep(*) ``` Memory maintenance: ``` mcp__cognitive-memory__memory_recall mcp__cognitive-memory__memory_search mcp__cognitive-memory__memory_store mcp__cognitive-memory__memory_relate mcp__cognitive-memory__memory_reflect mcp__cognitive-memory__memory_episode ``` ### Step 4: Write MCP config (`mcp.json`) — if needed Only include MCP servers the task actually needs. Use `--strict-mcp-config` (runner does this automatically when mcp.json exists). **Available MCP server configs to copy from:** Gitea: ```json { "gitea-mcp": { "type": "stdio", "command": "gitea-mcp", "args": ["-t", "stdio", "-host", "https://your-gitea-instance.com"], "env": { "GITEA_ACCESS_TOKEN": "" } } } ``` Cognitive Memory: ```json { "cognitive-memory": { "command": "python3", "type": "stdio", "args": ["/path/to/cognitive-memory/mcp_server.py"], "env": {} } } ``` n8n: ```json { "n8n-mcp": { "command": "npx", "type": "stdio", "args": ["n8n-mcp"], "env": { "MCP_MODE": "stdio", "N8N_API_URL": "http://your-n8n-host:5678", "N8N_API_KEY": "" } } } ``` Wrap in `{"mcpServers": { ... }}` structure. ### Step 5: Create the systemd timer Create `~/.config/systemd/user/claude-scheduled@.timer`: ```ini [Unit] Description=Claude Scheduled Task Timer: [Timer] OnCalendar= Persistent=true [Install] WantedBy=timers.target ``` **Common OnCalendar expressions:** | Schedule | Expression | |----------|------------| | Daily at 9am | `*-*-* 09:00:00` | | Every 6 hours | `*-*-* 00/6:00:00` | | Weekdays at 8am | `Mon..Fri *-*-* 08:00:00` | | Weekly Sunday 3am | `Sun *-*-* 03:00:00` | | Monthly 1st at midnight | `*-*-01 00:00:00` | `Persistent=true` means if the machine was off during a scheduled run, it catches up on next boot. ### Step 6: Enable the timer ```bash systemctl --user daemon-reload systemctl --user enable --now claude-scheduled@.timer ``` ## Managing Tasks ### List all scheduled tasks ```bash systemctl --user list-timers 'claude-scheduled*' ``` ### Manual test run ```bash ~/.config/claude-scheduled/runner.sh ``` ### Check logs ```bash # Latest log ls -t ~/.local/share/claude-scheduled/logs// | head -1 | xargs -I{} cat ~/.local/share/claude-scheduled/logs//{} # Via journalctl (if triggered by systemd) journalctl --user -u claude-scheduled@.service --since today ``` ### Disable a single task ```bash systemctl --user disable --now claude-scheduled@.timer ``` ### Disable ALL tasks (kill switch) ```bash touch ~/.config/claude-scheduled/disabled # To re-enable: rm ~/.config/claude-scheduled/disabled ``` ### Check task run history ```bash ls -lt ~/.local/share/claude-scheduled/logs// ``` ## How the Runner Works `runner.sh` is the universal executor. For each task it: 1. Reads `settings.json` for model, budget, tools, working dir 2. Reads `prompt.md` as the Claude prompt 3. Invokes `claude -p` with `--strict-mcp-config`, `--allowedTools`, `--no-session-persistence`, `--output-format json` 4. Unsets `CLAUDECODE` env var to allow nested sessions 5. Logs full output to `~/.local/share/claude-scheduled/logs//` 6. Stores a summary to cognitive-memory as a workflow + episode 7. Rotates logs (keeps last 30 per task) **The runner does NOT need modification to add new tasks** — just add files under `tasks/` and a timer. ## Key Constraints - **Read-only by default**: Tasks should use `--allowedTools` to restrict to only what they need. No Bash, no Edit unless explicitly required. - **Cost ceiling**: `max_budget_usd` is a hard limit per session. Typical Sonnet run with MCP tools: $0.15–0.30. - **Auth**: Uses Claude Max subscription via OAuth, or set `ANTHROPIC_API_KEY` for API key auth. - **Nested sessions**: The runner unsets `CLAUDECODE` so it works from within a Claude session or from systemd. - **Log retention**: 30 logs per task, oldest auto-deleted. ## Reference Files - Runner: `~/.config/claude-scheduled/runner.sh` - Template service: `~/.config/systemd/user/claude-scheduled@.service` - Example task: `~/.config/claude-scheduled/tasks/backlog-triage/`