Skip to main content

A local batch queue for Claude Code — SGE-style job files that run prompts unattended in 5-hour windows.

Project description

windowsill

A local batch queue for Claude Code.

Where you leave things overnight, in the window, to be dealt with in the morning.

The problem

The binding constraint on a Claude Code subscription usually isn't the weekly cap — it's the 5-hour usage window. A window starts on your first message and lasts 5 hours; whatever's left when you stop working is gone, not banked. Most of that headroom evaporates overnight while you sleep.

windowsill is a cron-driven queue that submits Markdown job files — prompt bodies with a small directive header — and dispatches them into windows you'd otherwise waste, up to a monthly ceiling you configure. You write jobs before bed; you run ccresume <id> in the morning to pick a conversation back up.

A quick note on words this project is careful about, since Claude Code overloads "session":

Concept Called Where you meet it
The 5-hour billing segment window /usage's "Current session: N% used"; the monthly benchmark
A Claude Code conversation with a session_id conversation --resume, ~/.claude/projects/
One execution attempt of a queued job run windowsill internals only

Install

uv tool install windowsill

That puts five commands on your PATH: the four short verbs ccsub, ccstat, ccdel, ccresume, and windowsill, the dispatcher that carries all of them plus the rest — windowsill show, windowsill report, windowsill tick. windowsill --help is the one place that lists everything.

From a checkout of this repo, run it straight from the project environment instead:

uv run ccstat

uv run uses the project's own venv with windowsill installed editable, so your source edits take effect immediately — prefer it while working in the repo. uvx --from . ccstat runs it too, but builds the package into a separate, ephemeral tool environment each time and runs that snapshot rather than your live source: handy for a one-off try, redundant once the code is already local.

Shell completion

windowsill completions install

Detects your shell from $SHELL (or take --shell bash|zsh|fish to skip that) and writes completion files for all five entry points — windowsill, ccsub, ccstat, ccdel, ccresume — straight into the directory that shell already scans on startup, so there's no rc file to edit and nothing to go stale: the script just re-invokes the program on each TAB press, so a CLI change shows up the next time you press TAB, not the next time you reinstall completions.

  • bash and fish autoload by filename, so install lands one real file — holding all five entry points' scripts — plus a symlink per remaining name in ~/.local/share/bash-completion/completions/ and ~/.config/fish/completions/ respectively.
  • zsh has no such fixed directory, so it gets five real files instead, under ~/.zsh/completions; if that isn't already on your $fpath, install prints the fpath+=(...) line to add.

Prefer an eval line in your rc over installed files? windowsill completions <bash|zsh|fish> emits the windowsill script alone to stdout:

eval "$(windowsill completions bash)"

Either way, a TAB press costs about 70-90ms now — imperceptible; it was ~220ms before commit 6daf5c3 trimmed cli.py's import-time cost.

Running on a schedule (cron)

windowsill dispatches jobs two ways (see design/03-execution-model.md). The fast path is the chain: when a job finishes it kicks the next tick itself, so back-to-back jobs in one open window start seconds apart. Cron is the liveness floor underneath that — it catches a broken chain, an --at coming due, and a rate-limit blocked_until expiring. Fifteen minutes is fine precisely because it is never the latency path.

Add one line to your crontab (crontab -e):

PATH=/home/you/.local/bin:/usr/bin:/bin
*/15 * * * * windowsill tick -q
  • -q suppresses tick's one-line summary, so cron has no output to mail you every 15 minutes.
  • PATH matters because cron runs with a nearly empty environment. Both windowsill and claude have to be findable on it — a tick with a due job spawns windowsill run, which spawns claude. uv tool install windowsill puts windowsill in ~/.local/bin, where claude usually already lives; confirm with command -v windowsill claude and set the PATH= line to match. (If you run from a checkout instead of installing, use uv run --project /path/to/windowsill windowsill tick -q and make sure uv is on that PATH too.)
  • HOME is set by cron automatically, which is all claude needs to find its credentials — a non-interactive job with no TTY authenticates fine (verified in design/09-findings.md).

To keep a heartbeat and capture any errors, redirect to a log instead:

*/15 * * * * windowsill tick >> "$HOME/.windowsill/logs/tick.log" 2>&1

Drop the -q when you redirect — the summary line becomes a useful "it's alive" trace in the log rather than mail in your inbox. windowsill creates ~/.windowsill/logs/ for you on first run.

The job file

A job is a UTF-8 text file: an optional shebang, a header of #$ directives, a blank line, then the prompt body — verbatim, no templating, any length.

#!/usr/bin/env ccsub
#$ --name refactor-auth
#$ --at 2026-07-18T02:00
#$ --model claude-opus-4-8
#$ --cwd ~/code/myapp
#$ --resume-from audit-auth
#$ --window fresh
#$ --isolate worktree
#$ --max-turns 60
#$ --retry 2
#$ --notify email

Continue the auth refactor from where the audit job left off.

Done means: every call site of `legacy_verify()` migrated to `verify_token()`,
`pytest tests/auth` green, and a single squashed commit on the branch.
If you hit a blocker, do not stop — write what you tried and what you would
need to `BLOCKED.md`, then continue with the next call site.

Parsing rules that matter:

  • The #! line, if present, is ignored — job files can be chmod +x and self-submitted. ccsub takes the file as its only argument, so #!/usr/bin/env ccsub makes ./refactor-auth.md submit itself.
  • Directives are lines matching ^#\$\s+(.*)$, and must appear before the first non-directive, non-blank, non-shebang line. Once prose starts, the header is closed — a #$ line can't resume later in the file.
  • The first blank line after the header ends the header. Everything after it, to EOF, is the body: verbatim, unmodified.
  • Bare # lines and #$ #... inside the header are comments.
  • A trailing \ continues a directive onto the next line.
  • Directive arguments are tokenised with shell-like quoting rules (so --name "has spaces" works as you'd expect).

Two kinds of directive. windowsill recognises a fixed set of scheduler directives (below); everything else is passed through verbatim, in order, to claude as argv. #$ --model claude-opus-4-8 becomes claude --model claude-opus-4-8. This means a new claude flag works the day it ships, with no change here — and windowsill never validates or rejects a directive it doesn't recognise; unknown ones flow straight through and claude complains if they're wrong.

Scheduler directives

Directive Default Meaning
--name <slug> filename stem Human label. Unique only among non-terminal jobs
--at <when> ASAP Earliest start. ISO-8601, tomorrow 02:00, or +3h
--before <time> none Do not start after this; job becomes expired if missed
--window <policy> share fresh | share | offpeak
--anchor false This is an anchor job. Implies --model claude-haiku-4-5 unless overridden
--cwd <path> submit-time cwd Working directory. ~ expanded at submit
--log <path> job dir only Also write the rendered transcript here. A trailing / (or an existing directory) means "in there", as <id>-<name>.md; otherwise it is the file. Relative to --cwd
--resume <conversation_id> Continue an explicit conversation
--resume-from <job> Continue the conversation produced by a completed job (resolved at dispatch)
--hold-jid <job>[,<job>] Wait for those jobs to reach done
--retry <n> 0 Retries on non-rate-limit failure. Rate-limit requeues are free and unbounded
--priority <-20..19> 0 Tie-break within a window; lower runs first
--isolate <mode> config none | worktree
--allow-dirty false Permit running against a dirty tree
--notify <channel>[:<dest>] config (none) none | ntfy | email. May carry this job's own recipient or topic: email:me@example.com
--hold false Submit held; needs windowsill release
--timeout <dur> 4h Wall-clock kill
--no-resume-on-limit false On rate-limit requeue, restart from the original body instead of resuming

--resume and --resume-from are mutually exclusive; both end up as claude --resume <id> at dispatch. --resume is the one case where a scheduler directive shadows a real claude flag.

Every directive above is parsed and acted on today, with two exceptions: --isolate worktree is stored but not yet honoured (runs happen in --cwd regardless), and --allow-dirty is not yet checked.

Commands

Three commands you'll use constantly, each its own executable, after Grid Engine's qsub/qstat/qdel:

  • ccsub <file> — submit a job. Use - to read from stdin. Prints the job id.
  • ccstat — see what's queued, running, held, or recently finished: id, name, state, model, when, cwd, elapsed.
  • ccresume <id> — the morning-after command. execs into claude --resume <conversation_id> in the job's working directory, so you drop straight back into last night's conversation.

Working today (milestones M1 and M2). Every row is also reachable as a subcommand of windowsill, which is the form cron and the job chain use:

Command Also Behaviour
ccsub <file> windowsill sub Submit. - reads stdin. Prints job id. --now bypasses --at
ccstat windowsill stat id, name, state, model, when, cwd, elapsed, plus window state and 30d count
ccstat -a windowsill stat -a Include terminal jobs from the last 7 days
ccdel <id> windowsill del Cancel; SIGTERM the process group if running
ccresume <id> windowsill resume exec into claude --resume <conversation_id> in the job's cwd
windowsill show <id> Directives, resolved argv, state, conversation id, Δ%, permission denials, transcript tail
windowsill log <id> [-f] Stream the transcript; -f follows a running job
windowsill hold <id> / windowsill release <id> Toggle held
windowsill usage [--probe] Cached window state; --probe reads /usage fresh (max once per 10s)
windowsill report [--since 24h] Digest of what ran: state, duration, Δ%, and a ccresume line each
windowsill tick Internal; cron and chain entry point

Times are printed in your machine's own local time with no zone attached — 2026-07-28 02:00. Pass --with-timezone to any of these for the offset-bearing ISO form instead.

Designed but not implemented yet — do not rely on these:

Command Intended behaviour
windowsill sub -e $EDITOR on a template header; submit on save
windowsill gc [--older-than 30d] Remove terminal job dirs and worktrees
windowsill doctor Environment/config sanity check

Correspondingly, the two things those cover — --isolate worktree and the dirty-tree refusal — are part of the design but not yet functional. Everything else is live: window policies decide dispatch from a /usage probe, rate limits requeue instead of failing, --retry retries, and finished jobs notify.

Notifications

Off until you ask for them, and windowsill will not invent an address to send to.

# ~/.windowsill/config.toml
[notify]
default = "email"          # or "ntfy", or "none" (the default)

[notify.email]
to       = "you@your-real-domain"
sendmail = "/usr/sbin/sendmail"

[notify.ntfy]
url   = "https://ntfy.sh"
topic = "your-private-topic"

There is no default recipient and no default ntfy topic. A channel that is on with nowhere to send to sends nothing and records a notify_skipped event — it will not fall back to a placeholder, and it will not guess from $USER and your hostname. A single job can carry its own destination with #$ --notify email:me@example.com, which is the whole configuration if you only want mail from one job. windowsill show <id> prints the address a job would actually reach.

Similar tools

Running Claude Code unattended is a small, crowded field. What separates these tools isn't the queue — it's what triggers a dispatch.

  • claude-code-queuetriggered by hitting a limit. Runs, gets rate-limited, waits out the reset, retries. Markdown files with YAML frontmatter, priorities, a prompt bank, and a /queue skill it installs into Claude Code so the assistant can suggest queueing on its own. The closest neighbour by far; see below.
  • jshchnz/claude-code-schedulertriggered by the wall clock. A Claude Code plugin rather than a standalone CLI: you install it from the marketplace and schedule in natural language ("every weekday at 9am review yesterday's code"). Optional git worktree mode commits to a branch so your working tree stays clean — the pattern --isolate worktree follows. The most established of these by some margin.
  • dnvriend/claude-code-schedulertriggered by the wall clock, plus interval and file-watch. Unrelated to the above despite the identical name. A PyQt6 desktop app with a REST API on :5679, a Job→Task→Run hierarchy, and profiles for AWS Bedrock and Z.AI.
  • gruckion/claude-schedulertriggered by the wall clock, one-shot. Small, but it captures the conversation id and hands you click-to-resume notifications — the ergonomics ccresume is aiming at.
  • Claude Code's own schedulingtriggered by the wall clock. /loop with the Cron* tools schedules within a session; the desktop app's scheduled tasks use a SKILL.md with YAML frontmatter, which is Anthropic's own take on a job file and worth reading before changing design/02-job-file.md.

Every one of these except claude-code-queue dispatches on a clock. A clock knows what time it is; it doesn't know whether you have a window open.

claude-code-queue is the closest neighbour, and the overlap is real: Markdown job files with a directive header, a queue, retries, and an awareness that the 5-hour window is the thing that actually hurts. The difference is which direction the tool faces. claude-code-queue is reactive — it exists so that hitting a limit doesn't block you, and it waits a window out. windowsill is opportunistic — it exists to spend windows that would otherwise expire unused, and it treats capacity as a budget rather than as free-until-blocked.

Three things follow from that, none of which have a counterpart above:

  • Window state is read, not inferred. A /usage probe (design/04-usage-oracle.md) is free and answers before dispatch, which is what makes --window fresh and --window offpeak expressible at all. Learning a window's state by crashing into it can only ever produce retry logic.
  • Anchors. An anchor doesn't create capacity, it moves a boundary — and it fires conditionally, no-opping if a window is already open. See design/05-windows.md §5.2 for why a fixed cron time can't do this.
  • A ceiling. 30-day rolling window accounting against a configured monthly limit.

See design/index.md for the full design, including the execution model, window accounting, and SQLite schema.

Project details


Download files

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

Source Distribution

windowsill-0.1.0.tar.gz (171.0 kB view details)

Uploaded Source

Built Distribution

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

windowsill-0.1.0-py3-none-any.whl (88.7 kB view details)

Uploaded Python 3

File details

Details for the file windowsill-0.1.0.tar.gz.

File metadata

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

File hashes

Hashes for windowsill-0.1.0.tar.gz
Algorithm Hash digest
SHA256 581466e5e8d4b550193275e62df39abcb639a164da43d4f2065eb06ce02cef53
MD5 3c777fddc567cd50b9e422c56f3f8c79
BLAKE2b-256 8cbda0aa57904065122637fd9b98cc8011ad33ff4b198d8019d46eb9c7f3bcaa

See more details on using hashes here.

Provenance

The following attestation bundles were made for windowsill-0.1.0.tar.gz:

Publisher: pypi.yaml on watercrossing/windowsill

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

File details

Details for the file windowsill-0.1.0-py3-none-any.whl.

File metadata

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

File hashes

Hashes for windowsill-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 13b070e8685b626a9561fab1897523494a5fc0a87c019fc13b748252c66af873
MD5 b4865464cef98c58580c127accc7c6d9
BLAKE2b-256 b567028c775036d18cea3ed17da8a9b0c9279e2c67e53a913d7ac446c15bacfd

See more details on using hashes here.

Provenance

The following attestation bundles were made for windowsill-0.1.0-py3-none-any.whl:

Publisher: pypi.yaml on watercrossing/windowsill

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 Pingdom Monitoring Sentry Error logging StatusPage Status page