Hard budget enforcement for Claude Code tool calls. The Cycles MCP server gives agents budget tools; this plugin puts Cycles in the dispatch path: gated tool calls are held behind a PreToolUse hook that reserves budget first, and a Cycles DENY blocks the call at the harness layer — the model cannot skip it. This closes the cooperative-enforcement gap documented in the MCP server's Security Model.
Enforcement scope, precisely: all tools are gated EXCEPT (a) the configurable skip list, which defaults to local zero-cost read tools (Read/Glob/Grep/LS/NotebookRead/TodoWrite/AskUserQuestion), and (b) the Cycles budget tools themselves, matched by exact namespace (recursion guard). Set CYCLES_CC_SKIP_TOOLS=^$ to gate every non-Cycles tool.
Built for teams that need enforceable AI-agent budgets, auditable action-level cost control, and a clear runtime authority boundary around Claude Code automation.
flowchart LR
A[Claude requests a tool] --> B[PreToolUse gate]
B -->|reserve cost| C[Cycles budget authority]
C -->|ALLOW / ALLOW_WITH_CAPS| D[Tool executes]
C -->|DENY or authoritative error| E[Tool is blocked]
D -->|success| F[Commit reservation]
D -->|failure| G[Release reservation]
F --> H[Session recovery retries unsettled usage]
G --> H
The model never decides whether the gate runs. Claude Code invokes the hook synchronously before dispatch. In normal operation, only a confirmed reservation lets a gated tool proceed; during a 5xx, network, or timeout outage, the default fail-open posture permits the call with a warning unless CYCLES_CC_FAIL_CLOSED=true.
- Platform and security teams governing autonomous Claude Code workflows.
- FinOps and engineering leaders who need tool usage tied to explicit budgets.
- Developers who want an enforcement layer that does not depend on the model voluntarily checking a balance.
- PreToolUse — reserves a flat per-call cost before each gated tool call. Any authoritative Cycles rejection — DENY, budget exhausted/frozen/closed, debt, auth failure, invalid request — blocks the call with a reason the model sees; malformed protocol or hook inputs also block, while fail-open applies ONLY to outages (5xx/network/timeout).
ALLOW_WITH_CAPSis validated against the protocol and tool allow/denylists are enforced with allowlist precedence; other caps are surfaced to the transcript. A granted call is denied and its hold returned if durable local settlement state cannot be written. Idempotency keys derive fromtool_use_id(unique per call), so transport retries never double-reserve. - PostToolUse (success) — durably marks the action as executed, then commits the reservation. Transient failures remain pending commits and are retried; if the reservation expired, disappeared, or was finalized, usage is charged via an idempotent fallback event. Injects a low-budget warning into the model's context under 15% remaining.
- PostToolUseFailure — releases the hold when the tool call failed: failed attempts return budget instead of charging it.
- SessionEnd — settles anything the per-call hooks could not: releases unresolved holds, retries pending commits, and applies pending usage events. State survives failed settlements.
- SessionStart — replays pending commits and usage-event charges from any session with the same server/subject/unit routing identity. It deliberately never releases holds — concurrent sessions are normal, and stranded holds are already time-bounded by the reservation TTL.
- Companion MCP server — the
@runcycles/mcp-servertoolset (balances, explicit reserves, usage events), pinned to an exact version and fetched via npx on first run (not vendored into this repo). /cycles-budget-guard:budget— one-command budget status report./cycles-budget-guard:doctor— secret-safe effective-configuration and connectivity diagnostics.
Requires Node.js 22 or newer.
- Add the marketplace and install the plugin:
/plugin marketplace add runcycles/cycles-claude-plugin
/plugin install cycles-budget-guard@runcycles
- Set the environment before launching or restarting Claude Code (the same variables are used by the companion MCP server).
For Bash or zsh:
export CYCLES_BASE_URL=https://your-cycles-server
export CYCLES_API_KEY=your-key
export CYCLES_DEFAULT_TENANT=acme # recommended; at least one CYCLES_DEFAULT_* field is required
export CYCLES_DEFAULT_APP=claude-code # optional, finer attributionFor PowerShell:
$env:CYCLES_BASE_URL = "https://your-cycles-server"
$env:CYCLES_API_KEY = "your-key"
$env:CYCLES_DEFAULT_TENANT = "acme"
$env:CYCLES_DEFAULT_APP = "claude-code"Supported subject fields are TENANT, WORKSPACE, APP, WORKFLOW, AGENT, and TOOLSET; configure at least one as CYCLES_DEFAULT_<FIELD>. Tenant is the usual top-level budget owner.
If CYCLES_BASE_URL or a subject default is missing, the plugin stays dormant (normal Claude Code permission flow) — it never half-enforces. An invalid value fails loudly by blocking every non-Cycles call with a config error, including tools that the valid skip pattern would normally exempt; exact Cycles recovery tools remain available.
- Start or restart Claude Code from that configured shell, then run:
/cycles-budget-guard:doctor
/cycles-budget-guard:budget
/reload-plugins is sufficient after installation only when the current Claude Code process already inherited the required environment variables.
The doctor's local diagnostic step remains subject to the normal gate and may reserve the configured per-call cost. It never adds a bypass or skip-list exemption.
- Ask Claude Code to run a harmless gated action, such as
printf 'Cycles gate active\n'with Bash. With available budget, Cycles reserves the configured per-call cost and the command proceeds.
- Use a test tenant with an exhausted, frozen, or closed Cycles budget.
- Set
CYCLES_CC_SKIP_TOOLS=^$if you want every non-Cycles tool included in the proof. - Ask Claude Code to run
printf 'THIS MUST NOT RUN\n'with Bash. - Confirm Cycles denies the reservation and the command produces no output because the tool never executes.
- Run
/cycles-budget-guard:budgetto inspect the active budget and routing identity.
For a fail-closed outage check, set CYCLES_CC_FAIL_CLOSED=true, point the test configuration at an unreachable Cycles endpoint, and confirm the gated tool call is blocked after the bounded four-second timeout.
| Variable | Default | Meaning |
|---|---|---|
CYCLES_CC_UNIT |
CREDITS |
USD_MICROCENTS, TOKENS, CREDITS, or RISK_POINTS |
CYCLES_CC_COST |
1 |
Positive integer cost reserved+committed per tool call |
CYCLES_CC_SKIP_TOOLS |
^(Read|Glob|Grep|LS|NotebookRead|TodoWrite|AskUserQuestion)$ |
Regex of tool names never gated (default: local zero-cost reads) |
CYCLES_CC_FAIL_CLOSED |
false |
true blocks tool calls when the Cycles server is unreachable |
CYCLES_CC_TTL_MS |
1800000 (30 min) |
Reservation TTL; must outlive permission prompts and long tool runs |
Cycles' own budget tools are never gated (recursion guard), regardless of CYCLES_CC_SKIP_TOOLS.
Once both a base URL and subject setting opt into enforcement, invalid URLs, subjects, units, costs, TTLs, regular expressions, or fail-mode values deny every non-Cycles call with a named configuration error before skip-list evaluation; they are never treated as outages.
Non-loopback Cycles endpoints must use HTTPS. Plain HTTP is accepted only for localhost, 127.0.0.0/8, and [::1] development endpoints so budget decisions and credentials cannot be intercepted or altered in transit.
- Bounded latency: every Cycles request carries a 4-second deadline, so a black-holed server can never hang tool dispatch.
- Fail-open by default: an unreachable (or timed-out) Cycles server allows the call and logs a warning. Enterprises wanting strict enforcement set
CYCLES_CC_FAIL_CLOSED=true. - Identity & retries: the idempotency key is derived from
tool_use_id— unique per tool call, stable across transport retries, so retries replay the same reservation and distinct identical calls are charged separately. On Claude Code versions that predatetool_use_id, a content-hash fallback applies; under the fallback, identical calls sharing session/prompt/tool/arguments collide and under-count (never over-charge). - Units: flat per-call cost is deliberate — tool calls don't carry token counts. Meter LLM spend itself with the Cycles integrations for your model gateway, and use this plugin for action-level authority.
| Symptom | Likely cause | Resolution |
|---|---|---|
| Plugin is installed but tools are not gated | Missing base URL or default subject leaves enforcement dormant | Run /cycles-budget-guard:doctor; set CYCLES_BASE_URL and at least one supported CYCLES_DEFAULT_* subject field (typically TENANT) |
| Companion MCP server does not start | Invalid/missing URL, authentication, or first-run npx download failure | Verify Node.js 22+, network access, URL, and CYCLES_API_KEY; then run /reload-plugins |
| Every gated action is blocked with a configuration error | A configured value is invalid | Use the named variable in the denial message; the doctor command reports the same strict validation safely |
| Server outage allows tool calls | Default posture is fail-open for transport outages only | Set CYCLES_CC_FAIL_CLOSED=true for strict environments |
| Server outage blocks tool calls | Fail-closed mode is active | Restore connectivity or explicitly choose fail-open after reviewing the policy impact |
| Expected tool is not charged | It matches the default or operator skip regex | Inspect CYCLES_CC_SKIP_TOOLS; set it to ^$ to gate every non-Cycles tool |
| Plugin update is not visible | Third-party marketplace auto-update is off or the session has not reloaded | Run /plugin marketplace update runcycles, run claude plugin update cycles-budget-guard@runcycles in the shell, then /reload-plugins |
When reporting a problem, include the redacted doctor output plus the plugin, Claude Code, Node.js, and operating-system versions. Never include API keys, prompts, tool arguments, or file contents.
The hooks send only enforcement metadata: your configured API credential and subject identifiers; the tool name; unit, amount, and reservation TTL; reservation and idempotency identifiers; and fixed settlement reasons. Tool arguments, file contents, and raw prompts are never sent. Current Claude Code calls derive the opaque idempotency hash from session/tool-use identifiers; the legacy fallback hashes the prompt identifier, tool name, and arguments locally, and only that digest leaves the machine. Non-loopback endpoints require HTTPS; redirects are never followed and their 3xx responses deny as authoritative, so the custom API-key header cannot be forwarded to another origin. Full policy: https://runcycles.io/privacy
- Cycles for Claude Code
- Claude and Codex integration guide
- Security and implementation audit
- Release history
- Security policy
- Report an issue
Apache-2.0