claude-plugins/plugins/create-scheduled-task/skills/create/SKILL.md
Cal Corum e02eb28e98 refactor: rename skill dirs to verb-based names to reduce autocomplete redundancy
Plugin:skill pairs now read as noun:verb commands instead of repeating
the plugin name. Also added concise descriptions to all SKILL.md frontmatter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:57:19 -05:00

250 lines
7.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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/
└── <task-name>/
├── 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@<task-name>.timer # One per task
```
## Creating a New Task
### Step 1: Create the task directory
```bash
mkdir -p ~/.config/claude-scheduled/tasks/<task-name>
mkdir -p ~/.local/share/claude-scheduled/logs/<task-name>
```
### 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": "<space-separated tool list>",
"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": "<your-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": "<your-n8n-api-key>"
}
}
}
```
Wrap in `{"mcpServers": { ... }}` structure.
### Step 5: Create the systemd timer
Create `~/.config/systemd/user/claude-scheduled@<task-name>.timer`:
```ini
[Unit]
Description=Claude Scheduled Task Timer: <task-name>
[Timer]
OnCalendar=<schedule>
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@<task-name>.timer
```
## Managing Tasks
### List all scheduled tasks
```bash
systemctl --user list-timers 'claude-scheduled*'
```
### Manual test run
```bash
~/.config/claude-scheduled/runner.sh <task-name>
```
### Check logs
```bash
# Latest log
ls -t ~/.local/share/claude-scheduled/logs/<task-name>/ | head -1 | xargs -I{} cat ~/.local/share/claude-scheduled/logs/<task-name>/{}
# Via journalctl (if triggered by systemd)
journalctl --user -u claude-scheduled@<task-name>.service --since today
```
### Disable a single task
```bash
systemctl --user disable --now claude-scheduled@<task-name>.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/<task-name>/
```
## 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/<task>/`
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.150.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/`