Skip to content

feat: add claude-loop skill for recurring scheduling on any model provider#3

Open
tuannvm wants to merge 2 commits intomainfrom
feat/claude-loop-skill
Open

feat: add claude-loop skill for recurring scheduling on any model provider#3
tuannvm wants to merge 2 commits intomainfrom
feat/claude-loop-skill

Conversation

@tuannvm
Copy link
Owner

@tuannvm tuannvm commented Mar 8, 2026

Summary

Adds the claude-loop skill — a recurring loop & reminder scheduler that works on any model provider, not just Anthropic.

Three-Tier Execution

Tier Method When
1 CronCreate via ToolSearch Always tried first — loads deferred Cron tools that may be available on any provider
2 Background Sleep Chain When Cron tools unavailable — uses Bash run_in_background sleep notifications for real recurring execution
3 Execute Once Last resort if both Tier 1 and 2 fail

Features

  • Recurring prompts: /claude-loop 5m check the deploy
  • One-time reminders: /claude-loop remind me at 3pm to review PRs
  • Job management: /claude-loop list, /claude-loop stop <id>
  • Natural language parsing
  • Native-feel output (no scheduler meta-commentary)
  • Cross-platform (macOS + Linux)

Why

The built-in /loop relies on CronCreate which may not auto-discover on custom model providers. This skill explicitly loads Cron tools via ToolSearch first, and provides a working Tier 2 fallback using background sleep chains.

Files

  • skills/claude-loop/SKILL.md — Skill definition (190 lines)
  • skills/claude-loop/README.md — Documentation
  • .claude-plugin/marketplace.json — Added marketplace entry

Review

Passed 3 rounds of Codex-5.3-High review covering correctness, edge cases, behavioral drift prevention, and cross-platform compatibility.

Summary by CodeRabbit

  • New Features
    • Added claude-loop plugin for scheduling recurring loops and one-time reminders in Claude Code
    • Supports interval-based scheduling with automatic three-tier fallback execution
    • New commands: start loops with custom intervals, list active jobs, and stop reminders by ID or prompt
    • Works across all supported model providers

…vider

Three-tier execution strategy:
- Tier 1: Load CronCreate via ToolSearch (works across providers)
- Tier 2: Background sleep chain fallback for real recurring execution
- Tier 3: Execute-once last resort

Features: interval parsing, one-shot reminders, job management (list/stop),
native-feel output suppression, cross-platform time handling.
@coderabbitai
Copy link

coderabbitai bot commented Mar 8, 2026

Walkthrough

A new "claude-loop" plugin is introduced as a recurring loop and reminder solution for Claude Code. The plugin includes manifest declarations, comprehensive documentation covering three-tier fallback execution (CronCreate, background sleep chain, execute-once), and command specifications for loop control, listing, help, and job termination.

Changes

Cohort / File(s) Summary
Plugin Manifest & Registry
.claude-plugin/marketplace.json, plugins/claude-loop/.claude-plugin/plugin.json
Registered new plugin entry in marketplace and created plugin manifest with metadata, version 0.1.0, and description of three-tier fallback scheduling mechanism.
Core Documentation
plugins/claude-loop/README.md
Documented plugin purpose, supported models, installation steps, and overview of execution tiers.
Command Documentation
plugins/claude-loop/commands/loop.md, plugins/claude-loop/commands/help.md, plugins/claude-loop/commands/list.md, plugins/claude-loop/commands/stop.md
Defined specifications for loop management commands including input parsing rules, method selection workflows, cron-to-interval conversion, state management, job control semantics, and user-facing behaviors.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A loop so clever, a reminder so bright,
With three fallback tiers, it works day and night—
CronCreate leads on, then sleep chains unfold,
A plugin of promises, recurring and bold! 🔁

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a claude-loop skill for recurring scheduling that works across any model provider, which aligns with the PR objectives and all file changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/claude-loop-skill

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Add .claude-plugin/plugin.json with metadata
- Split into commands: loop (main), list, stop, help
- Remove skills/claude-loop/ directory
- Update marketplace.json source path to plugins/claude-loop
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@skills/claude-loop/README.md`:
- Line 24: Update the README line that lists supported interval suffixes ("Ns",
"Nm", "Nh", "Nd") to accurately reflect runtime behavior: state that
second-based intervals ("Ns") are coerced to a minimum of 1 minute and any
cron-backed seconds are rounded up to whole minutes (so "/claude-loop 30s" will
run at 1m intervals), or alternatively remove "Ns" from the advertised formats;
mention the default "10m" and the 1 minute minimum explicitly to avoid
confusion.

In `@skills/claude-loop/SKILL.md`:
- Around line 161-165: The doc currently only re-arms recurring Tier 2 jobs
after successful prompt execution; update the flow for reading
/tmp/claude-loop-<id>.state so that for recurring: true the re-arm (spawn sleep
<interval_sec> && echo 'CLAUDE_LOOP_FIRE <id>' with run_in_background: true) is
scheduled regardless of whether the prompt execution succeeds or fails — either
schedule the sleep before executing the prompt or explicitly schedule it in the
prompt failure path; keep the non-recurring: false behavior of deleting the
state file unchanged.
- Around line 12-17: The fenced code block showing the /claude-loop command
syntax lacks a language tag (MD040); update the triple-backtick fence around the
four-line block (the lines starting with "/claude-loop [interval] <prompt or
/command>" and including "/claude-loop stop [job_id | all]") to include the
"text" language tag (i.e., change ``` to ```text) so the static analyzer no
longer flags it.
- Around line 172-178: The list/stop handlers currently operate on every
/tmp/claude-loop-*.state file; change them to only consider files whose JSON
session_pid matches the current process's session_pid (or explicitly provided
current session id) before showing or deleting; update the "List" path to filter
out states whose session_pid != current session, make "Stop by ID" verify the
target file's session_pid matches before deleting, make "Stop by prompt
substring" restrict matches to files with the current session_pid (and only
prompt user if multiple within the same session match), and make "Stop all"
delete only files with session_pid equal to the current session; ensure you
still skip and delete clearly stale entries (no running pid) but do not touch
files belonging to other sessions.
- Around line 61-69: The doc currently describes silently converting
non-cron-alignable intervals (e.g., "90m" → "2h" and the "M */N * * *" pattern
for Nh) which changes user intent; update the SKILL.md rules for the Ns/Nm/Nh
cases to instead reject or fall back to Tier 2 when exact cron spacing cannot be
represented, and add text that explains the fallback behavior (or prompts the
user to choose a cron-aligned interval), explicitly call out that "M */N * * *"
resets at midnight and so does not preserve true N-hour spacing, and keep the
existing guidance to "tell user what was picked" only when an equivalent
cron-exact value is chosen. Ensure the table entries for Ns/Nm (>=60)/Nh (>=24)
and the "M */N * * *" row clearly state rejection/fallback policy rather than
silent conversion.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 42b69f89-48c6-4b02-bf53-56ee775ae3a6

📥 Commits

Reviewing files that changed from the base of the PR and between a03d4d5 and 809b891.

📒 Files selected for processing (3)
  • .claude-plugin/marketplace.json
  • skills/claude-loop/README.md
  • skills/claude-loop/SKILL.md


## Intervals

Supports `Ns`, `Nm`, `Nh`, `Nd`. Defaults to `10m` if no interval specified. Minimum interval: 1 minute.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify how second-based intervals behave.

Line 24 says Ns is supported, but the skill spec later enforces a 60-second minimum and rounds cron-backed seconds up to whole minutes. A request like /claude-loop 30s ... will not run every 30 seconds as written here. Please document that Ns is coerced/rounded to at least 1m, or remove Ns from the advertised formats.

Suggested doc tweak
-Supports `Ns`, `Nm`, `Nh`, `Nd`. Defaults to `10m` if no interval specified. Minimum interval: 1 minute.
+Supports `Nm`, `Nh`, `Nd`, plus `Ns` values that are rounded up to at least `1m`. Defaults to `10m` if no interval specified. Minimum effective interval: 1 minute.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Supports `Ns`, `Nm`, `Nh`, `Nd`. Defaults to `10m` if no interval specified. Minimum interval: 1 minute.
Supports `Nm`, `Nh`, `Nd`, plus `Ns` values that are rounded up to at least `1m`. Defaults to `10m` if no interval specified. Minimum effective interval: 1 minute.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/claude-loop/README.md` at line 24, Update the README line that lists
supported interval suffixes ("Ns", "Nm", "Nh", "Nd") to accurately reflect
runtime behavior: state that second-based intervals ("Ns") are coerced to a
minimum of 1 minute and any cron-backed seconds are rounded up to whole minutes
(so "/claude-loop 30s" will run at 1m intervals), or alternatively remove "Ns"
from the advertised formats; mention the default "10m" and the 1 minute minimum
explicitly to avoid confusion.

Comment on lines +12 to +17
```
/claude-loop [interval] <prompt or /command>
/claude-loop <prompt> every <interval>
/claude-loop list
/claude-loop stop [job_id | all]
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a language tag to the syntax fence.

Static analysis is already flagging Lines 12-17 with MD040. text fits best here because this block is command syntax, not executable shell.

Suggested fix
-```
+```text
 /claude-loop [interval] <prompt or /command>
 /claude-loop <prompt> every <interval>
 /claude-loop list
 /claude-loop stop [job_id | all]
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.21.0)</summary>

[warning] 12-12: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @skills/claude-loop/SKILL.md around lines 12 - 17, The fenced code block
showing the /claude-loop command syntax lacks a language tag (MD040); update the
triple-backtick fence around the four-line block (the lines starting with
"/claude-loop [interval] <prompt or /command>" and including "/claude-loop stop
[job_id | all]") to include the "text" language tag (i.e., change ``` to

Comment on lines +61 to +69
| 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 |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid silently changing intervals that cron cannot represent exactly.

Line 66 turns values like 90m into 2h, and Line 67’s M */N * * * also breaks true N-hour spacing for values like 5h because the cadence resets at midnight. That changes the user’s requested schedule instead of preserving it. These cases should fall back to Tier 2 or be rejected with a prompt to choose a cron-aligned interval.

Suggested doc tweak
-| `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 |
+| `Nm`/`Nh` exactly representable by a single 5-field cron | Use the matching cron expression | Preserve the requested cadence |
+| `Nm`/`Nh` not exactly representable by a single 5-field cron (e.g. `90m`, `5h`) | Fall back to Tier 2 or ask the user to choose a cron-aligned interval | Do not silently round/stretch the schedule |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| 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 |
| Interval | Cron | Notes |
|----------|------|-------|
| `Ns` | `*/ceil(N/60) * * * *` | Round up to nearest minute, warn user |
| `1m` | `* * * * *` | |
| `Nm` (2-59) | `*/N * * * *` | |
| `Nm`/`Nh` exactly representable by a single 5-field cron | Use the matching cron expression | Preserve the requested cadence |
| `Nm`/`Nh` not exactly representable by a single 5-field cron (e.g. `90m`, `5h`) | Fall back to Tier 2 or ask the user to choose a cron-aligned interval | Do not silently round/stretch the schedule |
| `Nh` (24+) | Convert to days | e.g., `48h``2d` |
| `Nd` (1-31) | `M H */N * *` | M ∈ 1-59, H ∈ 7-21 |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/claude-loop/SKILL.md` around lines 61 - 69, The doc currently
describes silently converting non-cron-alignable intervals (e.g., "90m" → "2h"
and the "M */N * * *" pattern for Nh) which changes user intent; update the
SKILL.md rules for the Ns/Nm/Nh cases to instead reject or fall back to Tier 2
when exact cron spacing cannot be represented, and add text that explains the
fallback behavior (or prompts the user to choose a cron-aligned interval),
explicitly call out that "M */N * * *" resets at midnight and so does not
preserve true N-hour spacing, and keep the existing guidance to "tell user what
was picked" only when an equivalent cron-exact value is chosen. Ensure the table
entries for Ns/Nm (>=60)/Nh (>=24) and the "M */N * * *" row clearly state
rejection/fallback policy rather than silent conversion.

Comment on lines +161 to +165
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Re-arm recurring Tier 2 jobs even when the prompt fails.

The current steps only schedule the next sleep after a successful prompt execution. If the stored prompt errors, Line 164 is skipped, so the loop dies even though Line 190 says transient failures should continue. Re-arm before execution or explicitly re-arm in the failure path as well.

Suggested doc tweak
-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
+2. If file exists:
+   - If `recurring: true`, guarantee the next sleep is re-armed even if prompt execution fails
+   - **Execute the prompt** — user sees only the prompt's output, nothing else
+   - If `recurring: false`: delete the state file — no confirmation text

Also applies to: 190-190

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/claude-loop/SKILL.md` around lines 161 - 165, The doc currently only
re-arms recurring Tier 2 jobs after successful prompt execution; update the flow
for reading /tmp/claude-loop-<id>.state so that for recurring: true the re-arm
(spawn sleep <interval_sec> && echo 'CLAUDE_LOOP_FIRE <id>' with
run_in_background: true) is scheduled regardless of whether the prompt execution
succeeds or fails — either schedule the sleep before executing the prompt or
explicitly schedule it in the prompt failure path; keep the non-recurring: false
behavior of deleting the state file unchanged.

Comment on lines +172 to +178
**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, find those whose `prompt` contains the substring, delete matching files. If multiple match, list them and ask user to clarify.

**Stop all:** Glob and delete all `/tmp/claude-loop-*.state`.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep sleep-chain job management scoped to the current session.

These commands glob every /tmp/claude-loop-*.state file and only remove obviously stale ones. On a host with multiple active Claude sessions, list, stop <substring>, and especially stop all can inspect or delete another session’s jobs, which breaks the session-scoped contract described elsewhere in this file. Filter by the current session_pid before showing or deleting jobs.

Suggested doc tweak
-**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.
+**List:** Glob `/tmp/claude-loop-*.state`, read each, and only show jobs whose `session_pid` matches the current session. Filter stale jobs as cleanup.

 **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, find those whose `prompt` contains the substring, delete matching files. If multiple match, list them and ask user to clarify.
+**Stop by prompt substring:** Search only the current session's state files. If multiple match, list them and ask user to clarify.

-**Stop all:** Glob and delete all `/tmp/claude-loop-*.state`.
+**Stop all:** Delete only the current session's state files.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**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, find those whose `prompt` contains the substring, delete matching files. If multiple match, list them and ask user to clarify.
**Stop all:** Glob and delete all `/tmp/claude-loop-*.state`.
**List:** Glob `/tmp/claude-loop-*.state`, read each, and only show jobs whose `session_pid` matches the current session. Filter stale jobs as cleanup.
**Stop by ID:** Delete `/tmp/claude-loop-<id>.state`. The next sleep notification will find no file and stop.
**Stop by prompt substring:** Search only the current session's state files. If multiple match, list them and ask user to clarify.
**Stop all:** Delete only the current session's state files.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@skills/claude-loop/SKILL.md` around lines 172 - 178, The list/stop handlers
currently operate on every /tmp/claude-loop-*.state file; change them to only
consider files whose JSON session_pid matches the current process's session_pid
(or explicitly provided current session id) before showing or deleting; update
the "List" path to filter out states whose session_pid != current session, make
"Stop by ID" verify the target file's session_pid matches before deleting, make
"Stop by prompt substring" restrict matches to files with the current
session_pid (and only prompt user if multiple within the same session match),
and make "Stop all" delete only files with session_pid equal to the current
session; ensure you still skip and delete clearly stale entries (no running pid)
but do not touch files belonging to other sessions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@plugins/claude-loop/commands/loop.md`:
- Around line 113-121: The current design writes job state to predictable global
files like /tmp/claude-loop-<id>.state which allows other local processes to
enumerate or tamper with prompts; change to create a private per-session temp
directory with restrictive permissions (owner-only) and store state files inside
it (e.g., session-specific temp dir created at session start), update the logic
that generates state files (the code that writes `/tmp/claude-loop-<id>.state`
and sets "session_pid") to write into that session directory, and update the
list/stop and the fire path to only read/inspect files in that per-session
directory so only the owning session can discover, delete, or execute its jobs.
Ensure the session dir is created atomically with secure perms and cleaned up
when the session ends.
- Around line 58-64: The docs currently show automatic translations for
unsupported intervals (see table entries like `Ns`, `Nm (60+)`, `Nh (24+)`,
`Nd`) which silently round or convert intervals into different cron cadences;
update the text and the table to stop suggesting these silent translations and
instead state that intervals not representable exactly in 5-field cron should
either fall back to Tier 2 scheduling or be rejected with an explicit error
message to the user; specifically remove or mark as unsupported the mappings for
`Nm (60+)` → `0 */H * * *`, `Nh (24+)` → Convert to days, and any rounding
behavior under `Ns`/`Nm` that implies drift, and add a short note that
exact-interval requirements must use Tier 2 or be declined.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3428b44a-c06d-4e24-aa15-d822e4229fa3

📥 Commits

Reviewing files that changed from the base of the PR and between 809b891 and 9213cd1.

📒 Files selected for processing (7)
  • .claude-plugin/marketplace.json
  • plugins/claude-loop/.claude-plugin/plugin.json
  • plugins/claude-loop/README.md
  • plugins/claude-loop/commands/help.md
  • plugins/claude-loop/commands/list.md
  • plugins/claude-loop/commands/loop.md
  • plugins/claude-loop/commands/stop.md
✅ Files skipped from review due to trivial changes (1)
  • plugins/claude-loop/.claude-plugin/plugin.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • .claude-plugin/marketplace.json

Comment on lines +58 to +64
| `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 |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In standard 5-field cron, does 0 */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):

  • Next runs: 2026-03-08 14:00, 16:00, 18:00, 20:00, 22:00, 2026-03-09 00:00, …

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):

  • Next runs: 2026-03-08 15:07, 20:07, 2026-03-09 00:07, 05:07, 10:07, …
  • Notice the gap 20:07 → 00:07 is 4 hours, not 5. (en.wikipedia.org)

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
-| `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 |
-| `Nd` (1-31) | `M H */N * *` | M ∈ 1-59, H ∈ 7-21 |
+| `Nm` (60+) | Only if exactly representable in cron | Otherwise fall back to Tier 2 or reject |
+| `Nh` (1-23) | Only if exactly representable in cron | Otherwise fall back to Tier 2 or reject |
+| `Nd` (1-31) | Only if exact calendar-day semantics are acceptable | Otherwise fall back to Tier 2 or reject |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@plugins/claude-loop/commands/loop.md` around lines 58 - 64, The docs
currently show automatic translations for unsupported intervals (see table
entries like `Ns`, `Nm (60+)`, `Nh (24+)`, `Nd`) which silently round or convert
intervals into different cron cadences; update the text and the table to stop
suggesting these silent translations and instead state that intervals not
representable exactly in 5-field cron should either fall back to Tier 2
scheduling or be rejected with an explicit error message to the user;
specifically remove or mark as unsupported the mappings for `Nm (60+)` → `0 */H
* * *`, `Nh (24+)` → Convert to days, and any rounding behavior under `Ns`/`Nm`
that implies drift, and add a short note that exact-interval requirements must
use Tier 2 or be declined.

Comment on lines +113 to +121
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Use a private per-session state directory instead of shared /tmp files.

Storing executable prompts in predictable /tmp/claude-loop-<id>.state files lets other local processes discover, delete, or overwrite another session's jobs. Because the fire path later reads that file and executes its prompt, this is both a session-isolation bug and a prompt-injection risk. Put Tier 2 state under a per-session temp directory created with restrictive permissions, and have list/stop only inspect that directory.

🔒 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
Verify each finding against the current code and only fix it if needed.

In `@plugins/claude-loop/commands/loop.md` around lines 113 - 121, The current
design writes job state to predictable global files like
/tmp/claude-loop-<id>.state which allows other local processes to enumerate or
tamper with prompts; change to create a private per-session temp directory with
restrictive permissions (owner-only) and store state files inside it (e.g.,
session-specific temp dir created at session start), update the logic that
generates state files (the code that writes `/tmp/claude-loop-<id>.state` and
sets "session_pid") to write into that session directory, and update the
list/stop and the fire path to only read/inspect files in that per-session
directory so only the owning session can discover, delete, or execute its jobs.
Ensure the session dir is created atomically with secure perms and cleaned up
when the session ends.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant