Metadata-Version: 2.5
Name: claudekeeper
Version: 0.3.1
Summary: Cap-aware tmux scheduler & auto-resume for Claude Code — schedule prompts and keep sessions running through the 5-hour usage limit.
Project-URL: Homepage, https://github.com/PursuitOfDataScience/claudekeeper
Project-URL: Repository, https://github.com/PursuitOfDataScience/claudekeeper.git
Project-URL: Documentation, https://github.com/PursuitOfDataScience/claudekeeper#readme
Project-URL: Issues, https://github.com/PursuitOfDataScience/claudekeeper/issues
Author-email: Youzhi Yu <yuyouzhi666@icloud.com>
License-Expression: MIT
License-File: LICENSE
Keywords: anthropic,auto-resume,automation,claude,claude-code,cli,cron,daemon,developer-tools,productivity,rate-limit,scheduler,terminal,tmux,usage-limit
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: System :: Shells
Classifier: Topic :: Utilities
Requires-Python: >=3.8
Description-Content-Type: text/markdown

<div align="center">

# ⏻ claudekeeper

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

[![CI](https://github.com/PursuitOfDataScience/claudekeeper/actions/workflows/ci.yml/badge.svg)](https://github.com/PursuitOfDataScience/claudekeeper/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
![Python](https://img.shields.io/badge/python-3.8%2B-blue.svg)
![Platform](https://img.shields.io/badge/platform-Linux%20%7C%20macOS-lightgrey.svg)
![Dependencies](https://img.shields.io/badge/dependencies-none-brightgreen.svg)

<img src="docs/demo.gif" alt="claudekeeper demo" width="820">

</div>

---

`claudekeeper` is a tiny background daemon that watches your [Claude Code](https://claude.com/claude-code) sessions running in [tmux](https://github.com/tmux/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 <kbd>Enter</kbd> 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):**

```sh
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:**

```sh
uv tool install claudekeeper      # or:  pipx install claudekeeper
```

<details>
<summary><b>Why not <code>npm i -g</code>?</b></summary>

`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.
</details>

## ⚡ Quickstart

```sh
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 <kbd>Enter</kbd>) 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.pane` — **or 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 <kbd>Enter</kbd> 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:

```sh
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 <kbd>Enter</kbd> lands on whatever has focus.** If a pane is sitting on a tool‑permission prompt whose default is destructive, an <kbd>Enter</kbd> 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_command`s 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.

<details>
<summary><b>"claude panes: 0" but Claude Code is right there?</b></summary>

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:

```sh
tmux display -p -t <session> '#{pane_current_command}'
```
</details>

## 🖥️ 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

```sh
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](LICENSE) © 2026 [PursuitOfDataScience](https://github.com/PursuitOfDataScience)
