What 200 public agent repositories declare, and what they do
A static scan of 200 permissively-licensed public agent repositories, at pinned commits, measured against ARS v1.0. No repository is named.
What this is
Two hundred public agent repositories, selected by a rule written before the results were known, fetched at pinned commits, and read by the same instrument this site publishes. Nothing was executed. No repository is named.
The short version. Of 4,445 tools observed across the corpus, 103 — 2.3% — state anything at all about whether they change state. Of the 100 whose claim could be checked against their own implementation, 0 were found contradicting it. The gap is not that declarations are wrong. It is that there are almost none.
Read the headline carefully, because a zero is the easiest number on a page to misread. It does not say that agent tools are safe. It says that in a corpus of 4,445 tools, only 103 made a statement specific enough to be checked at all — and that the ones that did were, by these deliberately conservative rules, keeping to it. The finding is the size of that denominator.
That is the measurable version of the problem this standard exists for. An assurance argument compares what a system is permitted to do with what it does, and across this corpus the first half of that comparison is mostly absent.
The headline figure
Of the tools whose OWN self-description asserts that they only read — a declared read side-effect, or a description shipped to the model saying it changes nothing — how many have an implementation that changes state?
headline0.0% 0 of 100 tools
The denominator excludes the 3 not-observable cases. The tool asserts read-only and its body could not be read: the implementation is in a language this does not parse, the function could not be bounded without risking a neighbour's code, or the source line was not resolvable
A zero here is a measurement, not a reassurance. It says that of the 100 tools that made a checkable claim, none was found breaking it under these rules — rules deliberately chosen to under-report: POST is not counted as a write, calls into helpers are not followed, and writes to caches, temp paths and logs are discounted. It is not a statement about the 3 that could not be checked, and it is not a statement about anything that made no claim at all.
provenance
primary-contradiction-by-tool in
aggregate.json — 0
+ 100 + 3 references,
one per member of each bucket, as salted identifiers
The corpus
How it was built. Fifteen GitHub topic queries — the frame is committed beside the data and was fixed before anything was measured. Every entry cleared 80 stars, is not a fork, is not archived, and carries a permissive SPDX licence; anything with no licence, an unresolvable one, or a copyleft one was excluded and recorded. 1,017 repositories were discovered, 435 excluded by a published rule, 582 eligible, and 200 selected by walking the fifteen strata in turn rather than taking the top 200 by stars — which would have described whichever two topics have the largest repositories.
Every entry is pinned to a commit SHA, never a branch. A corpus that moves under its own findings cannot be re-checked by anybody who disputes them.
Nothing was executed and nothing was re-hosted. Clones were shallow, at the pinned commit,
with hooks and both gitconfigs pointed at an empty directory, symlinks materialised as inert files,
and .git removed after checkout. This work references public repositories and extracts
facts about them; it does not redistribute anyone's source.
What the sample is biased toward, stated rather than discovered later. Popularity — every entry cleared a star floor, so this describes agent code people found. Topic labels — a repository that applies none of the fifteen topics is invisible to the frame. And a clear licence, which correlates with organisational maturity. All three bias the sample toward more careful code, which is the conservative direction for a finding of this shape.
Where "undeclared" comes from, when nobody wrote a declaration
Not one repository in this corpus has ever written an Agent Contract. So the finding cannot come from comparing code against an owner's declaration — there is none. It comes from the artifact contradicting itself.
Primary, and the only source the headline uses. A tool's own self-description against its
own body. Either a machine-readable classification in the declaration — an MCP
readOnlyHint, a read_only keyword, a declared read side-effect — or the
description string that gets shipped to the model, saying in words that the tool changes nothing. A
tool description is not documentation in the usual sense: it is the operative text the agent plans
against, at the moment of the decision.
Excluded from the headline: the tool's name. fetch_ in HTTP code
frequently means "make a request", and a get_ on a search client can legitimately POST.
That reading is computed and published below, separately, so a reader can see what the looser
interpretation would have produced — it is not folded in.
Also excluded: the repository's README. A README describes a project; a contradiction has to be about a tool.
⚠️ POST is not a write, and that decision is why this number is small
The tempting implementation counts any outbound HTTP call from a read-named tool as a
transmission. It is wrong. Every vector search, GraphQL query, embedding lookup and model inference
call is an HTTP POST that changes nothing. A detector that counted them would fire on nearly every
retrieval tool in the corpus, and the first reader to open an example would find a
requests.post to a search endpoint and stop reading.
So POST and PUT are excluded entirely. What counts is behaviour that changes state under any reading: local file writes and deletes, subprocess execution, runtime code evaluation, explicit HTTP DELETE, mail and message dispatch, state-changing SQL, and payment. Losing volume to keep the claim true is the trade this page makes.
Two further choices, both of which shrink the number: a write whose target names a cache, a temp path or a log is not counted; and the detector reads each tool's own body and follows no calls, so a tool that delegates to a helper that deletes is not reported. The figure understates.
Every rule is a published regular expression. Nothing was judged by a model, so a maintainer who disputes a result can run the same rules over their own code and get the same answer.
The distribution
Every figure carries three buckets: what was observed present, what was observed absent, and what could not be observed at all. The third is printed at the same size as the other two, because a percentage whose unreadable share is hidden is a statement about the instrument.
How many repositories contain at least one tool that asserts read-only and does not behave that way?
0.0% 0 of 11 repositories
The denominator excludes the 189 not-observable cases. No tool in the repository made a tier A or tier B read-only assertion that could be checked — either no tool was observed at all, none of the observed tools said anything about reading, or the bodies of those that did could not be read. Plus: nothing was read for these repositories: 1 scan_failed.
A zero here is a measurement, not a reassurance. It says that of the 11 repositories that made a checkable claim, none was found breaking it under these rules — rules deliberately chosen to under-report: POST is not counted as a write, calls into helpers are not followed, and writes to caches, temp paths and logs are discounted. It is not a statement about the 189 that could not be checked, and it is not a statement about anything that made no claim at all.
provenance
primary-contradiction-by-repository in
aggregate.json — 0
+ 11 + 189 references,
one per member of each bucket, as salted identifiers
Of every tool observed across the corpus, how many make any statement at all about whether they change state — either a machine-readable annotation in the declaration, or a sentence in the description shipped to the model?
2.3% 103 of 4445 tools
The denominator excludes the 0 not-observable cases. None: whether a tool says anything about its own side effects is readable from the tool as observed, and does not depend on reading its body. Tools in repositories the adapters could not read at all are absent from this population entirely, which the coverage figures below account for.
⚠️ A tool that states nothing is not thereby doing anything wrong. This measures the availability of a declaration to check an implementation against — not the quality of any implementation.
provenance
tools-that-state-their-own-side-effects in
aggregate.json — 103
+ 4342 + 0 references,
one per member of each bucket, as salted identifiers
Of the tools whose NAME begins with a read verb and whose description says nothing either way, how many have an implementation that changes state?
0.4% 2 of 516 tools
The denominator excludes the 41 not-observable cases. The tool name implies a read and its body could not be read
⚠️ A name is not a declaration. `fetch_` in HTTP code frequently means "make a request", and a `get_` on a search client can legitimately POST. This is reported because excluding it entirely would hide what the looser reading would have produced, and it is EXCLUDED FROM THE HEADLINE for the same reason it is caveated here.
provenance
name-implies-read-contradiction-by-tool in
aggregate.json — 2
+ 514 + 41 references,
one per member of each bucket, as salted identifiers
How many repositories register at least one tool that writes, sends, spends or deletes?
19.3% 16 of 83 repositories
The denominator excludes the 117 not-observable cases. No adapter observed any tool in the repository, so nothing can be said about what its tools do. Plus: nothing was read for these repositories: 1 scan_failed.
provenance
posture.registers_a_send_capable_tool in
aggregate.json — 16
+ 67 + 117 references,
one per member of each bucket, as salted identifiers
How many repositories configure any timeout, deadline or abort signal at all?
89.8% 176 of 196 repositories
The denominator excludes the 4 not-observable cases. The reading of the repository was incomplete, or no model identifier was recognised at all. Plus: nothing was read for these repositories: 1 scan_failed.
⚠️ Inferred from source and configuration text rather than from a declaration. A timeout set by a framework default, or a model pinned in a deployment manifest this scan never saw, would read as absent here.
provenance
posture.sets_any_timeout in
aggregate.json — 176
+ 20 + 4 references,
one per member of each bucket, as salted identifiers
How many repositories pin a dated or fully-qualified model version, rather than a floating alias?
48.6% 69 of 142 repositories
The denominator excludes the 58 not-observable cases. The reading of the repository was incomplete, or no model identifier was recognised at all. Plus: nothing was read for these repositories: 1 scan_failed.
⚠️ Inferred from source and configuration text rather than from a declaration. A timeout set by a framework default, or a model pinned in a deployment manifest this scan never saw, would read as absent here.
provenance
posture.pins_a_model_version in
aggregate.json — 69
+ 73 + 58 references,
one per member of each bucket, as salted identifiers
How many repositories interpolate a variable into a string used as a prompt or system message?
86.2% 169 of 196 repositories
The denominator excludes the 4 not-observable cases. The reading of the repository was incomplete, or no model identifier was recognised at all. Plus: nothing was read for these repositories: 1 scan_failed.
⚠️ Inferred from source and configuration text rather than from a declaration. A timeout set by a framework default, or a model pinned in a deployment manifest this scan never saw, would read as absent here.
provenance
posture.concatenates_untrusted_content_into_instructions in
aggregate.json — 169
+ 27 + 4 references,
one per member of each bucket, as salted identifiers
In how many repositories did any of the three adapters observe at least one tool?
41.7% 83 of 199 repositories
The denominator excludes the 1 not-observable case. Nothing was read for these repositories: 1 scan_failed
⚠️ An observed-absent here is a statement about the adapters, not about the repository. The three adapters read a documented pattern set; a repository defining its tools some other way is indistinguishable, to them, from a repository with no tools. This figure is published precisely so the headline can be read against it.
provenance
coverage.any-tool-observed in
aggregate.json — 83
+ 116 + 1 references,
one per member of each bucket, as salted identifiers
How many repositories are in a language one of the three adapters reads at all?
79.5% 159 of 200 repositories
The denominator excludes the 0 not-observable cases. None: the primary language is metadata from the corpus frame and is known for every entry, whether or not the repository was ever fetched or scanned
provenance
coverage.stack-is-readable in
aggregate.json — 159
+ 41 + 0 references,
one per member of each bucket, as salted identifiers
What the contradiction looks like
No instance of the primary contradiction survived the detector's rules on this corpus, so there is no pattern to show. That is the measurement, not an omission.
Disclosure posture
No repository is named anywhere on this page, and none will be before its maintainer has been contacted. Aggregate and anonymised statistics are published freely; an individual repository is not named as flawed until the person who maintains it has had the finding and a reasonable period to respond.
On this corpus the private disclosure ledger is empty. The primary contradiction figure is zero, so there is nobody to notify. The machinery is here anyway, tested and in the way, because the corpus with a non-zero figure is the one it has to be right about — and building it after finding something would be building it under pressure.
The provenance behind every figure above is a list of salted identifiers. They are complete — you can count them, and check that the buckets partition the population — and they do not resolve to a repository without a salt that is not published. Once any maintainer named in the private ledger has been notified, that salt can be released and every number here becomes attributable.
Notification is a human action. Nothing in the toolchain that produced this page can send a message, open an issue, or file an advisory, and none of it has network access.
What this does not establish
- Static analysis only. Nothing in this corpus was executed, so every finding is about what the code says and what it contains, never about what it does at run time.
- The detector reads each tool's own body and follows no calls. A tool that delegates to a helper that changes state is NOT counted. The primary figure therefore understates.
- HTTP POST and PUT are deliberately not treated as writes. Search, GraphQL, embedding and inference calls are POSTs that change nothing, and counting them would inflate the figure with false positives. Some genuine writes performed by POST are consequently missed.
- The three adapters read a documented pattern set. A repository that defines its tools some other way contributes a not_observable, and the coverage figures are published so this can be read against the headline.
- The corpus is popularity-weighted (every entry cleared 80 stars) and licence-gated. Both bias the sample toward more careful code, which is the conservative direction for a finding of this shape.
- Repository documentation was NOT used as a source of declared intent for the headline. A README describes a project; a contradiction has to be about a tool.
- A tool description asserting read-only is treated as a claim about that tool. It is not a contract, and no maintainer has agreed to be held to it.
- The scan is not bit-identical between runs. Re-running it moved one repository's Python adapter between `unavailable` and `partial` — a subprocess resource limit on a very large tree under concurrent scanning — which moved the tool count by ten. The state is visible rather than silent (see adapter_outcomes below), and an unavailable adapter contributes not_observable rather than zero tools, but figures at this precision can shift slightly on a re-run.
What the instrument could not see
tool.body_behaviour— A contradiction reached through a helper function is invisible. This reads the body of the tool itself and follows no calls, so a read-named tool whose delete happens one frame down is not reported.tool.body_behaviour— Dynamic dispatch — a handler selected by name at runtime, a registry populated from configuration, a method resolved through getattr — has no body this can locate statically.tool.body_behaviour— Bodies in languages other than Python, TypeScript and JavaScript are not read at all. The marker sets are language-specific and no attempt is made to guess at a third syntax.tool.self_description— A tool whose description is assembled at runtime, loaded from a file, or translated, has no static string to compare against its body.
This is a measurement of public code by one party, using one instrument, on one day. It is not a certification of anything, it confers no status on any repository named or unnamed, and it is not an audit. The standard it was measured against is itself a draft that no second party has applied.