[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M71J6JW7SAY9PDKAWHG3SJCR":3,"skill-same:01M71J6JW7SAY9PDKAWHG3SJCR":82},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":15,"categories":16,"keywords":17,"publishedAt":38,"linkOnly":39,"markdownContent":40,"markdownBody":41,"frontMatter":42,"refName":45,"commitSha":46,"skillDirectory":47,"forksCount":13,"repositoryHtmlUrl":48,"repositoryHomepageUrl":6,"ownerHtmlUrl":49,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":50,"install":51,"resources":63},"01M71J6JW7SAY9PDKAWHG3SJCR","usage-guard",null,"Monitors Claude Code's Usage Limit (5-hour = Current \u002F weekly = Weekly), pauses work when it exceeds 95%, and automatically resumes once the reset restores headroom. Bundles an engine form that a caller such as drive Reads at a checkpoint, and a standalone form, `\u002Fusage-guard \"\u003Ccontinuation command>\"`, that guards arbitrary work. Claude-only (depends on the OAuth usage-rate endpoint + ScheduleWakeup).","ozzy-labs\u002Fskills","ozzy-labs","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F246109093?v=4","01M6H0M2K2PZXHWNHJJTWY20FY","01M71J2B6FZ5KPEXPR5VS1TX7Z",0,"MIT","https:\u002F\u002Fgithub.com\u002Fozzy-labs\u002Fskills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fusage-guard\u002FSKILL.md",[],[18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37],"usage guard","guard","usage","limit","form","continuation","as","endpoint","status","rate","at","command","work","reset","code","when","guards","only","standalone","weekly","2026-11-10 10:12:30",false,"---\nname: usage-guard\ndescription: Monitors Claude Code's Usage Limit (5-hour = Current \u002F weekly = Weekly), pauses work when it exceeds 95%, and automatically resumes once the reset restores headroom. Bundles an engine form that a caller such as drive Reads at a checkpoint, and a standalone form, `\u002Fusage-guard \"\u003Ccontinuation command>\"`, that guards arbitrary work. Claude-only (depends on the OAuth usage-rate endpoint + ScheduleWakeup).\nuser-invocable: true\nargument-hint: \"\u003Ccontinuation command> (leave blank to just check status)\"\ndisable-model-invocation: true\n---\n\n# usage-guard - Usage Limit pause\u002Fresume engine\n\nWhen Claude Code's Usage Limit reaches 100%, the session is interrupted. This skill provides a mechanism that pauses work just before 100% (default 95%) and automatically resumes once the reset restores headroom.\n\n> **Claude-only**: Gated with `adapters: claude-code` because it depends on the OAuth usage-rate endpoint (the token in `~\u002F.claude\u002F.credentials.json`) and `ScheduleWakeup` (not distributed to Codex \u002F Gemini \u002F Copilot).\n>\n> **Self-contained document**: This SKILL.md does not Read other files (the canonical ones under `.agents\u002Fskills\u002F...`). Because the gate means the codex adapter's `.agents\u002F` output doesn't exist here, the full procedure is embedded in this document.\n\n## Obtaining the signal: the usage-check script\n\nThe deterministic part of the decision is handled by `usage-check.mjs`. It is bundled directly under the skill directory — at `~\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-check.mjs` in user-scope, and at `.claude\u002Fskills\u002Fusage-guard\u002Fusage-check.mjs` in dogfood. **At execution time, run the `usage-check.mjs` in the same directory as this SKILL.md** via Bash:\n\n```bash\nnode ~\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-check.mjs\n```\n\n> When running in dogfood (inside the skills\u002Fcommons repo), run `.claude\u002Fskills\u002Fusage-guard\u002Fusage-check.mjs` at the repo root. In either environment, this refers to \"the `usage-check.mjs` at the same level as this SKILL.md.\"\n\n## Environment requirements (the endpoint path must be usable)\n\nThe **canonical decision path** for `usage-check.mjs` is the OAuth usage-rate endpoint (`source: \"endpoint\"`). To use it, the Bash that runs the script must be permitted to do the following two things:\n\n- **(a) Egress to api.anthropic.com**: it hits `GET https:\u002F\u002Fapi.anthropic.com\u002Fapi\u002Foauth\u002Fusage`. If the harness's sandbox \u002F network permissions **block** sending to this host, the endpoint path fails.\n- **(b) Reading `~\u002F.claude\u002F.credentials.json`**: it reads `claudeAiOauth.accessToken` every time. If the permissions allowlist doesn't allow reading this file, the token can't be obtained and the endpoint path fails.\n\n**What happens if these aren't allowed**: if the endpoint fails, it degrades to the JSONL fallback (a rough estimate) → and in the worst case, to **fail-open** (`source: \"fail-open\"`, `ok: true`), which can leave **the guard effectively OFF without anyone noticing**. In practice, there have been cases where the relevant request from Bash was consistently denied by the sandbox \u002F permission gate, making the endpoint unusable.\n\n**How to allow it**:\n\n- network: allow egress to `api.anthropic.com` in the harness's settings (if using a sandbox, add this host to the allowlist).\n- permissions: add Read access to `~\u002F.claude\u002F.credentials.json` and permission to run the `node` process that launches `usage-check.mjs` as allowlist entries in settings permissions.\n- After configuring, run `node ...\u002Fusage-check.mjs` once and confirm the output JSON's `\"source\"` is **`endpoint`** (or `cache` within the immediately following TTL). If it stays `jsonl` \u002F `fail-open`, one of (a) or (b) above is still blocked.\n\n> **fail-open is a degraded signal**: When `source !== \"endpoint\"` (especially `fail-open`), the wait-loop \u002F standalone form \u002F PreToolUse hook issue a **degradation warning** (while still allowing). The drive caller should leave this degradation in its report (\"⚠️ usage-guard degraded: source=fail-open, not actually monitoring\").\n\n### Behavior\n\n- Re-reads `claudeAiOauth.accessToken` from `~\u002F.claude\u002F.credentials.json` **every time** (accounting for `expiresAt` expiry)\n- Fetches `five_hour` \u002F `seven_day` `utilization` and `resets_at` in one call via `GET https:\u002F\u002Fapi.anthropic.com\u002Fapi\u002Foauth\u002Fusage` (headers `Authorization: Bearer` \u002F `anthropic-beta: oauth-2025-04-20` \u002F `User-Agent: claude-code\u002F\u003Cversion>`)\n- A 30–60s local cache (`~\u002F.claude\u002Fusage-guard\u002Fcache.json`, not placed under `.claude\u002Fskills\u002F`) prevents rapid repeated calls. Shares the same cache as the PreToolUse hook in `#123`\n- On endpoint failure, a JSONL fallback estimates the 5h \u002F 7d window from the per-message `usage` + timestamp in `~\u002F.claude\u002Fprojects\u002F*\u002F*.jsonl`\n- **fail-open** (`ok: true`) if **both** the endpoint and JSONL fail, + a warning to stderr (so the guard never hard-stops on its own bug)\n- Adds a **post-reset buffer** (default +300 seconds) to `wait_seconds` when over the threshold. This absorbs the delay in server-side `utilization` propagation and the jitter in ScheduleWakeup firing, preventing re-entry into headroom that's cut exactly at the reset boundary and immediately tripping again (see \"Threshold\" \u002F \"Behavior: resume buffer\" below)\n- **Reflection-lag detection** (`suspected_reflection_lag`): right after a reset, the server-side `utilization` can sometimes return **an afterimage of the previous window** (e.g., 5h util 100% even though it has already reset). If the over-threshold window is **right at the boundary** (elapsed time since the window started, `elapsed = period - (resets_at - now)`, is under epsilon=900 seconds), this is treated as a contradiction, and `wait_seconds` switches from the full-window value to a **short recheck interval** (default 180 seconds) instead. This result is not written to cache (see \"Behavior: reflection-lag detection\" below)\n\n### Output JSON\n\n```json\n{\n  \"five_hour\": { \"utilization\": 0, \"resets_at\": \"...\" },\n  \"seven_day\": { \"utilization\": 0, \"resets_at\": \"...\" },\n  \"ok\": true,\n  \"wait_seconds\": 0,\n  \"resets_at\": null,\n  \"resume_buffer_seconds\": 300,\n  \"suspected_reflection_lag\": false,\n  \"resume_plan\": null,\n  \"source\": \"endpoint\"\n}\n```\n\n- `ok`: `true` if **`utilization + headroom` for both windows** is under the threshold (`headroom` default 0, in which case this means both windows' `utilization` is under the threshold; see \"dispatch headroom\" below)\n- `wait_seconds`: seconds until the **latest** `resets_at` (the window that resets last) among the over-threshold windows, **plus `resume_buffer_seconds`**. `0` when `ok`. **When `suspected_reflection_lag` is `true`**, this degrades to the short recheck interval (`USAGE_GUARD_LAG_RECHECK_SECONDS`, default 180 seconds)\n- `resets_at`: the `resets_at` of that latest over-threshold window (**left unchanged at the window boundary** — neither the buffer nor the lag degradation is applied). `null` when `ok`. The expected resume time = `resets_at + resume_buffer_seconds`, kept distinct\n- `resume_buffer_seconds`: the post-reset buffer seconds folded into `wait_seconds` (default 300; 0 restores prior behavior)\n- `suspected_reflection_lag`: `true` if reflection lag is suspected (over-threshold right at the boundary). When `true`, `wait_seconds` becomes the short recheck interval, and this result is **not written to cache** (so the next check can fetch the real endpoint value immediately). Always `false` when `ok`. Set on both the endpoint and jsonl paths\n- `resume_plan` (`#212`): the **deterministic resume plan** the caller should execute, or `null` when `ok`. `{ trigger, wait_seconds, fire_at }` — see \"Resume plan\" below. This moves the \"choosing a resume trigger\" decision from prose into the engine\n- `source`: `endpoint` \u002F `jsonl` \u002F `cache` \u002F `fail-open`\n\n### Resume plan (deterministic trigger, `#212`)\n\nThe purpose of usage-guard is to avoid \"stopped → reset arrives → still stopped\". The *stop* half is deterministic (the hook denies); the **resume half used to be prose** the model had to interpret. `usage-check.mjs` now emits a `resume_plan` so the caller **executes a plan** instead:\n\n```json\n\"resume_plan\": { \"trigger\": \"cron-oneshot\", \"wait_seconds\": 12900, \"fire_at\": \"2026-07-13T02:15:00.000Z\" }\n```\n\n- `trigger` selection is deterministic (see [ADR-0002](..\u002F..\u002F..\u002Fdocs\u002Fadr\u002F0002-usage-guard-resume-arming.md)):\n  - `suspected_reflection_lag` → **`short-recheck`** (re-check at the short interval; never pin a long trigger on a boundary afterimage)\n  - `context: \"durable\"` → **`cron-routine`** (a cloud Routine that survives a 100% hard-interrupt — `#213`)\n  - `context: \"orchestration\"`, or `wait_seconds > 3600` (the ScheduleWakeup per-call cap) → **`cron-oneshot`** (durable one-shot)\n  - otherwise → **`schedule-wakeup`** (in-session heartbeat)\n- `fire_at` = the planned resume wall-clock (`now + wait_seconds`, i.e. `resets_at + buffer` for a genuine overage).\n- `context` is set via CLI `--context orchestration|durable` or env `USAGE_GUARD_RESUME_CONTEXT`; default (unset) derives purely from `wait_seconds`.\n- **Mapping to host tools**: `short-recheck` \u002F `schedule-wakeup` → `ScheduleWakeup(min(wait_seconds, 3600))`; `cron-oneshot` → a durable `CronCreate` (`recurring:false`) at `fire_at`; `cron-routine` → a cloud Routine at `fire_at` (`#213`). The engine cannot call these host tools itself (`.mjs` limitation) — it emits the plan; the caller executes it.\n\n### Durable resume that survives a hard-interrupt (`cron-routine`, `#213`)\n\n`ScheduleWakeup` and a session-scoped `CronCreate` are **in-session** primitives: a genuine 100% **hard-interrupt** freezes the session and neither auto-fires across it. The only trigger that survives is a **cloud Routine** (scheduled deployment), which runs on session-independent infrastructure. Select it with `context: \"durable\"` (`--context durable` \u002F `USAGE_GUARD_RESUME_CONTEXT=durable`), which emits `trigger: \"cron-routine\"`.\n\n- **Pre-arm, don't react**: because a hard-interrupt kills the session *before* a reactive pause can arm anything, a durable resume must be armed **before** the risky run (at job start), scheduled to fire at `fire_at`. drive orchestration \u002F long jobs are the intended callers (`--context orchestration` gates dispatch; `durable` additionally pre-arms a Routine).\n- **Fire-after-reset guard**: a Routine firing at or before the reset is rejected (`session_rate_limited_error`). `fire_at = resets_at + resume_buffer` already lands in the fresh window; callers can assert this with `firesAfterReset(plan, resets_at)` before creating the Routine (it catches a `resume_buffer=0` misconfig \u002F clock skew — delay to `resets_at + buffer` on `false`).\n- **Idempotent continuation required**: the continuation must detect partial progress and be safe to re-run (drive already is — it detects an existing PR\u002Fbranch). A spurious fire (limit never hit) is then a harmless no-op re-run.\n- **Fallback**: where the host `\u002Fschedule` (Routines) is unavailable (e.g. a headless\u002Fcron run), fall back to the in-session plan (`cron-oneshot`) and emit a degradation warning — a durable path that cannot be armed must not silently drop the resume.\n\n### Behavior: resume buffer\n\nWhen an over-threshold state is detected, `resume_buffer_seconds` (default 300 seconds) is added to `wait_seconds`. This extends the wait from `resets_at` (the window boundary) to `resets_at + buffer`, avoiding the server-side reflection delay right after a reset and allowing a clean, single re-entry into the new window (in practice, \"reset + a few minutes\" achieves a clean resume).\n\n- `resets_at` is **left unchanged at the window boundary**. The expected resume time is expressed as `resets_at + resume_buffer_seconds`\n- No buffer is added when `ok` (not over threshold) (`wait_seconds: 0`)\n- The PreToolUse hook's deny hint (\"N minutes left\") is also derived from `wait_seconds`, so it is presented with the buffer folded in, keeping things consistent\n\n### Behavior: reflection-lag detection (avoiding false negatives right at the boundary)\n\nRight after a reset, the server-side `utilization` can sometimes drag along **an afterimage of the previous window** (e.g., the 5h window resets at 21:00, but the endpoint at 21:05 still returns util 100%, while `resets_at` already points to the next boundary at 02:00 — a contradictory state). Treating this state at face value produces a false negative that \"wastes ~5h leaving an already-recovered window idle.\" `resume_buffer` (`#129`) is a countermeasure premised on \"the reset hasn't arrived yet,\" and doesn't help in this case.\n\nDetection principle: for each over-threshold window, compute the **elapsed time since the window started**, `elapsed = period - (resets_at - now)` (period = 18000 seconds for `five_hour`, 604800 seconds for `seven_day`). If `elapsed` is under epsilon (`USAGE_GUARD_LAG_EPSILON_SECONDS`, default 900 seconds) and it's over threshold, this is a **contradiction** → set `suspected_reflection_lag = true` as a suspected reflection lag.\n\n- When lag is suspected, `wait_seconds` becomes the **short recheck interval** (`USAGE_GUARD_LAG_RECHECK_SECONDS`, default 180 seconds) instead of full-window+buffer. The caller should not pin a long-lived CronCreate right at the boundary, but instead re-fetch at this short interval to pick up the real value once the lag clears\n- `resets_at` is **left unchanged at the window boundary** (no information is discarded)\n- A suspected-lag result is **not written to cache** (this prevents the previous window's false 100% from being pinned in the cache for the whole TTL and suppressing rechecks — the next check fetches the real endpoint value immediately)\n- When `ok` (under threshold), no lag judgment is made and `suspected_reflection_lag` is always `false`\n\n### Behavior: dispatch headroom (countering wave overshoot, `#141`)\n\n**The problem (in-wave overshoot)**: in orchestration, wave \u002F worker dispatch commits to **N non-interruptible units** (such as heavy workers) at the moment of launch, and those **consume headroom while running**. If a boundary checkpoint gates dispatch based only on \"**current** `utilization` vs. threshold,\" it cannot account for a wave's worth of projected consumption, and the wave can blow past the threshold and hit 100% mid-run. `ok:false` is only detected **after the fact** at the next boundary, and can't prevent that wave's overshoot (observed in practice: `five_hour` 86% → 98% over one wave, with 3 parallel workers). Simply lowering the threshold can't account for concurrency \u002F worker weight either (starting at 86% with `threshold=90` still dispatches, since 86\u003C90, then overshoots).\n\n**Countermeasure**: gate `evaluate()`'s trip decision on the **projected post-dispatch value**, not the **current value**. It accepts a `headroom` (in percentage points, default 0), and the trip condition per window becomes `utilization + headroom >= threshold`. The dispatch checkpoint passes a reservation proportional to concurrency (`--concurrency`) as `headroom`, and **pauses before dispatch** if `util + reserve(N) >= threshold`.\n\n- `headroom` **only affects the threshold comparison**. The computation of `wait_seconds` \u002F `resets_at` \u002F reflection lag is **unchanged** and does not scale with the size of headroom (the wait for a tripped window is always window-boundary + buffer only; different headroom values that trip the same window produce the same wait)\n- The default `headroom=0` is prior behavior (gate on the current value). **Backward compatible**. The single-mode Phase 1 \u002F review-loop checkpoint stays at headroom=0\n- **Does not cross the shared cache**: `headroom > 0` (dispatch checkpoint) **neither reads nor writes** `~\u002F.claude\u002Fusage-guard\u002Fcache.json` (computed with headroom=0 and shared with the `#123` hook). Otherwise it could either mistakenly return a projected-overshoot `ok:true` for headroom=0 (disabling the gate), or contaminate the hook path with a headroom-overshoot `ok:false`. Results with `headroom > 0` always have a `source` other than `cache`\n- Negative or non-numeric values are clamped to 0 (a misconfiguration never loosens the guard below the current-value behavior)\n- Resolution order is **CLI `--headroom \u003Cpct>` > env `USAGE_GUARD_DISPATCH_HEADROOM` > default 0**\n\n```bash\nnode ~\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-check.mjs --headroom 12   # gate on the projected value for this checkpoint only\nUSAGE_GUARD_DISPATCH_HEADROOM=12 node ~\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-check.mjs   # override the default via env\n```\n\n> **Difference from the stopgap**: lowering `USAGE_GUARD_THRESHOLD=90` (or 85) is effective as a stopgap mitigation for now, but since it's **static and can't account for concurrency \u002F worker weight**, it's not a substitute for headroom-aware dispatch. Threshold lowering is the stopgap, headroom is the real countermeasure — the two play different roles.\n>\n> **A boundary checkpoint alone cannot fully prevent in-wave overshoot**. Headroom is a preventive measure that \"stops dispatch on a projection,\" but an overshoot mid-run (mid-unit), once a worker has already launched, isn't stopped at the boundary. The reliable ceiling during a run is owned by the **PreToolUse hook** described below (`#123`). The two are combined as defense in depth (boundary pause + mid-unit ceiling).\n\n### Threshold\n\nDefault 95%. Overridable via the environment variable `USAGE_GUARD_THRESHOLD` (e.g. `USAGE_GUARD_THRESHOLD=80`).\n\nDispatch headroom defaults to 0 (percentage points). Overridable via the environment variable `USAGE_GUARD_DISPATCH_HEADROOM` (e.g. `USAGE_GUARD_DISPATCH_HEADROOM=12`); CLI `--headroom \u003Cpct>` takes precedence over env. Negative, non-numeric, or empty values fall back to the default 0 (see \"Behavior: dispatch headroom\" above).\n\nThe post-reset resume buffer defaults to 300 seconds. Overridable via the environment variable `USAGE_GUARD_RESUME_BUFFER_SECONDS` (e.g. `USAGE_GUARD_RESUME_BUFFER_SECONDS=600`). `0` restores prior behavior (resuming exactly at `resets_at`). Negative or non-numeric values fall back to the default 300.\n\nThe reflection-lag detection thresholds can also be overridden via env:\n\n- `USAGE_GUARD_LAG_EPSILON_SECONDS` (default 900): the elapsed-seconds threshold for treating something as \"right at the boundary.\" An over-threshold window with `elapsed` smaller than this is treated as suspected lag. Negative, non-numeric, or empty values fall back to the default 900\n- `USAGE_GUARD_LAG_RECHECK_SECONDS` (default 180): the short recheck interval used when lag is suspected. Values `\u003C= 0`, non-numeric, or empty fall back to the default 180 (preventing a busy-loop)\n\n## Lightweight wait-loop (shared logic)\n\nThe core stop\u002Fresume logic shared by both forms:\n\n1. Run `usage-check.mjs` to get the JSON\n2. **Degradation check**: if `source !== \"endpoint\"` (`cache` is excluded, since it originates from the endpoint), issue a degradation warning. In particular, when `source === \"fail-open\"`, **explicitly surface** \"⚠️ usage-guard degraded: source=fail-open, not actually monitoring\" **to the caller \u002F user**, and the drive caller should leave this in its report (allow \u002F progress is still maintained; see § Environment requirements)\n3. If `ok`, **proceed normally** (run the continuation command \u002F the caller moves to the next checkpoint)\n4. If `ok` is `false`, choose a resume trigger (see \"Choosing a resume trigger\" below) and **wait**\n   - **When reflection lag is suspected (`suspected_reflection_lag: true`), recheck at short intervals**. Since `wait_seconds` has already degraded to the short recheck interval (default 180 seconds), don't pin a long-lived one-shot CronCreate at the boundary — instead wait just this short interval via `ScheduleWakeup(wait_seconds)` etc. and hit `usage-check.mjs` again. Once the lag clears, the next check will pick up the real value (typically `ok: true`) and continue (avoiding a false negative that leaves things idle for ~5h on the previous window's afterimage)\n   - On a normal over-threshold, `resume_buffer_seconds` (default 300) is already folded into `wait_seconds`, so the wait extends to `resets_at + buffer`\n   - **Does not re-enter while waiting** (consumes no budget at all)\n5. On waking, run `usage-check.mjs` again, and repeat 2–5 until `ok` (since a suspected-lag result is never written to cache, the recheck fetches the real endpoint value immediately)\n6. Once `ok`, proceed to the continuation command\n\n> `wait_seconds` is not second-precise, since it's computed from `resets_at` (+ buffer). ScheduleWakeup's firing is also somewhat delayed by its floor + overhead (in practice, ~110s for a 60s request). This is precise enough for waiting out a reset.\n\n### Choosing a resume trigger\n\nThere are 2 mechanisms for resuming after a wait (heartbeat \u002F waking). Use whichever fits the situation:\n\n| Situation | Resume trigger |\n|---|---|\n| \u002Floop dynamic, in-session, wait **≤1h** | `ScheduleWakeup(min(wait_seconds, 3600))`. Capped at 3600s per call. If `wait_seconds` is longer, recheck across multiple calls |\n| Non-\u002Floop orchestration (Agent tool \u002F Workflow drive), wait **>1h**, needs restart resilience | Set a `CronCreate` (`recurring: false`, durable) at **`resets_at + resume_buffer_seconds`**, and re-submit the continuation command when it fires |\n\n- **ScheduleWakeup**: suited to an in-session heartbeat. Since one call is capped at 3600s, a long wait becomes multi-stage. When `wait_seconds > 3600`, repeat `min(wait_seconds, 3600)`.\n- **CronCreate one-shot**: fires once by wall clock and is restart-resilient. Robust for `>3600s` and non-\u002Floop cases (Agent tool orchestration, etc.). Set the firing time to `resets_at + resume_buffer_seconds` (the resume time derived from `wait_seconds`). A one-shot (`recurring: false`) **auto-deletes after firing**. In practice, when running a ~72-minute wait via Agent tool orchestration, CronCreate one-shot proved more robust than a multi-stage ScheduleWakeup.\n\n## Pending-resume marker + SessionStart detection (`#212`)\n\nThe engine emits a `resume_plan`, but it cannot *call* the host scheduling tool, so it cannot **guarantee** the arm. A marker + a SessionStart check is the code-side net that **detects a missed resume** (the exact \"stuck after reset\" failure) instead of leaving it silent.\n\n- **When a caller pauses**, it writes the marker via `resume-marker.mjs` — `writeResumeMarker({ continuation, fire_at, trigger })` → `~\u002F.claude\u002Fusage-guard\u002Fresume-pending.json` (atomic temp→rename). `continuation` is the idempotent command to re-run (e.g. `\u002Fdrive #212-213 --merge`); `fire_at` is `resume_plan.fire_at`.\n- **On a successful resume**, the caller clears it: `clearResumeMarker()`.\n- **SessionStart hook `resume-check.mjs`** reads the marker at session start and surfaces:\n  - **OVERDUE** — `fire_at` has already passed but the marker was never cleared → \"a paused resume is OVERDUE — re-run `\u003Ccontinuation>`\". This is the failure usage-guard exists to prevent, now made visible.\n  - **pending** — paused, `fire_at` not yet reached (informational).\n  - read-only, fail-open (never blocks a session start).\n\n### Wiring the SessionStart hook (optional, recommended)\n\nAdd one SessionStart entry to `~\u002F.claude\u002Fsettings.local.json` (absolute path; same M3 caveat as the PreToolUse hook):\n\n```json\n{\n  \"hooks\": {\n    \"SessionStart\": [\n      {\n        \"hooks\": [\n          { \"type\": \"command\", \"command\": \"node \u002Fhome\u002F\u003Cyou>\u002F.claude\u002Fskills\u002Fusage-guard\u002Fresume-check.mjs\" }\n        ]\n      }\n    ]\n  }\n}\n```\n\n## Usage form 1: engine form (the caller Reads it)\n\nA caller such as drive (`#122`) Reads this SKILL.md at the **boundary of a resumable unit (checkpoint)** and runs the wait-loop above.\n\n### Checkpoint convention\n\n- Always pause at a **boundary that can be cleanly re-entered**. Never pause mid-implement (e.g., before creating a PR)\n- The caller runs usage-check at the **entry** of each unit, and enters the wait-loop if not `ok`\n- The continuation command is **supplied by the caller**. Since drive resumes idempotently (detecting an existing PR and resuming from Phase 3), the re-run after waiting is reused as-is as the resume mechanism (e.g., `\u002Fdrive \u003Cargs>` — since drive's usage-guard is ON by default, don't append `--usage-guard` to the continuation command)\n- In drive's orchestration mode, this is called at the granularity of wave boundaries. An over-threshold state within a running worker is caught as a mid-unit ceiling by the PreToolUse hook in `#123`\n\n## Usage form 2: standalone form `\u002Fusage-guard \"\u003Ccontinuation command>\"`\n\nGuards any long-running work with auto pause\u002Fresume, independent of drive (user-invocable).\n\n### Arguments\n\n- Interprets `$ARGUMENTS` as the **continuation command**\n- **If blank, it's a status check only**: runs `usage-check.mjs`, displays the current `five_hour` \u002F `seven_day` `utilization` and `ok` \u002F `wait_seconds`, then exits\n\n### Steps\n\n1. Run `usage-check.mjs`\n2. **Degradation check**: if `source !== \"endpoint\"` (excluding `cache`), show a degradation warning to the user. When `fail-open`, explicitly state \"not actually monitoring\" (progress is still maintained; see § Environment requirements)\n3. If **both windows are `ok`**, run the continuation command (proceed normally)\n4. If **over threshold**:\n   - Choose a resume trigger (see § Lightweight wait-loop, \"Choosing a resume trigger\"). For an in-session wait ≤1h, use `ScheduleWakeup(min(wait_seconds, 3600))`; for >1h and non-\u002Floop, set a `CronCreate` (`recurring: false`) at `resets_at + resume_buffer_seconds` and wait (don't re-enter while waiting; `wait_seconds` already has the buffer folded in)\n   - On waking (recovery detected), **self-re-enter `\u002Fusage-guard \"\u003Ccontinuation command>\"`**\n   - Repeat the heartbeat until `ok`, then run the continuation command\n\n### Idempotency of the continuation command\n\nThe continuation command is treated as **idempotent by assumption**. It is the user's responsibility to ensure it's safe to re-run across a wait (produces no duplicate side effects \u002F detects progress and can resume partway through).\n\n- Since drive is inherently idempotent (detecting an existing PR \u002F branch and resuming), it can be safely wrapped as `\u002Fusage-guard \"\u002Fdrive #123\"` (drive's usage-guard is ON by default)\n- When wrapping general long-running work (a build, a batch job, etc.), confirm for yourself that the design is safe to re-run\n\n### Example usage\n\n```text\n\u002Fusage-guard \"\u002Fdrive #123\"\n\u002Fusage-guard \"\"                 # status check only\nUSAGE_GUARD_THRESHOLD=80 \u002Fusage-guard \"\u003Ccontinuation command>\"   # temporarily set the threshold to 80%\nUSAGE_GUARD_RESUME_BUFFER_SECONDS=600 \u002Fusage-guard \"\u003Ccontinuation command>\"   # set the resume buffer to 10 minutes\nUSAGE_GUARD_RESUME_CONTEXT=durable \u002Fusage-guard \"\u002Fdrive #123\"   # arm a durable cloud Routine (survives a hard-interrupt, #213)\n```\n\n> **`--durable` \u002F `context: durable` (`#213`)**: selects the `cron-routine` trigger so the resume survives a 100% hard-interrupt (a cloud Routine, not an in-session wakeup). Since a hard-interrupt kills the session before a reactive pause can arm anything, prefer arming the durable Routine **at the start** of a long\u002Finterruptible run, and ensure the continuation is idempotent (drive is). Falls back to `cron-oneshot` + a warning where `\u002Fschedule` is unavailable.\n\n## Enabling the PreToolUse hook (recommended in combination)\n\nThis skill's stop granularity is the boundary of a resumable unit. Whereas drive's usage-guard checkpoint (`#122`, ON by default via `#130`) can only stop at unit boundaries, the PreToolUse hook (`usage-guard-hook.mjs`) is a mid-unit ceiling that takes effect **before every tool call**, stopping things on the spot even if the threshold is crossed partway through a long unit. The two are combined with a division of labor:\n\n| Mechanism | Granularity | Where it stops |\n|---|---|---|\n| drive usage-guard checkpoint (`#122` \u002F `#130`, ON by default) | Resumable unit boundary | Cleanly re-enterable checkpoints such as a Phase entry \u002F wave entry \u002F before worker dispatch |\n| PreToolUse hook (this section) | Per tool call (including inside subagents) | Mid-unit (in-flight ceiling) |\n\n> **For drive orchestration, wiring the hook as the primary ceiling is recommended by default (`#141`)**: a boundary checkpoint structurally cannot prevent **in-wave overshoot** (a group of heavy workers, once dispatched, consumes headroom while running and can't be stopped at a boundary before hitting 100% — see \"Behavior: dispatch headroom\" above). Dispatch-headroom is a preventive measure that stops dispatch on a projection, but **this hook, which fires before tool calls inside subagents, is the only reliable ceiling** during a run. Previously, the **pre-`#139`** hook could only be \"recommended in combination\" due to the risk of hard-stopping on a transient anomalous reading, but **that hard-stop risk has been resolved by `#139`** (file kill-switch \u002F reflection-lag ALLOW \u002F debounce \u002F spike rejection), so in environments running orchestration (`\u002Fdrive`'s wave parallelism), it's fine to **wire the `matcher:\"*\"` hook by default**. Any accidental deny can be released instantly with `touch ~\u002F.claude\u002Fusage-guard\u002FDISABLE` (see § Disabling). Only by pairing defense in depth — **boundary resumable pause (headroom-aware) + the hook's mid-unit ceiling** — is overshoot practically closed off.\n>\n> The hook first reads the **same cache** that `usage-check.mjs` writes (`~\u002F.claude\u002Fusage-guard\u002Fcache.json`, 30–60s TTL). On a hot cache it just returns that, so it doesn't spam the endpoint on every tool call. **On a cold\u002Fstale cache, the hook itself falls back to `getUsage` (cache-first), hits the endpoint just once, and writes the result back to the cache (self-sustaining)**. Since `getUsage` writes to the cache using the real fs even for callers that don't inject an fs, the TTL-window fetch converges to just one call even in standalone operation without `usage-check.mjs` having run beforehand (the hook path itself keeps the cache warm). Above the threshold, it denies (exit 2) + presents `resets_at` as `HH:MM`; if usage can't be read, it fails open (allow + stderr warning). Calls originating from a subagent are distinguished in the log via the payload's `agent_id`.\n>\n> **Degradation visibility**: when usage's `source` is anything other than `endpoint` \u002F `cache` (especially `fail-open`), the hook keeps allowing while issuing a degradation warning to stderr (\"⚠️ usage-guard DEGRADED: source=fail-open …\"). This is so that the endpoint path being unusable — i.e., the guard effectively being OFF — is never missed. See § Environment requirements for the cause and how to allow it.\n\n### Resilience to transient anomalous readings (`#139`)\n\nThe hook has built-in countermeasures against an incident (`#139`) where **a single transient over-threshold reading** — such as a reflection-lag afterimage right at a reset boundary, or a one-off endpoint spike — caused the hook to deny every tool, hard-stopping the session even against edits that would remove the guard. All of these are in service of \"never hard-stop on our own bug (fail-open)\":\n\n- **(a) file kill-switch (an escape hatch)**: if `~\u002F.claude\u002Fusage-guard\u002FDISABLE` exists, the hook immediately becomes a no-op at the very start (allow + warning). Since this preserves an escape hatch while keeping `matcher:\"*\"` intact, it doesn't lose its role as a mid-unit ceiling monitoring tool calls inside subagents. It can be released\u002Fre-enabled with `touch`\u002F`rm` from a `!` shell, instantly within the session with no config edit needed (see § Disabling).\n- **(d) reflection lag is ALLOWed**: when usage has `suspected_reflection_lag === true` (a previous-window afterimage right at the boundary), the hook lets it through. If it's a genuine over-threshold, the resumable-unit boundary checkpoint will stop it separately.\n- **(b) debounce**: doesn't deny on a single over-threshold reading — only denies once it's exceeded for **N consecutive reads** (default 2, overridable via `USAGE_GUARD_DEBOUNCE_COUNT`). Even a single under-threshold reading in between resets the consecutive count. The consecutive count and the most recent good reading are kept in a **per-origin** state file (see (f) below).\n- **(c) spike rejection**: a physically implausible jump from a recent good reading (threshold − `USAGE_GUARD_SPIKE_DELTA`, default under 25) to over threshold within `USAGE_GUARD_SPIKE_WINDOW_SECONDS` (default 120 seconds) is treated as suspect and ALLOWed + warned.\n- **(e) cache hygiene for over-threshold readings**: `usage-check.mjs` caches an over-threshold reading with a shortened TTL (10s, vs. the normal 45s). This avoids pinning a transient overshoot for the full TTL and re-verifies it early on the next check.\n- **(f) concurrency-safe state (`#211`)**: under Workflow fan-out (up to 16 concurrent subagents, each firing the hook before every tool call) the debounce\u002Fspike counter must not be corrupted or lost. Two measures ensure this: **(i) per-origin state files** — the counter lives at `~\u002F.claude\u002Fusage-guard\u002Fhook-state.json` for the main session and at `hook-state.\u003Cagent_id>.json` per subagent, so concurrent hooks (always from *different* origins) never share one file and a read-modify-write can't lose an update; **(ii) atomic writes** — both the state file and the shared `cache.json` are written to a temp sibling and `rename`d into place, so a concurrent reader never observes a torn\u002Fhalf-written JSON. Both bias the guard toward *holding* (never silently dropping a deny) under exactly the high-concurrency load where overshoot is most likely.\n\nA soft-allow via lag\u002Fspike\u002Fdebounce issues a degradation warning to stderr (so the guard doesn't just look like it's OFF).\n\n### Mechanism and placement\n\nThe hook itself is bundled as an extra file directly under the skill directory (same path as `usage-check.mjs`). Since this repo does **not** distribute settings\u002Fhooks (the build only outputs skills \u002F agents), enabling it is a **manual opt-in**.\n\n### settings.local.json snippet (`update-config` style)\n\nAdd one PreToolUse hook entry to `~\u002F.claude\u002Fsettings.local.json` (settings reload mid-session, so no restart is needed):\n\n```json\n{\n  \"hooks\": {\n    \"PreToolUse\": [\n      {\n        \"matcher\": \"*\",\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"node \u002Fhome\u002F\u003Cyou>\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-guard-hook.mjs\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\n> **M3: fill in the hook script path as an absolute path by hand.** Since skill-dir-relative references don't work inside settings, `command` must be an **absolute path**. The path varies by environment:\n>\n> - **user-scope** (placed via `npx @ozzylabs\u002Fskills install`): `~\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-guard-hook.mjs` (`~` is not expanded, so write it out in full as `\u002Fhome\u002F\u003Cyou>\u002F.claude\u002F...`)\n> - **dogfood** (run inside the skills\u002Fcommons repo): `\u003Crepo>\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-guard-hook.mjs` (e.g. `\u002Fhome\u002F\u003Cyou>\u002Fgithub\u002Fozzy-labs\u002Fskills\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-guard-hook.mjs`)\n>\n> Both refer to \"the `usage-guard-hook.mjs` at the same level as `usage-check.mjs`.\" Confirm your own environment's full path before filling it in. If overriding the threshold, also attach the env on the settings side (e.g. `\"command\": \"USAGE_GUARD_THRESHOLD=80 node \u002Fhome\u002F\u003Cyou>\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-guard-hook.mjs\"`).\n\n**`matcher` defaults to `\"*\"` (all tools).** This lets it function as a mid-unit ceiling that also monitors tool calls inside subagents. Since a hard-stop from a transient anomalous reading can be released via the **file kill-switch** (§ Disabling) without narrowing `matcher`, it's fine to keep operating with `matcher:\"*\"`. If you really want to limit monitoring to only heavy tools (weakening subagent monitoring), you can instead scope it to something like `\"matcher\": \"Bash|Workflow|Task\"`, always letting `Edit` \u002F `Read` \u002F `AskUserQuestion` through (optional).\n\n### Disabling\n\nTwo ways. **Use the kill-switch when you need something instant with no config edit**:\n\n- **file kill-switch (recommended, instant within the session, `#139` (a))**: creating `~\u002F.claude\u002Fusage-guard\u002FDISABLE` makes the hook an immediate no-op at the start. Since it can be run from a `!` shell, it can be released even in a situation where the hook has wrongly denied on a transient anomalous reading and even stopped the very edit meant to remove the guard:\n\n  ```bash\n  touch ~\u002F.claude\u002Fusage-guard\u002FDISABLE   # disable (instant)\n  rm    ~\u002F.claude\u002Fusage-guard\u002FDISABLE   # re-enable (instant)\n  ```\n\n- **Removing from settings (permanent)**: removing the above PreToolUse entry from `~\u002F.claude\u002Fsettings.local.json` disables it instantly (mid-session reload).\n\n## Notes\n\n- The guard **never hard-stops on its own bug**: if signal acquisition fails entirely, it fails open and continues work (`source: \"fail-open\"` + stderr warning)\n- However, **fail-open means the guard is OFF**. When `source !== \"endpoint\"`, each form issues a degradation warning — if you see one, restore the endpoint path per § Environment requirements\n- **Watch for false denies from transient anomalous readings (`#139`)**: the PreToolUse hook can falsely deny on a transient over-threshold reading, such as a reflection-lag afterimage or an endpoint spike. Against this, the hook is resilient via (d) lag ALLOW \u002F (b) debounce \u002F (c) spike rejection, and as a last resort can be released instantly within the session with no config edit via the **file kill-switch** (`touch ~\u002F.claude\u002Fusage-guard\u002FDISABLE`) (see § Resilience to transient anomalous readings \u002F § Disabling)\n- **Watch for in-wave overshoot in orchestration (`#141`)**: a boundary checkpoint cannot stop an overshoot occurring during a wave after dispatch. This is closed off via two layers — **dispatch-headroom** (stops dispatch on a projection; see § Behavior: dispatch headroom) and the **PreToolUse hook** (a mid-unit ceiling during a run; see § PreToolUse hook). Lowering the threshold is a stopgap and not a substitute\n- Consumes no budget while waiting (doesn't re-enter — heartbeat only)\n- The resume after waiting extends to `resets_at + resume_buffer_seconds` (default +5 minutes), to prevent re-entering headroom cut exactly at the reset and immediately tripping again\n- A long wait is assumed to be absorbed by a live session (WSL + always-on). For >1h and non-\u002Floop cases, a CronCreate one-shot (durable) is more robust (see § Lightweight wait-loop)\n","# usage-guard - Usage Limit pause\u002Fresume engine\n\nWhen Claude Code's Usage Limit reaches 100%, the session is interrupted. This skill provides a mechanism that pauses work just before 100% (default 95%) and automatically resumes once the reset restores headroom.\n\n> **Claude-only**: Gated with `adapters: claude-code` because it depends on the OAuth usage-rate endpoint (the token in `~\u002F.claude\u002F.credentials.json`) and `ScheduleWakeup` (not distributed to Codex \u002F Gemini \u002F Copilot).\n>\n> **Self-contained document**: This SKILL.md does not Read other files (the canonical ones under `.agents\u002Fskills\u002F...`). Because the gate means the codex adapter's `.agents\u002F` output doesn't exist here, the full procedure is embedded in this document.\n\n## Obtaining the signal: the usage-check script\n\nThe deterministic part of the decision is handled by `usage-check.mjs`. It is bundled directly under the skill directory — at `~\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-check.mjs` in user-scope, and at `.claude\u002Fskills\u002Fusage-guard\u002Fusage-check.mjs` in dogfood. **At execution time, run the `usage-check.mjs` in the same directory as this SKILL.md** via Bash:\n\n```bash\nnode ~\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-check.mjs\n```\n\n> When running in dogfood (inside the skills\u002Fcommons repo), run `.claude\u002Fskills\u002Fusage-guard\u002Fusage-check.mjs` at the repo root. In either environment, this refers to \"the `usage-check.mjs` at the same level as this SKILL.md.\"\n\n## Environment requirements (the endpoint path must be usable)\n\nThe **canonical decision path** for `usage-check.mjs` is the OAuth usage-rate endpoint (`source: \"endpoint\"`). To use it, the Bash that runs the script must be permitted to do the following two things:\n\n- **(a) Egress to api.anthropic.com**: it hits `GET https:\u002F\u002Fapi.anthropic.com\u002Fapi\u002Foauth\u002Fusage`. If the harness's sandbox \u002F network permissions **block** sending to this host, the endpoint path fails.\n- **(b) Reading `~\u002F.claude\u002F.credentials.json`**: it reads `claudeAiOauth.accessToken` every time. If the permissions allowlist doesn't allow reading this file, the token can't be obtained and the endpoint path fails.\n\n**What happens if these aren't allowed**: if the endpoint fails, it degrades to the JSONL fallback (a rough estimate) → and in the worst case, to **fail-open** (`source: \"fail-open\"`, `ok: true`), which can leave **the guard effectively OFF without anyone noticing**. In practice, there have been cases where the relevant request from Bash was consistently denied by the sandbox \u002F permission gate, making the endpoint unusable.\n\n**How to allow it**:\n\n- network: allow egress to `api.anthropic.com` in the harness's settings (if using a sandbox, add this host to the allowlist).\n- permissions: add Read access to `~\u002F.claude\u002F.credentials.json` and permission to run the `node` process that launches `usage-check.mjs` as allowlist entries in settings permissions.\n- After configuring, run `node ...\u002Fusage-check.mjs` once and confirm the output JSON's `\"source\"` is **`endpoint`** (or `cache` within the immediately following TTL). If it stays `jsonl` \u002F `fail-open`, one of (a) or (b) above is still blocked.\n\n> **fail-open is a degraded signal**: When `source !== \"endpoint\"` (especially `fail-open`), the wait-loop \u002F standalone form \u002F PreToolUse hook issue a **degradation warning** (while still allowing). The drive caller should leave this degradation in its report (\"⚠️ usage-guard degraded: source=fail-open, not actually monitoring\").\n\n### Behavior\n\n- Re-reads `claudeAiOauth.accessToken` from `~\u002F.claude\u002F.credentials.json` **every time** (accounting for `expiresAt` expiry)\n- Fetches `five_hour` \u002F `seven_day` `utilization` and `resets_at` in one call via `GET https:\u002F\u002Fapi.anthropic.com\u002Fapi\u002Foauth\u002Fusage` (headers `Authorization: Bearer` \u002F `anthropic-beta: oauth-2025-04-20` \u002F `User-Agent: claude-code\u002F\u003Cversion>`)\n- A 30–60s local cache (`~\u002F.claude\u002Fusage-guard\u002Fcache.json`, not placed under `.claude\u002Fskills\u002F`) prevents rapid repeated calls. Shares the same cache as the PreToolUse hook in `#123`\n- On endpoint failure, a JSONL fallback estimates the 5h \u002F 7d window from the per-message `usage` + timestamp in `~\u002F.claude\u002Fprojects\u002F*\u002F*.jsonl`\n- **fail-open** (`ok: true`) if **both** the endpoint and JSONL fail, + a warning to stderr (so the guard never hard-stops on its own bug)\n- Adds a **post-reset buffer** (default +300 seconds) to `wait_seconds` when over the threshold. This absorbs the delay in server-side `utilization` propagation and the jitter in ScheduleWakeup firing, preventing re-entry into headroom that's cut exactly at the reset boundary and immediately tripping again (see \"Threshold\" \u002F \"Behavior: resume buffer\" below)\n- **Reflection-lag detection** (`suspected_reflection_lag`): right after a reset, the server-side `utilization` can sometimes return **an afterimage of the previous window** (e.g., 5h util 100% even though it has already reset). If the over-threshold window is **right at the boundary** (elapsed time since the window started, `elapsed = period - (resets_at - now)`, is under epsilon=900 seconds), this is treated as a contradiction, and `wait_seconds` switches from the full-window value to a **short recheck interval** (default 180 seconds) instead. This result is not written to cache (see \"Behavior: reflection-lag detection\" below)\n\n### Output JSON\n\n```json\n{\n  \"five_hour\": { \"utilization\": 0, \"resets_at\": \"...\" },\n  \"seven_day\": { \"utilization\": 0, \"resets_at\": \"...\" },\n  \"ok\": true,\n  \"wait_seconds\": 0,\n  \"resets_at\": null,\n  \"resume_buffer_seconds\": 300,\n  \"suspected_reflection_lag\": false,\n  \"resume_plan\": null,\n  \"source\": \"endpoint\"\n}\n```\n\n- `ok`: `true` if **`utilization + headroom` for both windows** is under the threshold (`headroom` default 0, in which case this means both windows' `utilization` is under the threshold; see \"dispatch headroom\" below)\n- `wait_seconds`: seconds until the **latest** `resets_at` (the window that resets last) among the over-threshold windows, **plus `resume_buffer_seconds`**. `0` when `ok`. **When `suspected_reflection_lag` is `true`**, this degrades to the short recheck interval (`USAGE_GUARD_LAG_RECHECK_SECONDS`, default 180 seconds)\n- `resets_at`: the `resets_at` of that latest over-threshold window (**left unchanged at the window boundary** — neither the buffer nor the lag degradation is applied). `null` when `ok`. The expected resume time = `resets_at + resume_buffer_seconds`, kept distinct\n- `resume_buffer_seconds`: the post-reset buffer seconds folded into `wait_seconds` (default 300; 0 restores prior behavior)\n- `suspected_reflection_lag`: `true` if reflection lag is suspected (over-threshold right at the boundary). When `true`, `wait_seconds` becomes the short recheck interval, and this result is **not written to cache** (so the next check can fetch the real endpoint value immediately). Always `false` when `ok`. Set on both the endpoint and jsonl paths\n- `resume_plan` (`#212`): the **deterministic resume plan** the caller should execute, or `null` when `ok`. `{ trigger, wait_seconds, fire_at }` — see \"Resume plan\" below. This moves the \"choosing a resume trigger\" decision from prose into the engine\n- `source`: `endpoint` \u002F `jsonl` \u002F `cache` \u002F `fail-open`\n\n### Resume plan (deterministic trigger, `#212`)\n\nThe purpose of usage-guard is to avoid \"stopped → reset arrives → still stopped\". The *stop* half is deterministic (the hook denies); the **resume half used to be prose** the model had to interpret. `usage-check.mjs` now emits a `resume_plan` so the caller **executes a plan** instead:\n\n```json\n\"resume_plan\": { \"trigger\": \"cron-oneshot\", \"wait_seconds\": 12900, \"fire_at\": \"2026-07-13T02:15:00.000Z\" }\n```\n\n- `trigger` selection is deterministic (see [ADR-0002](..\u002F..\u002F..\u002Fdocs\u002Fadr\u002F0002-usage-guard-resume-arming.md)):\n  - `suspected_reflection_lag` → **`short-recheck`** (re-check at the short interval; never pin a long trigger on a boundary afterimage)\n  - `context: \"durable\"` → **`cron-routine`** (a cloud Routine that survives a 100% hard-interrupt — `#213`)\n  - `context: \"orchestration\"`, or `wait_seconds > 3600` (the ScheduleWakeup per-call cap) → **`cron-oneshot`** (durable one-shot)\n  - otherwise → **`schedule-wakeup`** (in-session heartbeat)\n- `fire_at` = the planned resume wall-clock (`now + wait_seconds`, i.e. `resets_at + buffer` for a genuine overage).\n- `context` is set via CLI `--context orchestration|durable` or env `USAGE_GUARD_RESUME_CONTEXT`; default (unset) derives purely from `wait_seconds`.\n- **Mapping to host tools**: `short-recheck` \u002F `schedule-wakeup` → `ScheduleWakeup(min(wait_seconds, 3600))`; `cron-oneshot` → a durable `CronCreate` (`recurring:false`) at `fire_at`; `cron-routine` → a cloud Routine at `fire_at` (`#213`). The engine cannot call these host tools itself (`.mjs` limitation) — it emits the plan; the caller executes it.\n\n### Durable resume that survives a hard-interrupt (`cron-routine`, `#213`)\n\n`ScheduleWakeup` and a session-scoped `CronCreate` are **in-session** primitives: a genuine 100% **hard-interrupt** freezes the session and neither auto-fires across it. The only trigger that survives is a **cloud Routine** (scheduled deployment), which runs on session-independent infrastructure. Select it with `context: \"durable\"` (`--context durable` \u002F `USAGE_GUARD_RESUME_CONTEXT=durable`), which emits `trigger: \"cron-routine\"`.\n\n- **Pre-arm, don't react**: because a hard-interrupt kills the session *before* a reactive pause can arm anything, a durable resume must be armed **before** the risky run (at job start), scheduled to fire at `fire_at`. drive orchestration \u002F long jobs are the intended callers (`--context orchestration` gates dispatch; `durable` additionally pre-arms a Routine).\n- **Fire-after-reset guard**: a Routine firing at or before the reset is rejected (`session_rate_limited_error`). `fire_at = resets_at + resume_buffer` already lands in the fresh window; callers can assert this with `firesAfterReset(plan, resets_at)` before creating the Routine (it catches a `resume_buffer=0` misconfig \u002F clock skew — delay to `resets_at + buffer` on `false`).\n- **Idempotent continuation required**: the continuation must detect partial progress and be safe to re-run (drive already is — it detects an existing PR\u002Fbranch). A spurious fire (limit never hit) is then a harmless no-op re-run.\n- **Fallback**: where the host `\u002Fschedule` (Routines) is unavailable (e.g. a headless\u002Fcron run), fall back to the in-session plan (`cron-oneshot`) and emit a degradation warning — a durable path that cannot be armed must not silently drop the resume.\n\n### Behavior: resume buffer\n\nWhen an over-threshold state is detected, `resume_buffer_seconds` (default 300 seconds) is added to `wait_seconds`. This extends the wait from `resets_at` (the window boundary) to `resets_at + buffer`, avoiding the server-side reflection delay right after a reset and allowing a clean, single re-entry into the new window (in practice, \"reset + a few minutes\" achieves a clean resume).\n\n- `resets_at` is **left unchanged at the window boundary**. The expected resume time is expressed as `resets_at + resume_buffer_seconds`\n- No buffer is added when `ok` (not over threshold) (`wait_seconds: 0`)\n- The PreToolUse hook's deny hint (\"N minutes left\") is also derived from `wait_seconds`, so it is presented with the buffer folded in, keeping things consistent\n\n### Behavior: reflection-lag detection (avoiding false negatives right at the boundary)\n\nRight after a reset, the server-side `utilization` can sometimes drag along **an afterimage of the previous window** (e.g., the 5h window resets at 21:00, but the endpoint at 21:05 still returns util 100%, while `resets_at` already points to the next boundary at 02:00 — a contradictory state). Treating this state at face value produces a false negative that \"wastes ~5h leaving an already-recovered window idle.\" `resume_buffer` (`#129`) is a countermeasure premised on \"the reset hasn't arrived yet,\" and doesn't help in this case.\n\nDetection principle: for each over-threshold window, compute the **elapsed time since the window started**, `elapsed = period - (resets_at - now)` (period = 18000 seconds for `five_hour`, 604800 seconds for `seven_day`). If `elapsed` is under epsilon (`USAGE_GUARD_LAG_EPSILON_SECONDS`, default 900 seconds) and it's over threshold, this is a **contradiction** → set `suspected_reflection_lag = true` as a suspected reflection lag.\n\n- When lag is suspected, `wait_seconds` becomes the **short recheck interval** (`USAGE_GUARD_LAG_RECHECK_SECONDS`, default 180 seconds) instead of full-window+buffer. The caller should not pin a long-lived CronCreate right at the boundary, but instead re-fetch at this short interval to pick up the real value once the lag clears\n- `resets_at` is **left unchanged at the window boundary** (no information is discarded)\n- A suspected-lag result is **not written to cache** (this prevents the previous window's false 100% from being pinned in the cache for the whole TTL and suppressing rechecks — the next check fetches the real endpoint value immediately)\n- When `ok` (under threshold), no lag judgment is made and `suspected_reflection_lag` is always `false`\n\n### Behavior: dispatch headroom (countering wave overshoot, `#141`)\n\n**The problem (in-wave overshoot)**: in orchestration, wave \u002F worker dispatch commits to **N non-interruptible units** (such as heavy workers) at the moment of launch, and those **consume headroom while running**. If a boundary checkpoint gates dispatch based only on \"**current** `utilization` vs. threshold,\" it cannot account for a wave's worth of projected consumption, and the wave can blow past the threshold and hit 100% mid-run. `ok:false` is only detected **after the fact** at the next boundary, and can't prevent that wave's overshoot (observed in practice: `five_hour` 86% → 98% over one wave, with 3 parallel workers). Simply lowering the threshold can't account for concurrency \u002F worker weight either (starting at 86% with `threshold=90` still dispatches, since 86\u003C90, then overshoots).\n\n**Countermeasure**: gate `evaluate()`'s trip decision on the **projected post-dispatch value**, not the **current value**. It accepts a `headroom` (in percentage points, default 0), and the trip condition per window becomes `utilization + headroom >= threshold`. The dispatch checkpoint passes a reservation proportional to concurrency (`--concurrency`) as `headroom`, and **pauses before dispatch** if `util + reserve(N) >= threshold`.\n\n- `headroom` **only affects the threshold comparison**. The computation of `wait_seconds` \u002F `resets_at` \u002F reflection lag is **unchanged** and does not scale with the size of headroom (the wait for a tripped window is always window-boundary + buffer only; different headroom values that trip the same window produce the same wait)\n- The default `headroom=0` is prior behavior (gate on the current value). **Backward compatible**. The single-mode Phase 1 \u002F review-loop checkpoint stays at headroom=0\n- **Does not cross the shared cache**: `headroom > 0` (dispatch checkpoint) **neither reads nor writes** `~\u002F.claude\u002Fusage-guard\u002Fcache.json` (computed with headroom=0 and shared with the `#123` hook). Otherwise it could either mistakenly return a projected-overshoot `ok:true` for headroom=0 (disabling the gate), or contaminate the hook path with a headroom-overshoot `ok:false`. Results with `headroom > 0` always have a `source` other than `cache`\n- Negative or non-numeric values are clamped to 0 (a misconfiguration never loosens the guard below the current-value behavior)\n- Resolution order is **CLI `--headroom \u003Cpct>` > env `USAGE_GUARD_DISPATCH_HEADROOM` > default 0**\n\n```bash\nnode ~\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-check.mjs --headroom 12   # gate on the projected value for this checkpoint only\nUSAGE_GUARD_DISPATCH_HEADROOM=12 node ~\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-check.mjs   # override the default via env\n```\n\n> **Difference from the stopgap**: lowering `USAGE_GUARD_THRESHOLD=90` (or 85) is effective as a stopgap mitigation for now, but since it's **static and can't account for concurrency \u002F worker weight**, it's not a substitute for headroom-aware dispatch. Threshold lowering is the stopgap, headroom is the real countermeasure — the two play different roles.\n>\n> **A boundary checkpoint alone cannot fully prevent in-wave overshoot**. Headroom is a preventive measure that \"stops dispatch on a projection,\" but an overshoot mid-run (mid-unit), once a worker has already launched, isn't stopped at the boundary. The reliable ceiling during a run is owned by the **PreToolUse hook** described below (`#123`). The two are combined as defense in depth (boundary pause + mid-unit ceiling).\n\n### Threshold\n\nDefault 95%. Overridable via the environment variable `USAGE_GUARD_THRESHOLD` (e.g. `USAGE_GUARD_THRESHOLD=80`).\n\nDispatch headroom defaults to 0 (percentage points). Overridable via the environment variable `USAGE_GUARD_DISPATCH_HEADROOM` (e.g. `USAGE_GUARD_DISPATCH_HEADROOM=12`); CLI `--headroom \u003Cpct>` takes precedence over env. Negative, non-numeric, or empty values fall back to the default 0 (see \"Behavior: dispatch headroom\" above).\n\nThe post-reset resume buffer defaults to 300 seconds. Overridable via the environment variable `USAGE_GUARD_RESUME_BUFFER_SECONDS` (e.g. `USAGE_GUARD_RESUME_BUFFER_SECONDS=600`). `0` restores prior behavior (resuming exactly at `resets_at`). Negative or non-numeric values fall back to the default 300.\n\nThe reflection-lag detection thresholds can also be overridden via env:\n\n- `USAGE_GUARD_LAG_EPSILON_SECONDS` (default 900): the elapsed-seconds threshold for treating something as \"right at the boundary.\" An over-threshold window with `elapsed` smaller than this is treated as suspected lag. Negative, non-numeric, or empty values fall back to the default 900\n- `USAGE_GUARD_LAG_RECHECK_SECONDS` (default 180): the short recheck interval used when lag is suspected. Values `\u003C= 0`, non-numeric, or empty fall back to the default 180 (preventing a busy-loop)\n\n## Lightweight wait-loop (shared logic)\n\nThe core stop\u002Fresume logic shared by both forms:\n\n1. Run `usage-check.mjs` to get the JSON\n2. **Degradation check**: if `source !== \"endpoint\"` (`cache` is excluded, since it originates from the endpoint), issue a degradation warning. In particular, when `source === \"fail-open\"`, **explicitly surface** \"⚠️ usage-guard degraded: source=fail-open, not actually monitoring\" **to the caller \u002F user**, and the drive caller should leave this in its report (allow \u002F progress is still maintained; see § Environment requirements)\n3. If `ok`, **proceed normally** (run the continuation command \u002F the caller moves to the next checkpoint)\n4. If `ok` is `false`, choose a resume trigger (see \"Choosing a resume trigger\" below) and **wait**\n   - **When reflection lag is suspected (`suspected_reflection_lag: true`), recheck at short intervals**. Since `wait_seconds` has already degraded to the short recheck interval (default 180 seconds), don't pin a long-lived one-shot CronCreate at the boundary — instead wait just this short interval via `ScheduleWakeup(wait_seconds)` etc. and hit `usage-check.mjs` again. Once the lag clears, the next check will pick up the real value (typically `ok: true`) and continue (avoiding a false negative that leaves things idle for ~5h on the previous window's afterimage)\n   - On a normal over-threshold, `resume_buffer_seconds` (default 300) is already folded into `wait_seconds`, so the wait extends to `resets_at + buffer`\n   - **Does not re-enter while waiting** (consumes no budget at all)\n5. On waking, run `usage-check.mjs` again, and repeat 2–5 until `ok` (since a suspected-lag result is never written to cache, the recheck fetches the real endpoint value immediately)\n6. Once `ok`, proceed to the continuation command\n\n> `wait_seconds` is not second-precise, since it's computed from `resets_at` (+ buffer). ScheduleWakeup's firing is also somewhat delayed by its floor + overhead (in practice, ~110s for a 60s request). This is precise enough for waiting out a reset.\n\n### Choosing a resume trigger\n\nThere are 2 mechanisms for resuming after a wait (heartbeat \u002F waking). Use whichever fits the situation:\n\n| Situation | Resume trigger |\n|---|---|\n| \u002Floop dynamic, in-session, wait **≤1h** | `ScheduleWakeup(min(wait_seconds, 3600))`. Capped at 3600s per call. If `wait_seconds` is longer, recheck across multiple calls |\n| Non-\u002Floop orchestration (Agent tool \u002F Workflow drive), wait **>1h**, needs restart resilience | Set a `CronCreate` (`recurring: false`, durable) at **`resets_at + resume_buffer_seconds`**, and re-submit the continuation command when it fires |\n\n- **ScheduleWakeup**: suited to an in-session heartbeat. Since one call is capped at 3600s, a long wait becomes multi-stage. When `wait_seconds > 3600`, repeat `min(wait_seconds, 3600)`.\n- **CronCreate one-shot**: fires once by wall clock and is restart-resilient. Robust for `>3600s` and non-\u002Floop cases (Agent tool orchestration, etc.). Set the firing time to `resets_at + resume_buffer_seconds` (the resume time derived from `wait_seconds`). A one-shot (`recurring: false`) **auto-deletes after firing**. In practice, when running a ~72-minute wait via Agent tool orchestration, CronCreate one-shot proved more robust than a multi-stage ScheduleWakeup.\n\n## Pending-resume marker + SessionStart detection (`#212`)\n\nThe engine emits a `resume_plan`, but it cannot *call* the host scheduling tool, so it cannot **guarantee** the arm. A marker + a SessionStart check is the code-side net that **detects a missed resume** (the exact \"stuck after reset\" failure) instead of leaving it silent.\n\n- **When a caller pauses**, it writes the marker via `resume-marker.mjs` — `writeResumeMarker({ continuation, fire_at, trigger })` → `~\u002F.claude\u002Fusage-guard\u002Fresume-pending.json` (atomic temp→rename). `continuation` is the idempotent command to re-run (e.g. `\u002Fdrive #212-213 --merge`); `fire_at` is `resume_plan.fire_at`.\n- **On a successful resume**, the caller clears it: `clearResumeMarker()`.\n- **SessionStart hook `resume-check.mjs`** reads the marker at session start and surfaces:\n  - **OVERDUE** — `fire_at` has already passed but the marker was never cleared → \"a paused resume is OVERDUE — re-run `\u003Ccontinuation>`\". This is the failure usage-guard exists to prevent, now made visible.\n  - **pending** — paused, `fire_at` not yet reached (informational).\n  - read-only, fail-open (never blocks a session start).\n\n### Wiring the SessionStart hook (optional, recommended)\n\nAdd one SessionStart entry to `~\u002F.claude\u002Fsettings.local.json` (absolute path; same M3 caveat as the PreToolUse hook):\n\n```json\n{\n  \"hooks\": {\n    \"SessionStart\": [\n      {\n        \"hooks\": [\n          { \"type\": \"command\", \"command\": \"node \u002Fhome\u002F\u003Cyou>\u002F.claude\u002Fskills\u002Fusage-guard\u002Fresume-check.mjs\" }\n        ]\n      }\n    ]\n  }\n}\n```\n\n## Usage form 1: engine form (the caller Reads it)\n\nA caller such as drive (`#122`) Reads this SKILL.md at the **boundary of a resumable unit (checkpoint)** and runs the wait-loop above.\n\n### Checkpoint convention\n\n- Always pause at a **boundary that can be cleanly re-entered**. Never pause mid-implement (e.g., before creating a PR)\n- The caller runs usage-check at the **entry** of each unit, and enters the wait-loop if not `ok`\n- The continuation command is **supplied by the caller**. Since drive resumes idempotently (detecting an existing PR and resuming from Phase 3), the re-run after waiting is reused as-is as the resume mechanism (e.g., `\u002Fdrive \u003Cargs>` — since drive's usage-guard is ON by default, don't append `--usage-guard` to the continuation command)\n- In drive's orchestration mode, this is called at the granularity of wave boundaries. An over-threshold state within a running worker is caught as a mid-unit ceiling by the PreToolUse hook in `#123`\n\n## Usage form 2: standalone form `\u002Fusage-guard \"\u003Ccontinuation command>\"`\n\nGuards any long-running work with auto pause\u002Fresume, independent of drive (user-invocable).\n\n### Arguments\n\n- Interprets `$ARGUMENTS` as the **continuation command**\n- **If blank, it's a status check only**: runs `usage-check.mjs`, displays the current `five_hour` \u002F `seven_day` `utilization` and `ok` \u002F `wait_seconds`, then exits\n\n### Steps\n\n1. Run `usage-check.mjs`\n2. **Degradation check**: if `source !== \"endpoint\"` (excluding `cache`), show a degradation warning to the user. When `fail-open`, explicitly state \"not actually monitoring\" (progress is still maintained; see § Environment requirements)\n3. If **both windows are `ok`**, run the continuation command (proceed normally)\n4. If **over threshold**:\n   - Choose a resume trigger (see § Lightweight wait-loop, \"Choosing a resume trigger\"). For an in-session wait ≤1h, use `ScheduleWakeup(min(wait_seconds, 3600))`; for >1h and non-\u002Floop, set a `CronCreate` (`recurring: false`) at `resets_at + resume_buffer_seconds` and wait (don't re-enter while waiting; `wait_seconds` already has the buffer folded in)\n   - On waking (recovery detected), **self-re-enter `\u002Fusage-guard \"\u003Ccontinuation command>\"`**\n   - Repeat the heartbeat until `ok`, then run the continuation command\n\n### Idempotency of the continuation command\n\nThe continuation command is treated as **idempotent by assumption**. It is the user's responsibility to ensure it's safe to re-run across a wait (produces no duplicate side effects \u002F detects progress and can resume partway through).\n\n- Since drive is inherently idempotent (detecting an existing PR \u002F branch and resuming), it can be safely wrapped as `\u002Fusage-guard \"\u002Fdrive #123\"` (drive's usage-guard is ON by default)\n- When wrapping general long-running work (a build, a batch job, etc.), confirm for yourself that the design is safe to re-run\n\n### Example usage\n\n```text\n\u002Fusage-guard \"\u002Fdrive #123\"\n\u002Fusage-guard \"\"                 # status check only\nUSAGE_GUARD_THRESHOLD=80 \u002Fusage-guard \"\u003Ccontinuation command>\"   # temporarily set the threshold to 80%\nUSAGE_GUARD_RESUME_BUFFER_SECONDS=600 \u002Fusage-guard \"\u003Ccontinuation command>\"   # set the resume buffer to 10 minutes\nUSAGE_GUARD_RESUME_CONTEXT=durable \u002Fusage-guard \"\u002Fdrive #123\"   # arm a durable cloud Routine (survives a hard-interrupt, #213)\n```\n\n> **`--durable` \u002F `context: durable` (`#213`)**: selects the `cron-routine` trigger so the resume survives a 100% hard-interrupt (a cloud Routine, not an in-session wakeup). Since a hard-interrupt kills the session before a reactive pause can arm anything, prefer arming the durable Routine **at the start** of a long\u002Finterruptible run, and ensure the continuation is idempotent (drive is). Falls back to `cron-oneshot` + a warning where `\u002Fschedule` is unavailable.\n\n## Enabling the PreToolUse hook (recommended in combination)\n\nThis skill's stop granularity is the boundary of a resumable unit. Whereas drive's usage-guard checkpoint (`#122`, ON by default via `#130`) can only stop at unit boundaries, the PreToolUse hook (`usage-guard-hook.mjs`) is a mid-unit ceiling that takes effect **before every tool call**, stopping things on the spot even if the threshold is crossed partway through a long unit. The two are combined with a division of labor:\n\n| Mechanism | Granularity | Where it stops |\n|---|---|---|\n| drive usage-guard checkpoint (`#122` \u002F `#130`, ON by default) | Resumable unit boundary | Cleanly re-enterable checkpoints such as a Phase entry \u002F wave entry \u002F before worker dispatch |\n| PreToolUse hook (this section) | Per tool call (including inside subagents) | Mid-unit (in-flight ceiling) |\n\n> **For drive orchestration, wiring the hook as the primary ceiling is recommended by default (`#141`)**: a boundary checkpoint structurally cannot prevent **in-wave overshoot** (a group of heavy workers, once dispatched, consumes headroom while running and can't be stopped at a boundary before hitting 100% — see \"Behavior: dispatch headroom\" above). Dispatch-headroom is a preventive measure that stops dispatch on a projection, but **this hook, which fires before tool calls inside subagents, is the only reliable ceiling** during a run. Previously, the **pre-`#139`** hook could only be \"recommended in combination\" due to the risk of hard-stopping on a transient anomalous reading, but **that hard-stop risk has been resolved by `#139`** (file kill-switch \u002F reflection-lag ALLOW \u002F debounce \u002F spike rejection), so in environments running orchestration (`\u002Fdrive`'s wave parallelism), it's fine to **wire the `matcher:\"*\"` hook by default**. Any accidental deny can be released instantly with `touch ~\u002F.claude\u002Fusage-guard\u002FDISABLE` (see § Disabling). Only by pairing defense in depth — **boundary resumable pause (headroom-aware) + the hook's mid-unit ceiling** — is overshoot practically closed off.\n>\n> The hook first reads the **same cache** that `usage-check.mjs` writes (`~\u002F.claude\u002Fusage-guard\u002Fcache.json`, 30–60s TTL). On a hot cache it just returns that, so it doesn't spam the endpoint on every tool call. **On a cold\u002Fstale cache, the hook itself falls back to `getUsage` (cache-first), hits the endpoint just once, and writes the result back to the cache (self-sustaining)**. Since `getUsage` writes to the cache using the real fs even for callers that don't inject an fs, the TTL-window fetch converges to just one call even in standalone operation without `usage-check.mjs` having run beforehand (the hook path itself keeps the cache warm). Above the threshold, it denies (exit 2) + presents `resets_at` as `HH:MM`; if usage can't be read, it fails open (allow + stderr warning). Calls originating from a subagent are distinguished in the log via the payload's `agent_id`.\n>\n> **Degradation visibility**: when usage's `source` is anything other than `endpoint` \u002F `cache` (especially `fail-open`), the hook keeps allowing while issuing a degradation warning to stderr (\"⚠️ usage-guard DEGRADED: source=fail-open …\"). This is so that the endpoint path being unusable — i.e., the guard effectively being OFF — is never missed. See § Environment requirements for the cause and how to allow it.\n\n### Resilience to transient anomalous readings (`#139`)\n\nThe hook has built-in countermeasures against an incident (`#139`) where **a single transient over-threshold reading** — such as a reflection-lag afterimage right at a reset boundary, or a one-off endpoint spike — caused the hook to deny every tool, hard-stopping the session even against edits that would remove the guard. All of these are in service of \"never hard-stop on our own bug (fail-open)\":\n\n- **(a) file kill-switch (an escape hatch)**: if `~\u002F.claude\u002Fusage-guard\u002FDISABLE` exists, the hook immediately becomes a no-op at the very start (allow + warning). Since this preserves an escape hatch while keeping `matcher:\"*\"` intact, it doesn't lose its role as a mid-unit ceiling monitoring tool calls inside subagents. It can be released\u002Fre-enabled with `touch`\u002F`rm` from a `!` shell, instantly within the session with no config edit needed (see § Disabling).\n- **(d) reflection lag is ALLOWed**: when usage has `suspected_reflection_lag === true` (a previous-window afterimage right at the boundary), the hook lets it through. If it's a genuine over-threshold, the resumable-unit boundary checkpoint will stop it separately.\n- **(b) debounce**: doesn't deny on a single over-threshold reading — only denies once it's exceeded for **N consecutive reads** (default 2, overridable via `USAGE_GUARD_DEBOUNCE_COUNT`). Even a single under-threshold reading in between resets the consecutive count. The consecutive count and the most recent good reading are kept in a **per-origin** state file (see (f) below).\n- **(c) spike rejection**: a physically implausible jump from a recent good reading (threshold − `USAGE_GUARD_SPIKE_DELTA`, default under 25) to over threshold within `USAGE_GUARD_SPIKE_WINDOW_SECONDS` (default 120 seconds) is treated as suspect and ALLOWed + warned.\n- **(e) cache hygiene for over-threshold readings**: `usage-check.mjs` caches an over-threshold reading with a shortened TTL (10s, vs. the normal 45s). This avoids pinning a transient overshoot for the full TTL and re-verifies it early on the next check.\n- **(f) concurrency-safe state (`#211`)**: under Workflow fan-out (up to 16 concurrent subagents, each firing the hook before every tool call) the debounce\u002Fspike counter must not be corrupted or lost. Two measures ensure this: **(i) per-origin state files** — the counter lives at `~\u002F.claude\u002Fusage-guard\u002Fhook-state.json` for the main session and at `hook-state.\u003Cagent_id>.json` per subagent, so concurrent hooks (always from *different* origins) never share one file and a read-modify-write can't lose an update; **(ii) atomic writes** — both the state file and the shared `cache.json` are written to a temp sibling and `rename`d into place, so a concurrent reader never observes a torn\u002Fhalf-written JSON. Both bias the guard toward *holding* (never silently dropping a deny) under exactly the high-concurrency load where overshoot is most likely.\n\nA soft-allow via lag\u002Fspike\u002Fdebounce issues a degradation warning to stderr (so the guard doesn't just look like it's OFF).\n\n### Mechanism and placement\n\nThe hook itself is bundled as an extra file directly under the skill directory (same path as `usage-check.mjs`). Since this repo does **not** distribute settings\u002Fhooks (the build only outputs skills \u002F agents), enabling it is a **manual opt-in**.\n\n### settings.local.json snippet (`update-config` style)\n\nAdd one PreToolUse hook entry to `~\u002F.claude\u002Fsettings.local.json` (settings reload mid-session, so no restart is needed):\n\n```json\n{\n  \"hooks\": {\n    \"PreToolUse\": [\n      {\n        \"matcher\": \"*\",\n        \"hooks\": [\n          {\n            \"type\": \"command\",\n            \"command\": \"node \u002Fhome\u002F\u003Cyou>\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-guard-hook.mjs\"\n          }\n        ]\n      }\n    ]\n  }\n}\n```\n\n> **M3: fill in the hook script path as an absolute path by hand.** Since skill-dir-relative references don't work inside settings, `command` must be an **absolute path**. The path varies by environment:\n>\n> - **user-scope** (placed via `npx @ozzylabs\u002Fskills install`): `~\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-guard-hook.mjs` (`~` is not expanded, so write it out in full as `\u002Fhome\u002F\u003Cyou>\u002F.claude\u002F...`)\n> - **dogfood** (run inside the skills\u002Fcommons repo): `\u003Crepo>\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-guard-hook.mjs` (e.g. `\u002Fhome\u002F\u003Cyou>\u002Fgithub\u002Fozzy-labs\u002Fskills\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-guard-hook.mjs`)\n>\n> Both refer to \"the `usage-guard-hook.mjs` at the same level as `usage-check.mjs`.\" Confirm your own environment's full path before filling it in. If overriding the threshold, also attach the env on the settings side (e.g. `\"command\": \"USAGE_GUARD_THRESHOLD=80 node \u002Fhome\u002F\u003Cyou>\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-guard-hook.mjs\"`).\n\n**`matcher` defaults to `\"*\"` (all tools).** This lets it function as a mid-unit ceiling that also monitors tool calls inside subagents. Since a hard-stop from a transient anomalous reading can be released via the **file kill-switch** (§ Disabling) without narrowing `matcher`, it's fine to keep operating with `matcher:\"*\"`. If you really want to limit monitoring to only heavy tools (weakening subagent monitoring), you can instead scope it to something like `\"matcher\": \"Bash|Workflow|Task\"`, always letting `Edit` \u002F `Read` \u002F `AskUserQuestion` through (optional).\n\n### Disabling\n\nTwo ways. **Use the kill-switch when you need something instant with no config edit**:\n\n- **file kill-switch (recommended, instant within the session, `#139` (a))**: creating `~\u002F.claude\u002Fusage-guard\u002FDISABLE` makes the hook an immediate no-op at the start. Since it can be run from a `!` shell, it can be released even in a situation where the hook has wrongly denied on a transient anomalous reading and even stopped the very edit meant to remove the guard:\n\n  ```bash\n  touch ~\u002F.claude\u002Fusage-guard\u002FDISABLE   # disable (instant)\n  rm    ~\u002F.claude\u002Fusage-guard\u002FDISABLE   # re-enable (instant)\n  ```\n\n- **Removing from settings (permanent)**: removing the above PreToolUse entry from `~\u002F.claude\u002Fsettings.local.json` disables it instantly (mid-session reload).\n\n## Notes\n\n- The guard **never hard-stops on its own bug**: if signal acquisition fails entirely, it fails open and continues work (`source: \"fail-open\"` + stderr warning)\n- However, **fail-open means the guard is OFF**. When `source !== \"endpoint\"`, each form issues a degradation warning — if you see one, restore the endpoint path per § Environment requirements\n- **Watch for false denies from transient anomalous readings (`#139`)**: the PreToolUse hook can falsely deny on a transient over-threshold reading, such as a reflection-lag afterimage or an endpoint spike. Against this, the hook is resilient via (d) lag ALLOW \u002F (b) debounce \u002F (c) spike rejection, and as a last resort can be released instantly within the session with no config edit via the **file kill-switch** (`touch ~\u002F.claude\u002Fusage-guard\u002FDISABLE`) (see § Resilience to transient anomalous readings \u002F § Disabling)\n- **Watch for in-wave overshoot in orchestration (`#141`)**: a boundary checkpoint cannot stop an overshoot occurring during a wave after dispatch. This is closed off via two layers — **dispatch-headroom** (stops dispatch on a projection; see § Behavior: dispatch headroom) and the **PreToolUse hook** (a mid-unit ceiling during a run; see § PreToolUse hook). Lowering the threshold is a stopgap and not a substitute\n- Consumes no budget while waiting (doesn't re-enter — heartbeat only)\n- The resume after waiting extends to `resets_at + resume_buffer_seconds` (default +5 minutes), to prevent re-entering headroom cut exactly at the reset and immediately tripping again\n- A long wait is assumed to be absorbed by a live session (WSL + always-on). For >1h and non-\u002Floop cases, a CronCreate one-shot (durable) is more robust (see § Lightweight wait-loop)\n",{"name":5,"description":7,"argument-hint":43,"user-invocable":44,"disable-model-invocation":44},"\u003Ccontinuation command> (leave blank to just check status)",true,"main","db212a47efe751fa1c42bd76e61dbfd8328e5b10",".claude\u002Fskills\u002Fusage-guard","https:\u002F\u002Fgithub.com\u002Fozzy-labs\u002Fskills","https:\u002F\u002Fgithub.com\u002Fozzy-labs","2026-07-13 06:49:42",{"source":8,"skillName":5,"commands":52,"prompt":62,"zipAvailable":44},[53,56,59],{"tool":54,"command":55},"npx","npx skills add ozzy-labs\u002Fskills --skill usage-guard",{"tool":57,"command":58},"bunx","bunx skills add ozzy-labs\u002Fskills --skill usage-guard",{"tool":60,"command":61},"pnpm","pnpm dlx skills add ozzy-labs\u002Fskills --skill usage-guard","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fozzy-labs\u002Fskills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fusage-guard\u002FSKILL.md",[64,70,74,78],{"path":65,"resourceType":66,"resourceTypeDesc":67,"sizeBytes":68,"sourceHtmlUrl":69},"resume-check.mjs","SCRIPT","脚本",3358,"https:\u002F\u002Fgithub.com\u002Fozzy-labs\u002Fskills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fusage-guard\u002Fresume-check.mjs",{"path":71,"resourceType":66,"resourceTypeDesc":67,"sizeBytes":72,"sourceHtmlUrl":73},"resume-marker.mjs",3865,"https:\u002F\u002Fgithub.com\u002Fozzy-labs\u002Fskills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fusage-guard\u002Fresume-marker.mjs",{"path":75,"resourceType":66,"resourceTypeDesc":67,"sizeBytes":76,"sourceHtmlUrl":77},"usage-check.mjs",39104,"https:\u002F\u002Fgithub.com\u002Fozzy-labs\u002Fskills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-check.mjs",{"path":79,"resourceType":66,"resourceTypeDesc":67,"sizeBytes":80,"sourceHtmlUrl":81},"usage-guard-hook.mjs",24249,"https:\u002F\u002Fgithub.com\u002Fozzy-labs\u002Fskills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fusage-guard\u002Fusage-guard-hook.mjs",[83,108,130,162,184,203],{"skillNo":84,"name":85,"nameZh":6,"description":86,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":87,"categories":88,"keywords":89,"publishedAt":38},"01M71J6R63H4E6ZE6KMDM4CT85","verify","An integrated skill that runs the combined validation of build \u002F typecheck \u002F test \u002F lint in one shot. The `verify.mjs` engine auto-discovers validation commands via a discovery chain (AGENTS.md's 「検証」 section → package.json scripts → justfile\u002FMakefile\u002Flefthook → language heuristics), executes them serially with their source attached, and returns a result summary. If a higher-priority stage produces a hit, only that stage is executed (no crossing stages).","https:\u002F\u002Fgithub.com\u002Fozzy-labs\u002Fskills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fverify\u002FSKILL.md",[],[85,90,91,92,93,94,95,96,35,97,98,99,100,101,102,103,104,105,106,107],"combined","heuristics","chain","repo","discovery","section","lefthook","auto","package","stage","Bash","one","lint","dry","engine","with","test","typecheck",{"skillNo":109,"name":110,"nameZh":6,"description":111,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":112,"categories":113,"keywords":114,"publishedAt":38},"01M71J6FRG119REA1G0S99Q4GZ","topics","Selects GitHub topic candidates by having the `topics.mjs` engine perform constraint validation, popularity measurement, broad+narrow \u002F singular-plural comparison, and hardcoded ozzy-labs conventions, then applies them via `gh repo edit --add-topic` following the policy's `externally-visible` gate (default batch-confirm). Scope is limited to use within ozzy-labs.","https:\u002F\u002Fgithub.com\u002Fozzy-labs\u002Fskills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Ftopics\u002FSKILL.md",[],[110,115,116,93,117,118,119,120,121,122,123,100,124,125,126,103,104,127,128,129],"measurement","scope","policy","default","true","gh","plural","use","comparison","apply","candidate","batch","candidates","github","via",{"skillNo":131,"name":132,"nameZh":6,"description":133,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":134,"categories":135,"keywords":146,"publishedAt":38},"01M71J6DC27P4FDSP10SDT2PSN","skill-observability","Defines the skill observability event contract (event.schema.json) and emit substrate (obs-emit.mjs). The SSOT for the measurement layer of the skill improvement loop. Referenced by other skills and hooks.","https:\u002F\u002Fgithub.com\u002Fozzy-labs\u002Fskills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fskill-observability\u002FSKILL.md",[136,141],{"categoryNo":137,"categoryName":138,"taxonomyType":139,"taxonomyTypeDesc":140},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",{"categoryNo":142,"categoryName":143,"taxonomyType":144,"taxonomyTypeDesc":145},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[147,148,115,149,150,151,152,153,154,155,156,157,158,159,160,161],"observability","skill observability","improvement","skill improvement","for","event","hooks","contract","schema","loop","false","substrate","json","improvement loop","layer",{"skillNo":163,"name":164,"nameZh":6,"description":165,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":166,"categories":167,"keywords":169,"publishedAt":38},"01M71J6AYECH1J94RAZ49QZ19Y","skill-metrics","Aggregates the local observability event log (~\u002F.agents\u002Fobservability\u002Fevents.jsonl) and presents, read-only, per-skill invocation counts and notable events (fallback \u002F HITL rejection \u002F loop cap reached \u002F abort). Fires on \"show me the skill metrics,\" \"aggregate the observability results,\" \"which skills get used the most?\" Does not send anything.","https:\u002F\u002Fgithub.com\u002Fozzy-labs\u002Fskills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fskill-metrics\u002FSKILL.md",[168],{"categoryNo":137,"categoryName":138,"taxonomyType":139,"taxonomyTypeDesc":140},[170,171,147,172,173,174,175,176,152,177,178,179,180,119,35,156,181,100,182,183],"skill metrics","metrics","event log","which","show","get","read-only","iso","aggregates","send","local","show me","log","jsonl",{"skillNo":185,"name":186,"nameZh":6,"description":187,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":188,"categories":189,"keywords":190,"publishedAt":38},"01M71J68TD0QFATGW3BBZVCWA2","ship","Runs verification, commit, and PR creation all at once. An integrated pipeline that runs verify (build + type + test + lint) → commit → PR creation in sequence against the changes.","https:\u002F\u002Fgithub.com\u002Fozzy-labs\u002Fskills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fship\u002FSKILL.md",[],[186,191,192,106,193,194,85,195,196,197,28,198,199,200,119,201,100,202,102],"type","verification","changes","pr","creation","build","pipeline","read","sequence","grep","pr creation","commit",{"skillNo":204,"name":205,"nameZh":6,"description":206,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":9,"repositoryNo":12,"starsCount":13,"licenseSpdxId":14,"licenseFileUrl":6,"sourceHtmlUrl":207,"categories":208,"keywords":210,"publishedAt":38},"01M71J66A4PXTXB13QYBE97XG5","review","Reviews code changes or PRs across 11 perspectives, reporting via JSON structured output plus a human-readable report. Supports switching between quick and deep modes. Takes a PR number or working-tree diff as input.","https:\u002F\u002Fgithub.com\u002Fozzy-labs\u002Fskills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Freview\u002FSKILL.md",[209],{"categoryNo":137,"categoryName":138,"taxonomyType":139,"taxonomyTypeDesc":140},[205,211,212,24,213,214,215,194,216,151,217,218,119,219,220,221,100,32,222,223],"structured output","tree","deep","reviews","quick","diff","prs","modes","switching","number","workflow","output","across"]