← Back to the collection

runway

Know how much session you have left before Claude Code tells you the hard way.

Two budgets end a Claude Code session: the subscription quota (the 5-hour window, plus weekly buckets) and the context window. Neither is visible while you work. You find out about the first one when a request fails with HTTP 429 halfway through an edit, and about the second when a compaction eats the plan you were following.

runway reads both on every session start and decides what to do about it: stay quiet, warn once, or, near a quota limit, keep working with a wake-up armed for the reset so the same session carries on after it.

What it does

Install

claude plugin marketplace add jtsternberg/claude-plugins
claude plugin install runway@jtsternberg
claude plugin install delayed-work@jtsternberg   # the wake-up at the reset

Restart Claude Code, because hooks load at startup. That is the whole install: the plugin ships its hooks in hooks/hooks.json, so there is no installer to run.

Claude Code only. Codex has no session hooks and no quota transcript for the script to read.

The skill still answers on demand without any hooks. They are what makes the check automatic instead of something you have to remember to ask for.

What the hooks do

Event Command Why
SessionStart runway.py --brief opening snapshot
UserPromptSubmit runway.py --brief --throttle 900 periodic re-check
PostModelSwitch runway.py --record-model prints nothing; records the model you switched to, which tells the script whether the window is 200k or 1M

The prompt hook re-checks at most every 15 minutes while things are clean, and on every prompt once a budget is tight. Once every tight quota bucket has its wake-up armed or acknowledged, it goes back to the 15-minute pace and stops asking Claude to invoke the skill each prompt.

Standalone skill copy

Copying skills/runway/ into your skills folder instead of installing the plugin gives you the files without hooks/hooks.json. Wire the hooks once with the bundled installer, run from that copy:

python3 <skill-dir>/scripts/install-hooks.py                 # wire all three hooks
python3 <skill-dir>/scripts/install-hooks.py --check         # is it current?
python3 <skill-dir>/scripts/install-hooks.py --dry-run       # show the JSON, write nothing
python3 <skill-dir>/scripts/install-hooks.py --no-prompt-hook   # skip the prompt re-check
python3 <skill-dir>/scripts/install-hooks.py --uninstall     # remove every runway hook
python3 <skill-dir>/scripts/install-hooks.py --settings ./.claude/settings.json  # per project

<skill-dir> is wherever you put the copy. The installer merges the three entries above into ~/.claude/settings.json, pointing at runway.py in that copy, and leaves every other hook alone. Re-running it replaces the runway entries instead of duplicating them, and it copies the settings file to settings.json.bak-<timestamp> before writing. The commands it writes run through python3, so they keep working when a copy loses the execute bit.

Forgetting the installer is recoverable: in a standalone copy, any run of runway.py without --brief notices the hooks are missing or out of date and wires them itself, so asking the skill anything installs it. Pass --no-self-install to suppress it. Inside the plugin this never happens, and the installer refuses to install there, since the plugin’s own hooks already run.

Updating and repairing a standalone copy

Every hook command the installer writes carries --hooks-version N. An update that changes the hooks raises that number, so an install from an older copy shows up as out of date. Three things check it:

The repair process after any update:

  1. Run python3 <skill-dir>/scripts/install-hooks.py. Claude can do this when it sees the runway SETUP: line.
  2. If Claude is not allowed to run it, or it fails with a permission error, run it yourself by typing ! python3 <skill-dir>/scripts/install-hooks.py in the Claude Code prompt. The skill tells Claude to ask you for this instead of looking for a workaround.
  3. Restart Claude Code.
  4. Confirm with python3 <skill-dir>/scripts/install-hooks.py --check.

A hook failing with Permission denied on runway.py runs the script without python3. The same repair process rewrites it.

The plugin install needs none of this: its hooks ship in hooks/hooks.json, so the session-start hook never prints a runway SETUP: line there.

Moving from a standalone copy to the plugin

Remove the standalone copy’s hooks, or every runway hook runs twice. Its installer’s --uninstall strips every hook in ~/.claude/settings.json whose command names runway.py, whichever copy wrote it, and leaves other hooks alone. With the standalone copy already deleted, run the plugin’s copy instead; it edits only the settings file, so the plugin’s own hooks stay:

python3 <skill-dir>/scripts/install-hooks.py --uninstall

Inside the plugin, install-hooks.py --check names any runway entries still in the settings file.

Use it

python3 <skill-dir>/scripts/runway.py            # full report
python3 <skill-dir>/scripts/runway.py --brief    # the hook line
python3 <skill-dir>/scripts/runway.py --json     # raw figures
python3 <skill-dir>/scripts/runway.py --limit 200000   # force a window size

<skill-dir> is skills/runway inside the installed plugin, or your standalone copy. In a session you rarely need the path: ask the skill, and Claude runs the script from where it lives.

The remaining flags are there for the hooks and the skill rather than for you:

An armed or acknowledged bucket stays quiet until its recorded wake time passes, so a reset time that drifts by a minute between checks does not re-ask.

Quota (enterprise / default_claude_max_5x)
  session                [#############-----------]  56.0%  resets 22:40 (in 2h 16m)
  weekly_scoped:Fable    [##----------------------]  10.0%  resets 21:00 (in 144h 36m)  (Fable only - not this session)

Context window (claude-opus-5[1m])
  used                   [##----------------------]   8.2%  81,865 / 1,000,000
  free 918,135 tokens   cache-read 75,446, cache-write 6,417, fresh input 2
  session 11991bbf-5962-4e6e-a1ad-00ca83545494

Verdict: ok

In conversation, just ask: “how much runway is left?”, or “am I about to get rate limited?”.

Thresholds

Percent used. The defaults fire late on purpose, so the skill keeps quiet until things are actually tight.

Budget notice wrap_up stop
Context window 80 90 95
Quota bucket 90 95 98

Override any level with an environment variable, either in the env block of ~/.claude/settings.json or in your shell:

RUNWAY_CONTEXT_NOTICE   RUNWAY_CONTEXT_WRAP_UP   RUNWAY_CONTEXT_STOP
RUNWAY_QUOTA_NOTICE     RUNWAY_QUOTA_WRAP_UP     RUNWAY_QUOTA_STOP
RUNWAY_RESET_SOON_MINUTES   # refill-is-close window, default 10
RUNWAY_CONTEXT_LIMIT        # context window size, when the model guess is wrong
RUNWAY_STOP_WORK            # set to pause until the reset at quota wrap_up/stop instead of working to the 429

CLAUDE_CODE_MAX_CONTEXT_TOKENS is read too, since Claude Code’s own override should not have to be repeated here. RUNWAY_CONTEXT_LIMIT wins when both are set.

How it reads the numbers

Quota comes from GET https://api.anthropic.com/api/oauth/usage, authorised with the OAuth token Claude Code already stores: the macOS Keychain item Claude Code-credentials, or ~/.claude/.credentials.json on other platforms. The script only reads, and sends nothing anywhere else.

Context comes from the usage block on the newest assistant message in ~/.claude/projects/<slug>/<session-id>.jsonl. Input plus cache-read plus cache-write tokens is the context the model just saw.

The window size comes from the model. The resolved model id, the one carrying a [1m] marker, reaches the script only through the hook payload on stdin: model on SessionStart, to_model on PostModelSwitch. It is cached per session in ~/.claude/runway/state.json, because the transcript records the plain API id without that marker. The script takes the first answer it can get, in this order: --limit, RUNWAY_CONTEXT_LIMIT, CLAUDE_CODE_MAX_CONTEXT_TOKENS, the model from the hook payload or the cache, the transcript id, the model setting, then 1M. A 200k family such as Haiku gets 200k and an unknown model gets 1M. If the measured usage already exceeds whatever it settled on, the script widens the window to 1M.

Tests

python3 -m unittest discover -s plugins/runway/skills/runway/tests   # from the repo root

The suite runs on the standard library’s unittest, with no dependencies to install. In-process cases point the scripts at a temporary home and stub the credential read wherever they reach collect(). Subprocess cases run under a temporary HOME that holds no credentials, so a test run cannot reach the network.

Limits worth knowing