Skip to main content

⏻ claudekeeper

Schedule prompts for Claude Code, and keep your sessions running through the 5‑hour usage cap.

CI License: MIT Python Platform Dependencies

claudekeeper demo

claudekeeper is a tiny background daemon that watches your Claude Code sessions running in tmux and does two things your usage limit won't let you do on your own:

  • ⏰ Schedule — pre‑type a prompt in a pane and have claudekeeper press Enter at a wall‑clock time (2am, now + 30min, …). Kick off long runs while you sleep.
  • ♻️ Auto‑resume — when a session hits the 5‑hour usage cap, claudekeeper waits out the reset and continues it automatically, so work doesn't stall while you're away.

And because it does both, it does the thing neither half can alone:

🧠 Cap‑aware deferral — a prompt scheduled for 2am is never fired into a capped pane and lost. claudekeeper holds it until the cap resets, then submits it.

It's dependency‑free Python driven entirely through the tmux CLI, so it runs anywhere tmux does — Linux boxes, HPC login nodes, macOS.

🚀 Install

One‑liner (recommended):

curl -fsSL https://raw.githubusercontent.com/PursuitOfDataScience/claudekeeper/main/install.sh | sh

Drops claudekeeper in ~/.local/share/claudekeeper and a launcher at ~/.local/bin/claudekeeper. The only runtime requirements are python3 ≥ 3.8 and tmux — both already on almost any machine you'd run Claude Code on. No pip, no virtualenv, nothing to compile.

Or via a Python package manager:

uv tool install claudekeeper      # or:  pipx install claudekeeper
Why not npm i -g?

npm installs Node programs; claudekeeper is Python. The Python equivalent of npm's global one‑liner is uv tool install / pipx install above, or the curl | sh script (which needs nothing but python3). If ~/.local/bin isn't on your PATH, the installer tells you how to add it.

⚡ Quickstart

claudekeeper setup                              # 1. one-time: start the background daemon

# 2. Open Claude Code in a tmux session, type your prompt, DON'T hit enter — then:
claudekeeper schedule mysession 2am             #    claudekeeper presses Enter at 2am
claudekeeper schedule mysession "now + 30min" --prompt "run the full test suite"
claudekeeper schedule a,b,c 2am                 #    several sessions at once, fired together

# 3. (optional) auto-resume after the usage cap:
claudekeeper watch on

claudekeeper status                             # the daemon, your jobs, your sessions
claudekeeper logs -f                            # watch what it does, live

Run claudekeeper with no arguments any time for a refresher — it even lists the Claude sessions you have running right now.

🧭 The mental model (read this once)

claudekeeper has two moving parts:

  1. A background daemon (claudekeeper setup installs it). It's the thing that actually presses keys. Scheduled jobs only fire while the daemon is running — the one gotcha, and claudekeeper schedule warns you if it isn't.
  2. A job list + settings you manage from the CLI (schedule, ls, rm, watch on/off). The daemon reads these live.

You pre‑type your prompt into the Claude Code input box and leave it there; claudekeeper submits it for you (a single Enter) at the right moment. With --prompt "...", claudekeeper types the text for you instead.

📖 Commands

Command What it does
claudekeeper setup Install + start the daemon, then print personalized next steps
claudekeeper schedule <target,...> <time> [--prompt T] [--clear] [--daily] [--force] Arm a keypress. <target> is a tmux session or session:win.paneor a comma‑separated list (a,b,c) to arm several at once. --clear wipes the input box before typing --prompt.
claudekeeper ls List scheduled jobs (with ETA and any deferral)
claudekeeper rm <id,...|all> Cancel jobs — one id, a comma‑separated list, or all
claudekeeper watch on|off|status Toggle cap auto‑resume (off by default)
claudekeeper status Daemon state, jobs, detected Claude panes
claudekeeper logs [-f] [-n N] View / follow activity
claudekeeper service install|uninstall|start|stop|restart|status Manage the daemon directly
claudekeeper daemon Run the loop in the foreground (what the service runs)
claudekeeper doctor Check python, tmux, service manager, timezone, paths

Time formats: 2am, 3pm, 9:30am, 14:30, 22:00, now + 5min, +10m, in 90s, noon, midnight, teatime.

Targets are matched exactly. Plain tmux -t abc falls back to prefix and glob matching, so a typo'd abc would resolve to a session called abcdef — not something you want an automated Enter doing.

🎯 Several sessions at once

Pass a comma‑separated list instead of a single target — handy when you've staged a prompt in a, b and c and want them all to go at 2am:

claudekeeper schedule a,b,c 2am
✓  will press Enter at Sun 2026-07-26 02:00:00 in 3 tmux sessions:
     a  [pj-38ef2713]
     b  [pj-0089457b]
     c  [pj-13904796]
  • Every session gets its own job id sharing one fire time, so they submit in the same second — but you can rm one without touching the others, and each is deferred independently if its pane is capped.
  • All‑or‑nothing: if any session in the list doesn't exist, nothing is scheduled and the error names the bad one — a typo never leaves you half‑armed. (--force skips the check.)
  • Entries may be pane‑qualified and mixed freely: claudekeeper schedule a:0.0,b 2am. Repeats are dropped.
  • rm takes a list too: claudekeeper rm pj-38ef2713,pj-0089457b.

Quoting: a,b,c works bare; "a, b, c" works quoted. Unquoted a, b, c does not — your shell splits it into separate arguments before claudekeeper sees it.

🛡️ Safety

claudekeeper automates keystrokes into your terminals. It's built fail‑safe, but be aware:

  • Auto‑resume is opt‑in. watch is off until you run claudekeeper watch on. Out of the box claudekeeper only fires prompts you explicitly schedule.
  • A cap is only a cap if the pane shows a real reset time. Auto‑resume ignores API errors, stale scrollback, and mere chatter about limits — it acts only on a parseable reset time. Each cap is identified by (reset time, pane) and resumed at most resume_attempts times (2 by default, the second only if the first demonstrably didn't take), so it can never loop and drain your budget. A new cap — a different reset time — re‑arms normally.
  • A blind Enter lands on whatever has focus. If a pane is sitting on a tool‑permission prompt whose default is destructive, an Enter would accept it. Schedule against panes you've left at the prompt with your text staged.
  • It's node‑local. The daemon watches the tmux server on the machine it runs on; run it on the same host as your sessions. Jobs record the host that armed them, so a daemon on another node of a shared‑$HOME cluster leaves them alone rather than acting on — or deleting — them.

⚙️ Configuration

Settings live in ~/.local/state/claudekeeper/config.json (override the dir with CLAUDEKEEPER_HOME). Toggle the common one with claudekeeper watch on/off; edit the file for the rest. The daemon re‑reads it every loop, so changes take effect without a restart, and out‑of‑range or wrong‑type values are clamped to safe defaults rather than crashing the daemon. Only settings you actually change are written, so improved defaults still reach you on upgrade.

Setting Default What it does
watch_enabled false Cap auto‑resume (claudekeeper watch on)
msg "continue the work" Typed to resume when the input box is empty
match_command ["claude", "node"] tmux pane_current_commands to watch (see below)
poll 30 Seconds between scans (the daemon also wakes early for a due job)
buffer 60 Wait this long past a parsed reset before resuming
stagger 45 Minimum gap between resumes of different panes
stale_past 900 A reset older than this is leftover scrollback, not a live cap
maxwait 21600 Midnight‑wrap window for a bare clock reset
max_defer 691200 Hard ceiling on any deferral (8 days, so weekly limits fit)
grace 900 Hold a due job this long while its pane is unreachable
resume_attempts / retry_after 2 / 90 Retry an unverified resume, then give up loudly
prompt_marks ["❯", ">"] Glyphs that mark Claude Code's input line
host_check true Don't let one node's daemon act on another node's jobs
only / exclude [] Restrict which sessions watch touches

Environment: CLAUDEKEEPER_HOME (state dir), CLAUDEKEEPER_TMUX (tmux binary), and CLAUDEKEEPER_TMUX_SOCKET / CLAUDEKEEPER_TMUX_SOCKET_PATH if your sessions live on a non‑default tmux server (tmux -L work). claudekeeper setup bakes all of these — plus your TZ — into the service it installs.

"claude panes: 0" but Claude Code is right there?

tmux reports the interpreter, not the script: an npm‑installed Claude Code shows up as node, a native‑binary install as claude. Both are matched by default, and a pane matched by a generic name like node must additionally look like Claude Code on screen before claudekeeper will touch it. If your install reports something else, claudekeeper status names it — add it to match_command. Check by hand with:

tmux display -p -t <session> '#{pane_current_command}'

🖥️ On an HPC cluster

  • systemctl --user usually has no session bus on a login node. claudekeeper detects that and starts the daemon directly in a detached session instead; claudekeeper service start|stop|restart|status work the same either way. It prints a @reboot crontab line if you want it back after a reboot.
  • Keep CLAUDEKEEPER_HOME node‑local — e.g. export CLAUDEKEEPER_HOME=/tmp/$USER/claudekeeper. On a shared $HOME the state dir is visible from every login node, flock may be a no‑op there, and the daemons would be fighting over one job list. claudekeeper doctor tells you the filesystem type and warns if it looks networked. (Jobs also record their host, so even in that setup a daemon elsewhere leaves them alone instead of deleting them — but node‑local is still the right answer.)
  • Run the daemon on the same node as your sessions. tmux is node‑local; a daemon on login3 cannot see or drive a pane on login1.
  • On a compute node, start it over ssh or from inside tmux — not from a bare srun step. A shell from srun --jobid=N --overlap --pty bash lives in the cgroup job_<id>/step_<N>, and slurmstepd destroys that cgroup — every process in it — the instant the step exits. Detaching does not help: nohup/setsid leave the terminal, not the cgroup. (ssh to the node is adopted into step_extern, which lives as long as the job, which is why the same command "works over ssh".) claudekeeper notices when it is being started inside a step and parents the daemon to the tmux server instead, which is in step_extern and which it needs anyway — so starting it from tmux works too. With no tmux server to host it, it starts the daemon anyway and warns that the step will take it down.
  • The state dir is created 0700 and the log 0600 — both hold your prompt text, and a shared filesystem plus a default umask would otherwise leave them readable.

🔬 How it works

A single loop, every poll seconds (or sooner, if a job is due before then):

  1. list tmux panes running Claude Code, capture each one's text (panes are tracked by tmux pane_id, which — unlike sess:win.pane — doesn't shift when a neighbouring pane closes);
  2. if watch is on, detect real caps and schedule a one‑time resume at reset + buffer (staggered across sessions), then confirm on a later pass that it took;
  3. fire any due scheduled jobs — deferring any whose target pane is capped, until the pane's parsed reset time passes.

That last point is the whole trick. Claude Code leaves the "limit reached" banner on screen until something else prints, so a deferral can't wait for the banner to disappear — it has to key off the reset time the banner states.

Scheduled jobs are persisted before the keypress (at‑most‑once — a crash never double‑fires a prompt into Claude). A job is never lost to a tmux outage, and a job whose session or pane is temporarily missing is held for grace before being given up on. Handled caps persist across restarts, so a restart never re‑fires an already‑handled cap. Only one daemon runs per CLAUDEKEEPER_HOME (single‑instance lock, which doubles as the liveness check — status is right however you started the daemon).

🧪 Development

git clone https://github.com/PursuitOfDataScience/claudekeeper && cd claudekeeper
PYTHONPATH=src python3 -m claudekeeper doctor
for f in tests/test_*.py; do PYTHONPATH=src python3 "$f"; done   # unit tests (no deps)
PYTHONPATH=src pytest -q tests/                                  # ... or via pytest
ruff check .                                                     # lint (CI runs this)

The suite deliberately avoids bare assert so that python -O can't quietly turn it into a no‑op. tests/test_daemon.py drives the daemon against a fake tmux and a frozen clock — that's where the scheduling guarantees above are pinned down.

⚖️ Disclaimer

claudekeeper is an independent, community‑built tool. It is not affiliated with, endorsed by, or sponsored by Anthropic. "Claude" and "Claude Code" are trademarks of Anthropic, PBC — used here only to describe what the tool works with. claudekeeper just automates the tmux CLI around Claude Code sessions you run yourself.

📄 License

MIT © 2026 PursuitOfDataScience

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

claudekeeper-0.3.1.tar.gz (162.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

claudekeeper-0.3.1-py3-none-any.whl (48.6 kB view details)

Uploaded Python 3

File details

Details for the file claudekeeper-0.3.1.tar.gz.

File metadata

  • Download URL: claudekeeper-0.3.1.tar.gz
  • Upload date:
  • Size: 162.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for claudekeeper-0.3.1.tar.gz
Algorithm Hash digest
SHA256 4d06c10fab41d64ce46850ff8ed73eac43529ca52155c3e283a9eec84fb0eeb3
MD5 1c01ab744303f72c6887418fe9e1bf0d
BLAKE2b-256 7578ac7bb756998b667673eb4b8bf422a05e56ede6a872d2e08378df93a2f4a5

See more details on using hashes here.

Provenance

The following attestation bundles were made for claudekeeper-0.3.1.tar.gz:

Publisher: publish.yml on PursuitOfDataScience/claudekeeper

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file claudekeeper-0.3.1-py3-none-any.whl.

File metadata

  • Download URL: claudekeeper-0.3.1-py3-none-any.whl
  • Upload date:
  • Size: 48.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for claudekeeper-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 29c20fdfe808e34defaf849a22c91fba73fe5bbdfd3387a18d316eac9152fd04
MD5 832c97f0af18ce1c286c9989dcea4597
BLAKE2b-256 caa33185bbb0cd2a7d5c47428fc27e5837ba0404b15dbc22ee3c19944d529322

See more details on using hashes here.

Provenance

The following attestation bundles were made for claudekeeper-0.3.1-py3-none-any.whl:

Publisher: publish.yml on PursuitOfDataScience/claudekeeper

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page