Skip to main content

cwarm

CI PyPI Python License: MIT

Warm your Claude Code 5-hour usage window on a schedule, so its dead regeneration time lands outside your working hours instead of stranding you mid-flow. cwarm sends a tiny "Hi" at the times you choose to anchor the window early — and can stagger several accounts too, if you rotate them with claude-swap (cswap).

cwarm stores no credentials — claude-swap owns your account and tokens; cwarm only triggers the warmup.

Why I built it

I kept hitting the same wall: my Claude Code 5-hour window starts on the first message and resets exactly 5 hours later — and I burn through its budget in about 2 hours, then sit idle for the remaining 3 waiting for the reset. Start cold at 8 AM and that dead stretch lands around 10 AM–1 PM, right in the middle of my day.

The fix: send a throwaway "Hi" before I start. Fire it at 5 AM and the window runs 05:00–10:00 — so I work 8–10, exhaust it, and the reset is already there; the 3-hour gap happened before I sat down instead of mid-morning. Re-warm at the reset times and the windows stay anchored to clock times I choose, so the idle stretches land on lunch instead of mid-flow. It doesn't create more budget — it just moves the dead time out of my way.

I kept meaning to fire that ping every morning and kept forgetting. cwarm just does it, on a schedule. (It handles several accounts too, if you rotate them — that's just not how I use it.)

How it works

A Claude Code 5-hour window starts on your first message and resets exactly 5 hours later. It's a fixed budget, not free capacity — warming only relocates the dead/regeneration time. Send a warmup "Hi" at 05:00 and the window runs 05:00–10:00, so the idle stretch happens before work instead of mid-morning. Give an account several warmup times to re-anchor each new window to clock times you pick, dropping the dead gaps onto your breaks.

Multiple accounts (optional). If you keep more than one account in claude-swap, cwarm warms each on its own staggered schedule — switching to it, sending the message, then restoring whichever account you had active before (always, even if a warmup fails, so running it never leaves your setup changed). Staggered resets mean there's always a fresh account to switch into.

A weekly cap is shared across web, app, and Claude Code; cwarm does not track it.

Install

With uv (recommended):

uv tool install cwarm     # installs the `cwarm` command, isolated
# or run it without installing:
uvx cwarm --help

Or with pipx / pip:

pipx install cwarm
# or
pip install cwarm

Requires Python 3.12+ (uv/pipx handle that for you).

Quick start

cwarm init        # writes a starter config to ~/.config/cwarm/config.json
$EDITOR ~/.config/cwarm/config.json
cwarm validate    # checks your config + that cswap/claude are set up; sends nothing
cwarm list        # shows each account, whether it's warm, and the next run
cwarm run         # warm everything now (optional sanity check)
cwarm daemon      # leave running to warm on schedule

cwarm reads ~/.config/cwarm/config.json by default (override with --config), so the commands above work from any directory.

You also need, for each account you list:

  • claude-swap installed, with the account added (cswap --add-account or cswap --add-token sk-ant-oat01-…),
  • Claude Code (claude) installed.

Configuration (config.json)

No tokens. Accounts are referenced by their cswap handle — a slot number or email. The simplest config is a single account warmed before work:

{
  "defaults": {
    "message": "Hi",
    "timezone": "Asia/Kolkata",
    "settle_seconds": 3,
    "skip_if_warm": true
  },
  "accounts": [
    { "id": "1", "schedules": ["0 5 * * 1-5", "0 10 * * 1-5", "0 15 * * 1-5"] }
  ]
}

That warms account 1 at 05:00, 10:00, and 15:00 on weekdays, anchoring each new 5-hour window to those clock times. An account can warm at several times a day — give it a schedules array — or use the singular schedule string for one time. Each cron time fires in the account's timezone.

Rotating multiple accounts? Add more entries on staggered times so a fresh window is always available to switch into:

"accounts": [
  { "id": "1", "schedule": "0 5 * * 1-5" },
  { "id": "2", "schedule": "30 7 * * 1-5" },
  { "id": "3", "enabled": false, "schedule": "0 10 * * *" }
]
Field Required Default Notes
id yes — unique; the account's cswap slot number or email
schedule / schedules yes — one (string) or many (array) of 5-field cron times, read in the account's timezone
enabled no true false skips the account entirely
message no "Hi" the warmup message
model no "haiku" model for the ping (passed as --model); a ping needs no more. null = agent default
timezone no Asia/Kolkata IANA tz name
settle_seconds no 3 delay after switching before sending
skip_if_warm no false skip if the window is already open (saves usage)
agent no claude which coding agent warms this account (currently claude)

Your config lives at ~/.config/cwarm/config.json (it holds your real account ids — keep it out of version control). Point anywhere else with --config.

Commands

cwarm init                           # write a starter config.json
cwarm validate                       # check config + setup; sends nothing
cwarm list                           # accounts, live window state, next run
cwarm run                            # warm all enabled accounts now
cwarm run --account work@example.com # warm just one
cwarm daemon                         # long-lived; fires each account on its cron

# global flags
cwarm --config /path/to/config.json --log-file /path/to/cwarm.log <command>
  • validate — confirms the config is valid, cswap/claude are installed, and every configured id exists. Exits non-zero and sends nothing on a problem.
  • list — read-only table: account, enabled, live window state (warm/cold), next scheduled run, cron times. Sends nothing.
  • run — warms enabled accounts immediately. Non-zero if any warmup failed.
  • daemon — schedules one job per account per cron time. Warmups are serial.

Logging

Every attempt emits one structured line to stderr (and the log file if set):

2026-06-21T05:00:03+0530 INFO save-active switcher=cswap account=work@example.com ref=1
2026-06-21T05:00:09+0530 INFO warmup account=work@example.com outcome=ok reset=10:00
2026-06-21T05:00:10+0530 INFO restore-active switcher=cswap account=work@example.com ref=1 outcome=ok

Outcomes: ok (with the window reset time), failed (with an error summary), skipped (already warm, with skip_if_warm).

Running it on a schedule

systemd (Linux)

A sample unit ships in systemd/cwarm.service; it reads the default ~/.config/cwarm/config.json that cwarm init writes.

mkdir -p ~/.config/systemd/user ~/.local/state/cwarm
cp systemd/cwarm.service ~/.config/systemd/user/   # adjust the cwarm path if needed
systemctl --user daemon-reload
systemctl --user enable --now cwarm
loginctl enable-linger "$USER"        # run without an active login session
journalctl --user -u cwarm -f

It starts on boot and restarts on failure. On (re)start the schedule is rebuilt from config.json; a warmup missed by under an hour still fires once on recovery, but a warmup missed across a long power-off is skipped (firing a 05:00 warmup at noon would defeat the staggering).

Alternative: cron

One line per warmup time (cwarm must be on PATH):

0 5  * * 1-5 cwarm --config ~/.config/cwarm/config.json run --account work@example.com
0 11 * * 1-5 cwarm --config ~/.config/cwarm/config.json run --account work@example.com
30 7 * * 1-5 cwarm --config ~/.config/cwarm/config.json run --account 2

macOS / Windows

cwarm run/daemon are cross-platform — only the boot-persistence recipe differs. On macOS use launchd or cron; on Windows use Task Scheduler (or run cwarm daemon as a service). The tool needs claude and cswap available on that OS.

Tips

  • skip_if_warm: true is the biggest usage saver — it skips the whole claude -p call (a real LLM request) when an account's window is already open.
  • Stagger, don't stack. Overlapping schedules waste warmups and the shared weekly cap; one warmup per window per account is enough to anchor it.
  • The daemon doesn't poll — it sleeps until the next job (~25 MB RAM, ~0% CPU idle).

Safety

  • No credentials stored or logged — claude-swap owns them.
  • Switching changes your active account momentarily; the save/restore guarantees your default is unchanged after a run, so prefer off-hours anyway.
  • Only configure accounts you legitimately own or are authorised to use.

License

MIT — see LICENSE. Project internals and contribution notes live in CLAUDE.md.

Metadata

Release files for cwarm 0.2.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cwarm 0.2.1
File Size Uploaded Provenance Actions
cwarm-0.2.1.tar.gz 27.5 kB With provenance Details for cwarm-0.2.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for cwarm 0.2.1
File Interpreter ABI Platform Actions
cwarm-0.2.1-py3-none-any.whl Python 3 none any Details for cwarm-0.2.1-py3-none-any.whl

Total release size: 49.3 kB

Release files / cwarm-0.2.1.tar.gz

Download URL cwarm-0.2.1.tar.gz
Size 27.5 kB
Tags Source
SHA-256 checksum
How to use checksums
0818087e35e6bf0a5475b26f35b1b19af22b665699b29adb13e65f9cd7cc5ecd
BLAKE2b-256 checksum
How to use checksums
5b7e5837e58ec6e02cfe32e69a603f4921b9b7feed2c7ec58da99e912db8eca7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 Jul 19, 2026.

Transparency log

Release files / cwarm-0.2.1-py3-none-any.whl

Download URL cwarm-0.2.1-py3-none-any.whl
Size 21.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
52c042401a4714b31853b0dcba3b4deecfab559e9dc6ad60d9b307492e488e06
BLAKE2b-256 checksum
How to use checksums
13d3122a58a1b7fa201dcb9e9e6518f7bbc5cd49b5250b33b5fe5b67c17e0dbd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 Jul 19, 2026.

Transparency log

Security for cwarm 0.2.1

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

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for cwarm-0.2.1.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 Jul 19, 2026.

Transparency log

A 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.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page