Skip to main content

Ceiling Guard

Don't lose another agent run to a plan limit.

Ceiling Guard shows how much of your Claude Code, Codex or Grok subscription window is left, warns your agent before it runs out, and writes a handoff for the next run. It reads usage the official CLIs already record on your machine — no provider credentials, no proxy, no account access.

Codex and Grok usage appears immediately. Claude publishes its percentage only during a non-interactive run, so wrap one Claude command to start tracking it.

Zero dependencies. Python 3.11+. Linux, macOS, Windows, or a container.

pipx install ceiling-guard        # or: python3 -m pip install --user ceiling-guard

cg quota                                    # read-only, nothing to configure
cg wrap -- claude -p "continue the migration"

If pipx is not installed, the pip --user form above works and needs nothing else. Measured on a machine with neither: eleven seconds from no install to a true reading.

cg wrap runs your command and observes it; it terminates nothing unless you separately arm the daemon. For claude -p it appends --output-format stream-json --verbose and renders the answer back to plain text, because that event stream is the only place Claude's percentage exists — it is never written to disk. It says so on stderr rather than rewriting your command line silently. Every other command is passed through as you typed it.

Put it where you already look, instead of remembering to ask:

cg statusline                      # -> cg 48%   (or "cg -" when it cannot see)

# bash/zsh prompt
PS1='$(cg statusline) '"$PS1"

# tmux
set -g status-right '#(cg statusline)'

statusline reads no config, contacts no daemon and always exits 0, so it cannot break the prompt it is added to. It prints - rather than a stale percentage: a number that was true an hour ago is the failure this tool exists to prevent.

PROVIDER   WINDOW         USED  RESETS               AGE  STATUS
openai     primary       97.0%  Sat 11:19            2m   ok
grok       weekly        63.0%  Sat 23:21           23m   ok
anthropic  five_hour     94.0%  Sat 12:30            0m   ok     <- after one wrapped Claude job
           seven_day     73.0%  Mon 02:00            0m   ok

A browser-only subscription is not a user of this tool: the reading comes from what a CLI writes locally, and a browser writes nothing here.

Those numbers come from files the official clients already write, and from stream events passing through cg wrap. Each adapter was checked against the provider's own usage screen; the working is in docs/quota-accuracy.md.

What you will actually see

Those three sentences are the claim. This table is the evidence for them, and it belongs here rather than in a footnote.

you cg quota shows
use the codex or grok CLI their windows immediately. Those clients write usage to disk themselves.
use the claude CLI nothing, until you wrap one session
use claude.ai or ChatGPT in a browser nothing, ever

Runs hosted inside another vendor's client spend that vendor's pool, not your provider's. Cursor is the common case: CG does not see that ceiling. The measurement is in docs/quota-accuracy.md.

Claude is the awkward case. It does publish a percentage, but only as a rate_limit_event inside its stream-json output, and it never writes that to disk. Polling a file will not find it. The number has to be caught in flight:

cg wrap -- claude -p "whatever you were going to run anyway"
cg quota                           # the anthropic rows are there now

The substitution is only applied when the child is claude, run with -p, and you have not chosen a format yourself. A wrapper script under another name will not be recognised, and that is deliberate: guessing at what a binary is would be worse than missing one.

cg wrap terminates nothing, and says so on its first line every time.

It is not a transparent sleeve like timeout or nice, and it should not claim to be. For claude -p specifically it adds --output-format stream-json --verbose, because that is the only form in which the percentage exists, then prints the answer back as plain text so the run looks the same to you. It tells you it did that. An explicit --output-format of your own wins, an interactive claude with no -p is left alone, and no other program is ever rewritten.

Browser sessions leave no local record at all. If that is how you use your subscription, this tool has nothing to read and you should stop here.

Warn the job before the window closes

Reading the number is useful on its own. The reason it exists is what comes next: telling a running job that it is nearly out of room, while there is still room to do something about it.

from continuity_guard import guard

# `overnight` asks for termination, but asking is not enough: the daemon is in
# shadow mode until an operator arms it, so this snippet observes and nothing
# else. Two separate keys, deliberately.
with guard("nightly-refactor", profile="overnight") as s:
    for step in agent.run():
        s.progress(step=step.name, tool=step.tool, args=step.args,
                   tokens=step.tokens, depth=step.depth)
        if s.should_wrap_up():          # cheap, non-blocking, False by default
            save_state(s.headroom())    # what to save is yours to decide
            break

Every signalled session also gets a handoff written for it, whether or not it answers. That matters because the sessions most worth preserving are usually the ones too wedged to preserve themselves. What that document does and does not preserve is set out in what the handoff is and is not.

The watchdog, when you want it

Twelve detectors cover the agent failures that return HTTP 200 while nothing useful happens: loops, oscillation, pace collapse, token burn, runaway recursion, crash loops.

It ships observe-only. Nothing is terminated until you have watched it against your own traffic and decided to arm it.

cg init && cg install              # scaffold config, run as a service
cg status                          # how close each session is to firing, now

The order of this page is the order we suggest you adopt it. Reading the quota costs you nothing and needs no permission. The wrap-up signal costs one line in your loop. Kill authority comes last, because it is the only part that can take something away from you.


Plan windows, and not losing a run to one

The reading is the easy half. This section is the mechanics: how the number is kept honest, and what happens to a live session when the window runs low.

A stale reading is refused rather than trusted. Once, a Grok cache read 13.0% while the account was actually at 63%, from a file five days old. That is the most dangerous state a meter can be in, because it looks like knowledge. Any snapshot older than an hour is marked IGNORED and cannot trigger anything, and an unknown age counts as stale.

At wrapup_at, which defaults to 95% of the binding window, every live session is told to save its state. A handoff is written for each of them whether or not they respond. A wedged agent cannot act on the signal, and that is when the record is worth the most.

# `overnight` asks for termination, but asking is not enough: the daemon is in
# shadow mode until an operator arms it, so this snippet observes and nothing
# else. Two separate keys, deliberately.
with guard("nightly-refactor", profile="overnight") as s:
    for step in agent.run():
        s.progress(...)
        if s.should_wrap_up():          # cheap, non-blocking
            write_handoff(s.headroom()) # what to save is yours to decide
            break

The handoff Ceiling Guard writes itself is a record of what it observed: steps, tool calls, timings, tokens, the plan window at the time. Not a summary of intent; the agent did not write it. If your agent also acts on the signal you get a better one on top.

Ceiling signals go to the three ceiling conditions (a plan window, a budget, a deadline) and never to faults. Nothing is wrong with a session that is merely running out; a wedged one cannot answer anyway.

What the handoff is and is not

You always know what a run reached, including when the agent died without cooperating. Enumerable progress survives: which items are done, which remain, and the command that resumes past them. When the work is a list, and a great deal of agent work is a list, that is most of what you needed. The next run picks up without repeating itself.

What does not survive is the agent's understanding. The half-formed hypothesis, the reason approach A was abandoned, the thing it had noticed but not yet put into words: that lives in the model's context and goes when the context goes. Writing it down is lossy, and the loss is invisible in the result, because you cannot tell from a summary what is missing from it. This is the context-compaction problem and nothing here solves it.

Call it a warm start rather than a continuation. A successor reads the handoff and begins informed instead of blind. It does not resume mid-thought.

Two consequences are worth planning around.

Task shape decides how much this is worth to you. Enumerable, independent units hand off well. Exploratory debugging hands off badly, because its value was never in the enumeration; it was in the hypothesis space, which is the part that does not survive.

Findings also have a shelf life. In one measured case, two of four findings in a handoff had already been fixed a few hours later, and a third was wrong on its own terms. A document describing a moving codebase decays. Handoffs carry an observed-at stamp so a reader can judge for themselves, which is mitigation rather than a fix.


Shadow mode

The daemon ships observe-only. With shadow = true it evaluates every contract and logs every action it would have taken, and kills nothing.

Run it that way against real workloads first, and measure your own false-positive rate. Nobody should grant kill authority to a new watchdog before they have seen that number, and shadow mode produces the only metric worth having: N sessions observed, zero healthy terminations.

python3 -m continuity_guard.daemon -c config.toml            # config decides
python3 -m continuity_guard.daemon -c config.toml --shadow   # force observe-only
python3 -m continuity_guard.daemon -c config.toml --armed    # force enforcement

Notifications

Every sink declares which levels and detectors it wants.

[[notifications]]
kind = "file"
path = "~/continuity-guard-alerts.log"
on   = ["warn", "hard", "dead"]

[[notifications]]
kind    = "exec"                         # event passed as CG_* env vars
command = "~/bin/cg-notify.sh"
on      = ["hard", "dead"]

[[notifications]]
kind = "webhook"
url  = "http://127.0.0.1:9000/cg"
on   = ["hard", "burn", "repeat"]

A broken sink is logged and swallowed. Notification failure must never stop the daemon doing its actual job.


Accuracy

tests/bench_accuracy.py drives the real detector code against a simulated clock, which gets thousands of sessions in seconds where the realtime soak manages about a dozen in ten minutes. The healthy workloads are adversarial by design. Each one is a legitimate pattern chosen because it looks like a specific fault.

Healthy archetype Looks like Why it is legitimate
polling repeat re-reads the same status file forever, while progressing
pipeline cycle read → edit → test, a new file each pass
flaky errors 30% of tool calls genuinely fail (empty greps, 404s)
bursty decay fast bursts split by 4-minute thinking pauses
rate_limited decay periodic provider backoffs
build decay 22% of steps take 30–150s
long_tool silence single 20-minute test-suite runs

Current numbers on the archetypes above (default profile):

overall any-level FP   0.0%      hard-level FP   0.0%
loop 100%  cycle 100%  stall 100%  burn 100%  errors 100%
depth 100%  decay 100%  budget 100%  cold_freeze 100%  cold_stall 100%

That figure is not universal, and the qualifier is the honest part. Three workload shapes are known to produce false positives and are excluded from the gate, measured and published rather than deleted (tests/bench_accuracy.py --known-fail):

shape any-level FP hard-level FP detector
polls one endpoint forever 100% 0% repeat
alternates between two queues 100% 100% cycle
fast setup, then long work 100% 0% decay

Together those three measure any-level FP 25.0%, hard-level FP 8.33%, min recall 92% (tests/bench_accuracy.py --known-fail, which reports and exits 0 rather than gating).

The second is a real hard-level false positive on healthy work: an agent doing poll(queue-a), poll(queue-b) with seq advancing every frame — real, reported progress — reaches a hard cycle verdict on the sixth frame. That is what a queue worker, a CI watcher and a deploy monitor look like.

The default action for repeat and cycle is notify, so nothing is terminated out of the box. Under a terminating profile it would be. If your agent polls, set repeat.action and cycle.action to notify explicitly, or raise their thresholds.

No fix is offered, because a healthy periodic poller and a pathological loop emit identical frames indefinitely and no finite confirmation window separates them. See docs/quota-accuracy.md. The corpus missed this for a long time because the archetype named polling did not actually poll — its docstring claimed it did.

Against real traffic, not just the corpus

The numbers above come from an adversarial corpus: synthetic sessions with known ground truth, which is what makes false-positive rates measurable at all. That is the right instrument for tuning and the wrong one for the question anyone actually asks, which is whether it will kill their work.

So it also runs in shadow mode against real agent traffic, on two hosts (a Raspberry Pi 5 and an x86-64 NUC) across four provider paths: the claude CLI under cg wrap, and Grok, OpenRouter and local models through cg-proxy. Shadow mode records the verdict and takes no action, so every session is a free observation of what an armed daemon would have done.

As of 2026-08-29, since the harness fix in 63d4bc8:

observed healthy would have been terminated
real model sessions 629 475 0
including synthetic soak 3,470 0

Seven distinct real models. The one healthy session ever flagged in this pipeline (real-1787852493-clean, a Claude-Haiku repo-QA run) was a harness defect, not a detector one: the test loop's regex did not capture tool input, so every call hashed identically and repeat fired correctly on what it was shown. It is annotated in the ledger rather than deleted, and 63d4bc8 fixed the loop.

That table is a false-positive measurement, not a recall one. Faults in the real loop are injected against short sessions that often end before a detector's window elapses, so the catch rate there says nothing useful and is not quoted; recall comes from the corpus and the matrix, where the ground truth is exact.

The honest summary is that in roughly 630 real sessions, across seven models and four provider paths, an armed daemon would have terminated nothing healthy. The run is still accumulating, so treat that as a floor rather than a final number. It is also one operator's machines. It is not a substitute for measuring your own traffic before you arm anything.

Three things that only showed up at these sample sizes, all now fixed:

The decay hard level needed its own floor. When a session's learned baseline is small, floor dominates both decay levels and warn_k/hard_k collapse to the same threshold, so any qualifying slow run fires straight to hard. On build traffic that was a 1.2% hard-level false positive at --trials 200+ (invisible at 60). decay.hard_floor (default 200s) is now an absolute backstop above the slowest gap healthy heavy-tailed traffic produces; warn keeps the relative floor. Recall unchanged.

silence and decay need opposite statistics from the same data. Decay asks "has the typical pace collapsed?", which is a central quantile. Silence asks "how long may this agent legitimately go quiet?", which is the upper tail. Sharing one baseline made bursty workloads false-positive at 94%, because a median baseline calls the pauses anomalies when the pauses are the workload.

Before the first completed step there is no information at all. Firing a warning there is guessing, and it is how a 20-minute-per-call agent gets flagged in its first three minutes. Warnings are now suppressed until a session has produced one step; the hard ceiling still applies, widened.

Read next

The front page stops here on purpose. Everything below was in it and is now one click away, because none of it belongs before the install line.

Why a heartbeat is not enough what a liveness ping cannot see, and why there are twelve detectors
Tuning profiles, thresholds, adaptive baselines, and the sweeps behind the defaults
Integrating four paths, from zero-code wrapping to the Python API
Safety properties the invariants, and what each one prevents
The socket is kill authority the local threat model
Test matrix the 26 rows and what each proves
Testing with agents running the suite against real CLI agents
Accuracy, in full every measurement, including the withdrawn ones

Install as a service

sudo mkdir -p /opt/continuity-guard /etc/continuity-guard
sudo cp -r continuity_guard /opt/continuity-guard/
sudo cp config.example.toml /etc/continuity-guard/config.toml
# the unit keeps its old name: renaming it would make `cg install` enable a
# second daemon alongside one already running and holding the socket
sudo cp systemd/continuity-guard.service /etc/systemd/system/
sudo systemctl enable --now continuity-guard
systemctl status continuity-guard

The unit sets Restart=always and OOMScoreAdjust=-500: the init system is what watches the watchdog, and the daemon must outlive the memory pressure that kills what it watches.


Status

v0.5.3, reference implementation. 26/26 matrix checks across 25 rows — 18 realtime checks, a protocol fuzz, a ceiling-signal check, an authority check, a packaging check, an environment-knob check, and a wrapper output/signal check — passing on both architectures on this commit: Raspberry Pi 5 (aarch64, Python 3.13.5) and an x86-64 host (Python 3.12.3). See docs/platform-support.md.

Pure Python so the contract semantics and thresholds can be validated fast on real workloads. The wire protocol (continuity_guard/protocol.py, plus sanitize() as its acceptance rule) is the durable artifact; the native daemon reimplements against it once the semantics are proven here.

See docs/ for the engineering history and the measurement behind each decision.

MIT. Ak2tx LLC.

Download files

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

Source Distribution

ceiling_guard-0.5.4.tar.gz (303.5 kB view details)

Uploaded Source

Built Distribution

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

ceiling_guard-0.5.4-py3-none-any.whl (111.5 kB view details)

Uploaded Python 3

File details

Details for the file ceiling_guard-0.5.4.tar.gz.

File metadata

  • Download URL: ceiling_guard-0.5.4.tar.gz
  • Upload date:
  • Size: 303.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.5

File hashes

Hashes for ceiling_guard-0.5.4.tar.gz
Algorithm Hash digest
SHA256 d6e36d6cf5d78b8870a7bc10b6c34ba36faf9acead90c5c6a734f7706364a71e
MD5 f622c9bca6886210e213515670cd094f
BLAKE2b-256 bb654e1fa87fb52a2b58d6e47adcf6da3ce14c84c67a541d5fa7565004fb74d5

See more details on using hashes here.

File details

Details for the file ceiling_guard-0.5.4-py3-none-any.whl.

File metadata

  • Download URL: ceiling_guard-0.5.4-py3-none-any.whl
  • Upload date:
  • Size: 111.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.5

File hashes

Hashes for ceiling_guard-0.5.4-py3-none-any.whl
Algorithm Hash digest
SHA256 311e78d651ca7cb3e891551be670d67e41dc488eed7a314f5de784da29b81532
MD5 6f61ee17e02f9d12dc5cf0f4e1a28001
BLAKE2b-256 7ec9f9ddcc1b3ed9225b9176f4d84c03b932f14bcdc13a1a29a6525dd139ab6c

See more details on using hashes here.

Release history Release notifications | RSS feed

0.5.7

2 files

0.5.6

2 files

This release

0.5.4 This release

2 files

0.5.3

2 files

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