A tool adapter receives {"max_calls": false}. Its Python validation checks that the value is an integer between zero and three. The request passes. A later branch interprets a falsey limit as unlimited and admits all five items in the batch.

Two ordinary implementation choices have combined into a broken limit: bool is a subclass of int, and zero has acquired a second meaning. Neither choice requires a sophisticated attack. A malformed model-generated argument, a client bug or a configuration edit can reach the same path.

I would reject ambiguous resource limits before admitting any work. The contract must define the accepted representation, the permitted range and the meaning of zero. A numeric-looking value is insufficient evidence that the requested work stays within the service’s policy.

This is a synthetic application bug, not a vulnerability claim about Claude Code, MCP or a model provider. The downloadable example uses an in-memory list as its effect trace. It makes no model calls and spends no money.

Why the integer check passes

Python’s built-in type documentation states that bool is a subclass of int. In numeric contexts, False and True behave like zero and one. The standard JSON decoder converts JSON false to Python False, while an integer number normally becomes an int.

That combination makes this check weaker than its author may intend:

if not isinstance(limit, int) or not 0 <= limit <= 3:
    raise ValueError("bad limit")

Both Boolean values pass. On its own, that does not prove excess work: True used as a slice bound selects one item, and False selects none. The dangerous behaviour in this fixture comes from the next line:

selected = items[:limit] if limit else items

The fallback selects the whole batch. It also does so for the legitimate integer 0. Rejecting Booleans fixes one admission defect, but leaving this fallback in place would still make a zero-call request run everything.

The earlier NaN evaluation-gate example also rejects Booleans, but its decision is whether to accept a score. This case adds a different failure: even a correctly typed zero can select unbounded work. This is why I want tests of the admitted effects as well as tests of the validator’s return value. An input can satisfy the intended type and still acquire an unsafe meaning in a later branch.

Run the failure and the repaired path

Download and inspect the standard-library Python fixture, then run:

python3 agent-boolean-limit.py
python3 agent-boolean-limit.py --test
python3 -O agent-boolean-limit.py --test

The example has five items named a through e. Its trusted application cap is three. The default run prints:

unsafe_false=a,b,c,d,e
guarded_false=rejected
guarded_false_effects=0
guarded_zero=0
guarded_two=2

The unsafe branch accepts the decoded Boolean and selects every item. The repaired path rejects that request before appending anything to the effect trace. A valid zero selects nothing, while two selects the first two items.

The list records synthetic admissions, not actual downstream API calls. These tests establish the behaviour of this small function. They do not establish enforcement across retries, concurrent workers or a real provider’s billing system. To transfer the regression to your adapter, replace the list with a fake downstream client that records every attempted invocation and check its call history.

Keep the unsafe function in test material only. It exists to preserve a reproducible counterexample, not as an alternative mode for production use.

Define a narrower contract deliberately

The repaired fixture requires one object member, max_calls, whose value decodes to an exact Python int between zero and three:

if type(request) is not dict or set(request) != {"max_calls"}:
    raise ValueError("expected max_calls only")
limit = request["max_calls"]
if type(limit) is not int or not 0 <= limit <= SERVER_CAP:
    raise ValueError("max_calls must be an integer token from 0 to 3")
selected = ITEMS[:limit]

The exact-type comparison excludes bool. The slice gives zero one meaning: admit no items. The upper bound comes from trusted service code; the caller cannot raise it by adding another field. A production service can obtain that bound from authenticated policy rather than a constant, but it must keep the policy source outside client control.

This implementation is intentionally stricter than JSON Schema’s integer semantics. JSON Schema’s numeric reference explains that 1 and 1.0 represent the same mathematical integer. With Python’s default decoder, 1.0 becomes a float, so this fixture rejects it. It also rejects 1e0 and the string "1".

Do not describe this helper as a complete JSON Schema validator. If your public tool contract promises mathematical integers, use a schema validator with the declared semantics, test that it rejects Booleans, and convert accepted values through a bounded, explicit normalisation step. If you choose the narrower integer-token contract used here, document it and test the actual client encodings. A disagreement between the advertised schema and the adapter will otherwise appear as unexplained tool failures.

Avoid repairing the mismatch with a broad int(value) conversion. That conversion accepts values the fixture means to reject, including Boolean and string inputs; it can also truncate a non-integral float. Coercion should be a documented compatibility decision with its own tests, rather than an automatic response to invalid input.

Reject before the effect boundary

The fixture rejects a missing field, extra fields and a non-object root. It rejects duplicate object members through object_pairs_hook, so a request cannot supply the limit twice and rely on a last-value-wins interpretation. It also rejects the decoder’s non-standard NaN and infinity constants through parse_constant.

Those parser checks support the admission contract, but they do not make this a general untrusted-JSON gateway. The example assumes a small input. Python’s JSON documentation explicitly recommends limiting untrusted input size because decoding can consume substantial CPU and memory. Enforce request-size and parser-resource limits before accepting arbitrary bodies into a production endpoint.

The regression suite checks the Boolean failure, zero behaviour, allowed counts and values outside the cap. Other cases cover coercions, duplicate members, malformed JSON and invalid object shapes. Every rejected request must leave the effect trace empty. The tests use unittest checks, so they remain active under python3 -O.

There is a separate aggregate-budget problem even after this function is correct. Three calls per request can still become many calls across a retry loop or several workers. Reserve shared capacity at the point of admission, account for unknown outcomes, and reconcile provider receipts separately. The earlier retry-amplification article covers multiplication across retry layers. This example only enforces one batch’s local limit.

Add the counterexample to the adapter you ship

Locate the last application function that accepts a caller-supplied count before work begins. Send false, true, 0, a valid positive count and a value above the service cap through the real decoding path. Check both the error response and the downstream invocation history.

Then inspect every default applied to that count. Expressions such as limit or default, empty-value fallbacks and zero-as-unlimited sentinels need review against the contract. A legitimate zero must survive each layer if it means no work. When a field is missing, either reject it or apply a documented bounded default; do not let a convenience fallback silently increase authority.

For broader guidance on tool permissions and production operating controls, see my Claude Code book. Buy the Kindle edition on Amazon UK.