loops-routines
An independent project, not made or endorsed by Anthropic. "Claude" and "Claude Code" are Anthropic's trademarks; loops-routines only starts the
claudecommand you already have.
A daemon that runs Claude Code routines (on a cron line or an interval) within your plan's usage limits. One process serves everything: the scheduler, the REST API with live events, the MCP server for your Claude sessions, and a small web page. Agents tell you things with macOS notifications and ask you questions in system dialogs with their own buttons, waiting for your click.
Examples it is made for:
- a weekly summary of the changes in several repositories (
0 9 * * mon); - a cleanup every 2 hours, but only with tokens the 5-hour window would lose anyway (
profile = "spare"); - a log monitor every 30 minutes that stays quiet until something new breaks (
notify = "on_alert").
Install
Needs macOS, uv and Claude Code (claude
on your PATH, signed in).
uv tool install loops-routines # from PyPI: the `loops-routines` command in ~/.local/bin
loops-routines install --launchd --mcp # ~/.loops_routines, the daemon as a LaunchAgent, the MCP server in Claude Code
loops-routines status
--launchd writes ~/Library/LaunchAgents/com.loops-routines.daemon.plist and loads it: the daemon starts now
and at every login, and launchd restarts it if it stops. It runs as you (a LaunchAgent, not a system
LaunchDaemon), so agents use your Claude sign-in, keychain and tools. Run the install from a terminal
whose PATH finds claude and whatever your routines call: the LaunchAgent keeps that PATH.
Upgrade, then restart the daemon so it runs the new code:
uv tool upgrade loops-routines
loops-routines restart
Remove: loops-routines uninstall (the LaunchAgent; ~/.loops_routines is kept), then uv tool uninstall loops-routines.
Before you start
- macOS only. Notifications, dialogs and the LaunchAgent are macOS's; the scheduler itself is not, but nothing else is supported yet.
- Routines spend your plan. Every run is a
claude -psession on your own sign-in and counts against your 5-hour and 7-day windows. Limit profiles (spare,keep_free) decide how much a routine may take; start withprofile = "spare"for anything that doesn't have to run. - Agents act as you. They run with your files, keychain and
ghlogin, limited by the allowed tools inconfig.tomland the routine. Read SECURITY.md before switching on a routine that pushes, posts or reads untrusted text. - The example routines in
~/.loops_routines/routines/are switched off and name placeholder repositories and services: edit them first.
Quick start (from a checkout)
uv sync
uv run loops-routines install # ~/.loops_routines: config.toml, README, three example routines (off)
uv run loops-routines daemon # or: loops-routines install --launchd (keeps it running, starts at login)
uv run loops-routines status
uv run loops-routines open # the web page in the browser
uv run loops-routines install --mcp # let your Claude Code sessions see and steer the routines
A routine is a file ~/.loops_routines/routines/<name>.md: a TOML header between +++ lines, then the
prompt. See loops_routines/defaults/README.md (copied to ~/.loops_routines/README.md) for every key, and
loops-routines check <file> to validate one and see its next three runs.
Architecture
launchd (KeepAlive)
└─ loops-routines daemon ───────────────────────────────────────────────────────────┐
Service.tick (every 30 s): reload files → wind down runs past keep_free → │
evaluate each routine → expire / wait / launch by priority → probe usage │
Run × max_parallel → `claude -p` stream-json, own process group, own folder │
Store (SQLite, the daemon's only) deliver: reports/*.md │
REST /api/* + SSE /api/events MCP /mcp (user) MCP /agent/mcp (runs) │
/ui/ — a web page on the same API │
────────────────────────────────────────────────────────────────────────────────────┘
▲ REST + SSE ▲ MCP ▲ REST
browser Claude Code CLI
run ── /agent/mcp notify ──▶ daemon ──▶ banner (Notification Center)
run ── /agent/mcp ask_user ─▶ daemon ──▶ dialog with buttons ──┐ first answer wins:
◀── answer ── web page / CLI / MCP ┘ the dialog, or any of these
| Module | What it does |
|---|---|
core/triggers.py |
cron (5 fields, names, steps, @daily) and every 30m; next_after(moment) |
core/windows.py |
time windows mon-fri 09:00-19:00 (from Loops) |
core/routines.py |
routine files → Routine; broken files are kept with their error |
core/evaluate.py |
the pure decision: idle / wait (why) / launch / expire for one routine |
core/usage.py, core/probe.py |
5-hour and 7-day snapshots, the limit gates, the one-request probe (from Loops) |
core/integrations.py |
plugins, MCP servers, skills a routine names → claude flags (from Loops) |
core/runner.py |
one run: clones, per-run config files, stream-json in/out, stop, wrap-up, result file |
core/store.py |
SQLite: routine state, runs (and skipped occurrences), messages from agents |
service.py |
the scheduler tick and every query and action; REST, MCP and CLI are thin over it |
daemon/app.py, daemon/mcp.py, daemon/auth.py |
HTTP transports and the token check |
core/notify.py |
macOS banners and button dialogs through osascript; the prompt rules per notify |
ui/web/index.html |
the web page (loops-routines open) |
Scheduling rules
- Occurrences come from the trigger; only the latest due one counts, so a Mac that slept for a day runs a routine once, not 48 times.
- An occurrence may wait for its window and limits until its deadline:
graceafter it, or (withoutgrace) until the next occurrence. Past it, it is recorded as skipped with the reason it waited for. - Limits:
min_freeandbefore_resetdecide the start;keep_free/keep_free_weeklyalso wind a running run down (it getswind_down_minutesto write its result, then it is stopped). - When numbers are older than
max_snapshot_age_minutes, the daemon spends one haiku request on a probe (at most everyprobe_cooldown_minutes). Every run also refreshes them from its own events. - Several due routines: higher
priorityfirst, up tomax_parallelat once; a routine never overlaps itself. "Run now" does not use up an occurrence. - A routine switched off (in its file or the UI) owes nothing for the time it was off.
Results
The agent ends by writing {"status": "ok | alert | nothing", "summary", "report"} to its result file.
The daemon stores it and writes reports/<routine>/<run>.md. Without a result file, the agent's last
words become the summary with status unknown. While it works, the agent may post report_status
lines through /agent/mcp, and you can message it (loops-routines say RUN TEXT).
Notifications and questions
Agents use two tools of /agent/mcp, as their routine's notify allows (always: the outcome when it
finishes; on_alert: only an alert; never: neither tool):
notify(text, kind)— a Notification Center banner titled with the routine;kindis info, success, warning or error (error and warning play[notify] error_sound).ask_user(question, buttons, timeout_minutes)— a system dialog with 1–3 buttons of the agent's wording (the recommended one is the default, on the right) and waits. The first answer wins: a click in the dialog, the web page,loops-routines answer, oranswer_questionover MCP; the dialog then closes. No answer in time, or the run ends: the agent getstimed_out/cancelledand takes the safe path.
The daemon notifies by itself only what no agent can: a run that failed or timed out, an alert the agent
did not send, and a report_blocker. Everything is kept with the run.
Why a dialog and not buttons on the banner: macOS gives notification actions only to a signed app with its
own notification permission; osascript banners can't have buttons. The first banner may make macOS ask
whether "Script Editor" may notify — allow it in System Settings → Notifications. loops-routines notify-test (and --ask) checks both.
Security
The daemon listens on 127.0.0.1 only and checks the Host header. /api and /mcp need the token in
~/Library/Application Support/loops_routines/token (0600); /agent/mcp accepts only a live run's own token,
so an agent can't post as another run. The UI page gets the token from its URL fragment (never sent to
the server) and keeps it for the tab only.
API
GET /health GET /api/summary
GET /api/routines GET /api/routines/{name}
POST /api/routines/{name}/run {force, out_of_queue}
POST /api/routines/{name}/enabled {enabled: true|false|null}
GET /api/runs?routine=&limit=&before=&skipped=0
GET /api/runs/{id} GET /api/runs/{id}/log?lines=
POST /api/runs/{id}/stop POST /api/runs/{id}/message {text}
POST /api/runs/seen {ids}
GET /api/usage POST /api/usage/probe
GET /api/integrations POST /api/integrations/scan
GET /api/questions POST /api/questions/{id}/answer {answer}
POST /api/notify/test {kind, ask}
POST /api/reload GET /api/events (SSE)
MCP /mcp: list_routines, routine_status, run_routine, run_routine_now (outside the queue: on top
of max_parallel, whatever its limits and window say), set_routine_enabled, list_runs,
get_run, run_log, stop_run, message_run, usage, pending_questions, answer_question.
MCP /agent/mcp (runs only): report_status, report_blocker, notify, ask_user.
Development
uv run pytest
uv run loops-routines --root /tmp/ct daemon # a separate instance: /tmp/ct/home, state, logs
Tests run the real runner against a fake claude script (tests/conftest.py). See
CONTRIBUTING.md for the rest, and CHANGELOG.md for what changed.
License
MIT.
Metadata
Release files for loops-routines 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| loops_routines-0.1.0.tar.gz | 146.3 kB | With provenance | Details for loops_routines-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| loops_routines-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for loops_routines-0.1.0-py3-none-any.whl |
Total release size: 235.0 kB
Release files / loops_routines-0.1.0.tar.gz
| Download URL | loops_routines-0.1.0.tar.gz |
|---|---|
| Size | 146.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
72d7dfbc6504eec7916bdd669eb9ea38a901ea717f80f24bd8ebb4ff4a668dea
|
|
BLAKE2b-256 checksum How to use checksums |
1a93a1f94b204845b24ddde667c195de19647ebe5235839debd5e87d06474281
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / loops_routines-0.1.0-py3-none-any.whl
| Download URL | loops_routines-0.1.0-py3-none-any.whl |
|---|---|
| Size | 88.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
acac27cad9927fc672b37fe0ac730622dff27069cce6aa2eb11b7fc40f40ff50
|
|
BLAKE2b-256 checksum How to use checksums |
4be679964c56a8ea759ad21c003ac6829710fd8cd10139d0acb05e9c9e8c17f6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for loops-routines 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for loops_routines-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.