-
-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add claude-loop skill for recurring scheduling on any model provider #3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "name": "claude-loop", | ||
| "version": "0.1.0", | ||
| "description": "Recurring loop & reminder scheduling for Claude Code. Works on any model provider with three-tier fallback (CronCreate → Background Sleep Chain → Execute Once).", | ||
| "author": { | ||
| "name": "Tommy Nguyen", | ||
| "url": "https://github.com/tuannvm" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| # claude-loop | ||
|
|
||
| Recurring loop & reminder plugin for Claude Code. Works on **any model provider** — not just Anthropic. | ||
|
|
||
| ## Commands | ||
|
|
||
| | Command | Description | | ||
| |---------|-------------| | ||
| | `/claude-loop:loop [interval] <prompt>` | Schedule a recurring prompt | | ||
| | `/claude-loop:list` | List active loops | | ||
| | `/claude-loop:stop [id \| all]` | Stop loops | | ||
| | `/claude-loop:help` | Show usage and examples | | ||
|
|
||
| ## Features | ||
|
|
||
| - Schedule recurring prompts: `/claude-loop:loop 5m check the deploy` | ||
| - One-time reminders: `/claude-loop:loop remind me at 3pm to review PRs` | ||
| - Natural language: "check the deploy every 5 minutes" | ||
| - Intervals: `Ns`, `Nm`, `Nh`, `Nd` (default `10m`, minimum `1m`) | ||
|
|
||
| ## Three-Tier Execution | ||
|
|
||
| 1. **Tier 1: CronCreate** — Loads deferred Cron tools via ToolSearch. Full-featured, identical to built-in `/loop`. | ||
| 2. **Tier 2: Background Sleep Chain** — When Cron tools are unavailable, uses `Bash run_in_background` sleep notifications for real recurring execution. Output appears inline in the conversation. | ||
| 3. **Tier 3: Execute Once** — Last resort fallback if neither Tier 1 nor Tier 2 work. | ||
|
|
||
| ## Why This Exists | ||
|
|
||
| The built-in `/loop` command relies on `CronCreate`, which is a deferred tool that may not be discovered automatically on custom model providers. This plugin explicitly loads Cron tools via `ToolSearch` first, and provides a working fallback (Tier 2) for providers where Cron tools are genuinely unavailable. | ||
|
|
||
| ## Installation | ||
|
|
||
| ```bash | ||
| /plugin marketplace add tuannvm/plugins | ||
| /plugin install claude-loop@plugins | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| --- | ||
| description: "Show claude-loop usage and examples" | ||
| --- | ||
|
|
||
| # claude-loop Help | ||
|
|
||
| Show the user the following: | ||
|
|
||
| ## Usage | ||
|
|
||
| ``` | ||
| /claude-loop:loop [interval] <prompt or /command> | ||
| /claude-loop:loop <prompt> every <interval> | ||
| /claude-loop:list | ||
| /claude-loop:stop [job_id | all] | ||
| ``` | ||
|
|
||
| ## Examples | ||
|
|
||
| | Command | Effect | | ||
| |---------|--------| | ||
| | `/claude-loop:loop 5m check the deploy` | Check deploy every 5 minutes | | ||
| | `/claude-loop:loop 1m print hello world` | Print "hello world" every minute | | ||
| | `/claude-loop:loop check CI status every 10m` | Check CI every 10 minutes | | ||
| | `/claude-loop:loop remind me at 3pm to review PRs` | One-time reminder at 3pm | | ||
| | `/claude-loop:loop 30s /babysit-prs` | Run /babysit-prs every minute (30s rounds up) | | ||
| | `/claude-loop:list` | Show active loops | | ||
| | `/claude-loop:stop all` | Cancel all loops | | ||
| | `/claude-loop:stop a1b2c3d4` | Cancel specific loop by ID | | ||
|
|
||
| ## Intervals | ||
|
|
||
| `Ns` (seconds, rounds up to 1m minimum), `Nm` (minutes), `Nh` (hours), `Nd` (days). Default: `10m`. | ||
|
|
||
| ## How It Works | ||
|
|
||
| Works on **any model provider** via three-tier fallback: | ||
|
|
||
| 1. **Tier 1:** Loads CronCreate via ToolSearch — may be available on any provider | ||
| 2. **Tier 2:** Background sleep chain — real recurring via `Bash run_in_background` notifications | ||
| 3. **Tier 3:** Execute once — if neither tier works | ||
|
|
||
| Jobs are session-scoped and stop when Claude Code exits. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| --- | ||
| description: "List all active recurring loops and reminders" | ||
| --- | ||
|
|
||
| # List Active Loops | ||
|
|
||
| Show all active jobs scheduled in this session. | ||
|
|
||
| ## Method Selection | ||
|
|
||
| 1. Call `ToolSearch("select:CronList")` to load the tool | ||
| 2. **If loaded** → call `CronList` and display results | ||
| 3. **If unavailable** → Glob `/tmp/claude-loop-*.state`, read each file, show active ones | ||
|
|
||
| ### Stale Detection (Tier 2) | ||
|
|
||
| For each state file, check if `session_pid` matches a running process: | ||
| ```bash | ||
| kill -0 <session_pid> 2>/dev/null | ||
| ``` | ||
| If not running, the job is stale — delete the file and skip it. | ||
|
|
||
| ### Output Format | ||
|
|
||
| Show a table with: Job ID, Prompt, Interval, Created time. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| --- | ||
| description: "Run a prompt on a recurring interval or set a one-time reminder. Works on any model provider." | ||
| argument-hint: "[interval] <prompt or /command>" | ||
| --- | ||
|
|
||
| # Recurring Loop & Reminder | ||
|
|
||
| Schedule a prompt to run on a recurring interval, or set a one-time reminder, within this session. | ||
|
|
||
| ## Input | ||
|
|
||
| Parse `$ARGUMENTS` into `[interval] <prompt…>`: | ||
|
|
||
| 1. **No args / "help"** → show usage and examples, then stop | ||
| 2. **"list"** → list active jobs (see list command logic below) | ||
| 3. **"stop" / "cancel"** → cancel jobs (see stop command logic below) | ||
| 4. **Leading token** matches `^\d+[smhd]$` (e.g. `5m`, `2h`) → that's the interval; rest is prompt | ||
| 5. **Trailing "every" clause** ends with `every <N><unit>` → extract interval, strip from prompt. Only match when followed by a time expression — `check every PR` has no interval. | ||
| 6. **One-shot** — "remind me at/in", "at Xpm", "in N minutes" → schedule once (`recurring: false`) | ||
| 7. **Default** — interval is `10m`, entire input is the prompt | ||
|
|
||
| If resulting prompt is empty, show usage `/claude-loop:loop [interval] <prompt>` and stop. | ||
|
|
||
| ## Method Selection (Three-Tier) | ||
|
|
||
| **ALWAYS try Tier 1 first.** Do NOT skip to lower tiers without attempting. | ||
|
|
||
| ### Tier 1: Load CronCreate via ToolSearch | ||
|
|
||
| CronCreate is a **deferred tool** in Claude Code — it exists in the infrastructure, not the model. It may be available on any provider but must be loaded first. | ||
|
|
||
| 1. Call `ToolSearch("select:CronCreate,CronList,CronDelete")` to load the tools | ||
| 2. **If loaded** → use the Cron Method below | ||
| 3. **If ToolSearch fails or tools don't exist** → proceed to Tier 2 | ||
|
|
||
| ### Tier 2: Background Sleep Chain | ||
|
|
||
| Use when CronCreate is genuinely unavailable. | ||
|
|
||
| **How it works:** Run `sleep <seconds>` via Bash with `run_in_background: true`. When the sleep completes, Claude receives an automatic notification. Claude then executes the prompt inline and starts the next sleep cycle. | ||
|
|
||
| **Detection:** If the first Bash `run_in_background` call errors, fall to Tier 3. | ||
|
|
||
| ### Tier 3: Execute Once (last resort) | ||
|
|
||
| Only if both Tier 1 and Tier 2 fail: | ||
| 1. Execute the prompt once immediately in the conversation | ||
| 2. Explain the limitation and offer alternatives (system cron, `watch`, separate terminal) | ||
|
|
||
| --- | ||
|
|
||
| ## Cron Method (Tier 1) | ||
|
|
||
| ### Interval-to-Cron Conversion | ||
|
|
||
| | Interval | Cron | Notes | | ||
| |----------|------|-------| | ||
| | `Ns` | `*/ceil(N/60) * * * *` | Round up to nearest minute, warn user | | ||
| | `1m` | `* * * * *` | | | ||
| | `Nm` (2-59) | `*/N * * * *` | | | ||
| | `Nm` (60+) | `0 */H * * *` where H=round(N/60) | Must divide 24 evenly; tell user what was picked | | ||
| | `Nh` (1-23) | `M */N * * *` | M ∈ 1-59, avoid 0 and 30 | | ||
| | `Nh` (24+) | Convert to days | e.g., `48h` → `2d` | | ||
| | `Nd` (1-31) | `M H */N * *` | M ∈ 1-59, H ∈ 7-21 | | ||
| | `Nd` (32+) | Reject | Tell user max is 31d | | ||
|
|
||
| **Anti-spike:** For hourly+, never use minute 0 or 30 unless user requests exact time. | ||
|
|
||
| ### How CronCreate Works | ||
|
|
||
| The `prompt` parameter is a **Claude prompt**, not a bash command. When the job fires, the prompt is fed back into the current Claude Code session on the next idle turn. Claude then interprets and executes it inline — output appears directly in the conversation. | ||
|
|
||
| ### Execution | ||
|
|
||
| 1. Parse interval and prompt | ||
| 2. Convert to cron expression | ||
| 3. `CronCreate`: `cron`, `prompt`, `recurring: true` (or `false` for one-shot) | ||
| 4. Confirm: prompt, interval, cron, job ID, 3-day expiry note | ||
|
|
||
| ### One-Time Reminders (Cron) | ||
|
|
||
| Parse target time → pin to cron fields → `CronCreate` with `recurring: false`. | ||
|
|
||
| ### Managing Jobs (Cron) | ||
|
|
||
| - **List:** `CronList` | ||
| - **Cancel by ID:** `CronDelete` (8-char ID) | ||
| - **Cancel all:** `CronList` → `CronDelete` each | ||
|
|
||
| Max 50 tasks per session. | ||
|
|
||
| ### Runtime Behavior | ||
|
|
||
| - **Session-scoped** — gone when Claude Code exits | ||
| - **Fires between turns** — only when idle, not mid-response | ||
| - **No catch-up** — missed fires execute once when idle | ||
| - **Local timezone** — not UTC | ||
| - **3-day expiry** — recurring auto-expire | ||
| - **Jitter** — recurring: up to 10% late (max 15 min); one-shot on :00/:30: up to 90s early | ||
|
|
||
| ### Cron Reference | ||
|
|
||
| Standard 5-field: `minute hour day-of-month month day-of-week`. Supports `*`, values, `*/N` steps, ranges, lists. Day-of-week: 0 or 7 = Sunday. | ||
|
|
||
| --- | ||
|
|
||
| ## Background Sleep Chain (Tier 2) | ||
|
|
||
| Use when CronCreate is genuinely unavailable after ToolSearch attempt. | ||
|
|
||
| ### State File | ||
|
|
||
| Each job creates `/tmp/claude-loop-<id>.state` (JSON): | ||
| ```json | ||
| {"id": "<8-char-hex>", "prompt": "<prompt>", "interval_sec": 60, "recurring": true, "created": "<ISO>", "session_pid": <PID>} | ||
| ``` | ||
|
|
||
| - Generate `id`: `openssl rand -hex 4` | ||
| - **Escape prompt** for JSON: replace `\` → `\\`, `"` → `\"`, newlines → `\n` | ||
| - `session_pid`: `echo $PPID` (Claude Code's PID — used for staleness detection) | ||
| - `recurring`: `false` for one-shot reminders, `true` for loops | ||
|
Comment on lines
+113
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a private per-session state directory instead of shared Storing executable prompts in predictable 🔒 Suggested doc fix-Each job creates `/tmp/claude-loop-<id>.state` (JSON):
+Create a private session directory first, e.g. `${TMPDIR:-/tmp}/claude-loop.<session-id>/` with mode `0700`.
+Each job creates `<session_dir>/<id>.state` (JSON) with mode `0600`:-- Generate `id`: `openssl rand -hex 4`
+- Generate `id`: `openssl rand -hex 4`
+- Create state files with restrictive permissions and never glob outside the current session directory🤖 Prompt for AI Agents |
||
|
|
||
| ### One-Time Reminders (Tier 2) | ||
|
|
||
| For absolute times ("at 3pm", "remind me at 14:30"): compute seconds until target time and use as `interval_sec` with `recurring: false`. | ||
| - **macOS:** `$(( $(date -j -f '%H:%M' '15:00' '+%s') - $(date '+%s') ))` | ||
| - **Linux:** `$(( $(date -d '15:00' '+%s') - $(date '+%s') ))` | ||
| - If result is negative, target is tomorrow — add 86400. | ||
|
|
||
| ### Starting a Loop | ||
|
|
||
| 1. Parse interval and prompt (same rules as Cron method) | ||
| 2. Convert interval to seconds (`1m`→60, `5m`→300, `1h`→3600). Enforce minimum 60s. | ||
| 3. Create the state file | ||
| 4. Run Bash: `sleep <interval_sec> && echo 'CLAUDE_LOOP_FIRE <id>'` with `run_in_background: true` | ||
| - The echo embeds the job ID so the notification is self-describing | ||
| 5. Confirm to user: prompt, interval, job ID | ||
|
|
||
| ### On Sleep Completion (notification received) | ||
|
|
||
| When you see a background task notification containing `CLAUDE_LOOP_FIRE <id>`: | ||
|
|
||
| **CRITICAL behavioral rules:** | ||
| - **MUST output ONLY the result of executing the stored prompt.** Nothing else. | ||
| - **MUST NOT output any meta-commentary**, reflection, or speculation about the loop, the user's intent, or what might happen next. NEVER emit phrases like "Loop fired", "reading state file", "next cycle started", "the loop is working", "the user might want to...", or similar. | ||
| - **Treat each fire as atomic and isolated.** Do not reflect on previous fires or accumulated loop history. | ||
| - If the prompt produces no visible output, output nothing at all. | ||
|
|
||
| **Steps:** | ||
|
|
||
| 1. Read `/tmp/claude-loop-<id>.state` (no commentary) | ||
| 2. If file exists: | ||
| - **Execute the prompt** — user sees only the prompt's output, nothing else | ||
| - If `recurring: true`: re-arm with `sleep <interval_sec> && echo 'CLAUDE_LOOP_FIRE <id>'` (`run_in_background: true`) — no confirmation text | ||
| - If `recurring: false`: delete the state file — no confirmation text | ||
| 3. If file missing → do nothing (loop was cancelled) | ||
|
|
||
| **After executing the prompt and re-arming, STOP. Do not add any trailing commentary or thoughts.** | ||
|
|
||
| ### Managing Jobs (Sleep Chain) | ||
|
|
||
| - **List:** Glob `/tmp/claude-loop-*.state`, read each, show active ones. Filter stale: if `session_pid` doesn't match a running process (`kill -0 <pid> 2>/dev/null`), skip and delete. | ||
| - **Stop by ID:** Delete `/tmp/claude-loop-<id>.state`. The next sleep notification will find no file and stop. | ||
| - **Stop by prompt substring:** Glob all state files, match prompt substring, delete. If multiple match, list and ask user to clarify. | ||
| - **Stop all:** Glob and delete all `/tmp/claude-loop-*.state`. | ||
|
|
||
| Max 20 concurrent jobs per session (check count before creating). | ||
|
|
||
| ### Runtime Behavior | ||
|
|
||
| - **Session-scoped** — state files go stale when Claude exits; detected via `session_pid` | ||
| - **Fires between turns** — notification arrives when idle | ||
| - **Output inline** — Claude executes the prompt in the conversation, identical to Cron behavior | ||
| - **Minimum interval** — 1 minute (60 seconds) | ||
| - **Self-describing notifications** — `CLAUDE_LOOP_FIRE <id>` in background output identifies the job | ||
| - **No 3-day expiry** — runs until stopped or session ends | ||
| - **On prompt error** — log the error inline, continue the loop (don't stop on transient failures) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| --- | ||
| description: "Stop active loops by ID, prompt substring, or all" | ||
| argument-hint: "[job_id | prompt_substring | all]" | ||
| --- | ||
|
|
||
| # Stop Loops | ||
|
|
||
| Cancel active jobs. Parse `$ARGUMENTS`: | ||
|
|
||
| - **No args** → list active jobs and ask which to stop | ||
| - **"all"** → stop all active jobs | ||
| - **Otherwise** → match by: exact ID → ID prefix → prompt substring. If multiple match, list them and ask user to clarify. | ||
|
|
||
| ## Method Selection | ||
|
|
||
| 1. Call `ToolSearch("select:CronList,CronDelete")` to load the tools | ||
| 2. **If loaded** → use `CronList` to find jobs, `CronDelete` to cancel | ||
| 3. **If unavailable** → Glob `/tmp/claude-loop-*.state`, match against argument, delete matching files | ||
|
|
||
| Confirm what was stopped: job ID, prompt, interval. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In standard 5-field cron, does0 */2 * * *represent every 90 minutes, and does7 */5 * * *maintain exact 5-hour spacing across day boundaries? Please include example next-run timestamps.💡 Result:
No.
0 */2 * * *This means: at minute 0 of every 2nd hour (hours 0,2,4,…,22)—i.e., every 2 hours on the clock, not “every 90 minutes.” (cronviz.com)
Example (assume “now” is 2026-03-08 13:26 local time):
7 */5 * * *This means: at minute 7 of every 5th hour (hours 0,5,10,15,20) each day. It does not preserve exact 5-hour spacing across the midnight boundary because the schedule “wraps” to the next day’s matching hours (cron is a pattern matcher, not an interval timer). (en.wikipedia.org)
Example (assume “now” is 2026-03-08 13:26):
Citations:
Don't silently translate unsupported intervals into a different cadence.
In standard 5-field cron,
0 */2 * * *runs every 2 hours at minute 0 (hours 0,2,4,…,22), not every 90 minutes. Similarly,7 */5 * * *does not preserve exact 5-hour spacing across day boundaries; the gap from 20:07 to 00:07 the next day is 4 hours, not 5. These mappings drift and create uneven gaps. For intervals that cannot be represented exactly in 5-field cron, fall back to Tier 2 or reject them instead of rounding to a different schedule.🛠️ Suggested doc fix
🤖 Prompt for AI Agents