Getting it into your agent
One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.
npx agentmods add instructions/erraggy/oastools/claude-mdgit clone --depth 1 https://github.com/erraggy/oastoolsWrote this? Show the measurements
A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.
[](https://agentmods.dev/instructions/erraggy/oastools/claude-md)<a href="https://agentmods.dev/instructions/erraggy/oastools/claude-md"><img src="https://agentmods.dev/badge/instructions/erraggy/oastools/claude-md.svg" alt="Measured on agentmods" height="20"></a>What it costs to keep this loaded
Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.
| Model | Per session | Once invoked |
|---|---|---|
| Fable 5 | $0.03811 | $0.03811 |
| Opus 5 | $0.01906 | $0.01906 |
| Sonnet 5 | $0.00762 | $0.00762 |
| Haiku 4.5 | $0.00381 | $0.00381 |
Grade A, and why
oastools CLAUDE.md scanned grade A with 0 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 3d ago.
A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.
Nothing flagged
None of the 26 patterns this scan looks for appear in this file: no shell pipes, no recursive deletes, no credential paths, no hidden text, no instruction-override or anti-refusal phrasing, no agent-config snooping. That is not a guarantee, it is the absence of the things that are checkable.
How it starts
The opening of the file, as written. The whole thing — 107 lines — stays where its author put it; the contents beside it link to each section on GitHub.
CLAUDE.md
⚠️ BRANCH PROTECTION: Never commit directly to main. A PreToolUse hook enforces this automatically.
Project Overview
oastools is a Go CLI for OpenAPI Specification files. Validates, fixes, joins, converts, diffs, walks, generates, and builds OAS 2.0-3.2.
Style
- Emojis welcome in PR descriptions and release notes, but not required in code or docs
- GitHub formatting: Bare hashes/issues auto-link; backticks break linking
- Good:
Fixed in commit 1f3eb93→ clickable - Bad:
Fixed in commit \1f3eb93`` → not clickable
- Good:
Quick Reference
make checkbefore committing- Conventional commits:
feat(parser): add feature - Never amend a pushed PR commit once review has begun — add a new commit. PRs squash on merge, so amending only breaks review diffs and comment anchors
- See WORKFLOW.md for PR/release process
- See AGENTS.md for agent workflow
Architecture
| Package | Purpose |
|---|---|
| cmd/oastools/ | CLI entry point |
| parser/ | Parse YAML/JSON OAS, resolve refs, detect versions |
| validator/ | Validate against spec schema |
| fixer/ | Auto-fix common errors |
| joiner/ | Join multiple OAS files |
| converter/ | Convert between OAS versions |
| differ/ | Compare specs, detect breaking changes |
| httpvalidator/ | Runtime HTTP validation |
| generator/ | Generate Go client/server |
| builder/ | Programmatic spec construction |
| overlay/ | Apply Overlay transformations |
| walker/ | Traverse with typed handlers |
Key Patterns
- Format preserved: JSON/YAML auto-detected from extension or content
- Use constants:
httputil.MethodGet,severity.SeverityError - Always run
go_diagnosticsafter edits—hints improve perf 5-15% - Favor fixing immediately over deferring issues
- Deep copy: Use generated
doc.DeepCopy()methods, never JSON marshal/unmarshal (losesinterface{}types, dropsjson:"-"fields) - "No conversion needed" is not "same object": A field needing no per-version work is still a field two documents will share if you assign it straight across.
converterhit this at 23 sites (Info, tags, externalDocs, security, response headers, OAuth scope maps) afterjoinerhit it in 8c55687, and Info was passed through whole, so a write to the converted document changed the source's title. When building an object field by field, copy:parser.DeepCopyExtensionsforExtra,parser.DeepCopySecurityRequirementsforSecurity,DeepCopy()for anything that has one, andconverter/deepcopy.gofor the shapes with neither - A deep copy is not a conversion: the inverse of the bullet above, and #532 was both halves of it at different scales.
parser.Headerwas copied across whole, so its Schema reached no pass, andconvertOAS3ToOAS3copied the whole document, so a 3.0 to 3.1 conversion ran no per-schema pass at all. Both emitted the source version's spelling into a document declaring the target, andvalidateaccepted the result, which is why neither showed up as a failure. When a field's type is the same in both versions, ask whether its contents are version-spelled before reaching for a copy - Choosing one media type out of a content map has one owner: OAS 2.0 admits a single schema where OAS 3.x offers several, and ranging the map to pick one makes the output depend on Go's map order.
internal/httputil.MediaTypeRankandPreferredMediaTypeown the order (application/json, then a+jsonsuffix, then everything else, with the name breaking a tie), andinternal/schemautil.SortedContentTypesapplies it to a content map. #533 and #535 fixed the same defect inconverter,generatorandbuilderacross two PRs, so a fourth package writingfor ct := range content { ...; break }is the shape to catch in review. Each caller still applies its own predicate:converter.selectContentSchemaskips a media type carrying no schema, since selecting it would lose the schema a sibling was offering - Put a refusal at the shared entry point, not the convenience wrapper:
converter's parse-error guard sat inConvert, which reads a file, while every other caller arrives throughConvertParsed. The same document was refused from a file and converted from stdin - A new
parserfield has five homes, andinternal/driftguardis what catches the ones you miss: the hand-builtMarshalJSONpath, the structural hasher ininternal/schemautil, the type's equality function, the joiner's schema comparison, and the deepcopy generator's field list. The guard is test-only and reflects over the struct, so a missed home is a test failure rather than a silent bug. Read its failure message before assuming the guard is wrong: it names the field and the home - Two passes that rank the same names must share the ranking function:
joinerdecides which of several equivalent schema names survives in two places, theStrategyDeduplicateOrRenamecollapse and theSemanticDeduplicationpass. The collapse ranked byoutranks(a name no rename generated beats one a rename produced), the second pass sorted alphabetically, and with both enabled the second consolidated into the generated alias and dropped the name every document wrote (#498). The fix is one function reached from two places:internal/schemautil.DeduplicationConfig.Outranks, whichjoinerfills fromoutranksGeneratedandbuilderleaves nil for the alphabetical default. The two passes have to agree only where one of them deletes a name:DeduplicationModePointerkeeps every name, so it fillsOutranksfromoutranksDeclarationinstead (#553). Order matters too: take the generated-name set before the collapse, sincerenameScope.redirectpoints a rename at the name the class kept, so a set taken afterwards reports that surviving name as generated - A generated name is only unique against the documents behind it:
uniqueSchemaNamerefuses a name the join already holds, which is what #483 established, but the map it checks holds only the documents merged so far. A document merged later can declare the very name a rename just minted, and then two things go wrong at once: the minted name keeps the spelling and the author's declaration is suffixed out of it, and the surviving name is recorded as generated even though a document declared it, sooutranksGeneratedranks a published name as an alias andSemanticDeduplicationfolds it away (#547). Neither shows up as a failure, since the document still validates and every reference still resolves.JoinResult.reservedholds every name any source declares, spelled as the merge will store it (soAlwaysApplyPrefixreserves the prefixed name), and it is built on the first rename that asks for it, so a join renaming nothing does not pay for it. When a guard reads accumulated state, ask what a document merged later can still add. - Semantic deduplication compares shapes, so a name's meaning has to come from somewhere else: two equal-comparing schemas have only their names telling them apart, and a schema referencing both relies on that: merging
OriginAddressandDestinationAddressunder aShipmentrequiring both gives a document that validates and says a shipment's origin is its destination (#501).internal/schemautil.DeduplicationConfig.Splitpartitions a group of equivalent names into the parts that may each collapse, andjoinerandbuilderfill it frominternal/schemarefs.Collect. The unit is the schema tree (a schema with no schema above it), so depth does not matter,oneOfalternatives are held apart, and an inline parent counts like a named one. Sharing an operation is not sharing a tree, so a matching request body and response still merge - Schema-or-bool fields are always promoted:
Items,AdditionalProperties,AdditionalItems,UnevaluatedItemsandUnevaluatedPropertiesareany, but every decode path (JSON, YAML,decodeFromMap) yields*Schema,[]*Schema(OAS 2.0 tuple form), orbool. Amap[string]anyarm is dead for parsed documents. Never type-assert to*parser.Schemaalone: that drops the tuple form, and #502 was ~50 sites across 7 packages doing exactly that, so-prunedeleted a schema a tuple element referenced andconvertleft the same $ref pointing at#/definitionsin an OAS 3 document. Iterate withinternal/schemautil.SchemaOrBoolSchemas, which yields each contained schema with its index, and build paths with itsIndexSuffixsoitemsanditems[0]stay distinct in error messages. The structural hasher,parser'sequalSchemaOrBoolWithVisitedandjoiner'scompareSchemaOrBoolcarry the tuple arm too, since a hash that ignores tuples buckets schemas the comparison then calls different.parser/equals.go'sequalSchemaOrBooldeliberately has none: its only caller intercepts[]*Schemafirst, so the arm is unreachable.differcarries it as of #511: a tuple classified as unknown drops every change inside it (both an element edit and a length change), and the%Tfallback message it used to print named a Go type for a perfectly legal OAS 2.0 document internal/schemautil.SchemaTupleowns whether a field IS the tuple form: it returns the positions and a bool, and the bool is the answer, neverlen(tuple) == 0. An empty tuple is still the tuple form, and draft 4 gives it a meaning: it names no position, soadditionalItemsgoverns every element andadditionalItems: falseadmits only an empty array.httpvalidatorandgeneratoreach had their owntupleItemSchemasand they disagreed on exactly that case, soitems: []withadditionalItems: falserejected a one element request while the generated type constrained nothing (#529). A package may still decide what to DO with the form, under a name that says so:generator.structTupleSchemasreports no struct for an empty tuple because there is no position to give a field to, which is a decision about output rather than about what the schema meansSchemaOrBoolSchemasskips nil elements, so a paired walk must not use it: comparing two tuples position by position (differ.diffSchemaTupleUnified) indexes the slices directly, because a nil present on one side alone would shift every later index and misreportitems[i]. Nil elements are real: YAML decodes- nullto one, while the JSON path drops it and changes the tuple's length (#510)- An OAS 2.0 constraint is often on the object, not in a schema: only a body parameter has a
schema. Every otherinputstype,enumand the rest on the parameter object itself, as does a response header, as does either one'sItemschain, and OAS 2.0 offers nowhere else to put them. A pass reaching a constraint throughParameter.Schemaalone expanded a body parameter's CSV enum and left the identical value on a query parameter (#513). The fix shape is to make the core take the constraint rather than the container:fixer's expansion takes(type string, enum *[]any), soparser.Parameter,parser.Header,parser.Itemsandparser.Schemaall reach it despite sharing no type. Before reaching for.Schema, ask whether the OAS 2.0 form declares the thing inline parser.GetOperationsflattensadditionalOperations, and a reported path has to re-separate it: the map it returns keys a custom OAS 3.2 method by its own name, beside the standard ones, so a caller building a path from the key writespaths.{p}.PURGEwhere the document sayspaths.{p}.additionalOperations.PURGE.walker/walk_oas3.go,validator/schema_traversal.goandconverter/oas3_schema_positions.goall spell it the long way;fixer.operationPathSegmentis the fourth. The version matters too, sinceGetOperationsreturns a custom method only at 3.2 and TRACE only at 3.0, and everyfixerpass reads the version from the document rather than from theParseResultit routed on. A document built in Go carries none, sofixOAS3adopts the parse result's version at the entry point rather than leaving each pass to treat it as the oldest OAS 3- Discriminator has two dialects: OAS 2.0 spells
discriminatoras a bare string, OAS 3.0+ as an object. Both decode intoparser.Discriminator;StringForm boolrecords which. The parser accepts either (it cannot see the document version), the validator rejects the wrong one for the version, and the converter flips the flag in both directions.StringFormis excluded from JSON, YAML, andequalDiscriminator— it is spelling, not meaning make checkbefore pushing — not justgo test; catches lint, formatting, and trailing whitespacedocs/is mixed source + generated: Source files (index.md,mcp-server.md,cli-reference.md, etc.) are edited directly indocs/. Generated files (docs/packages/,docs/examples/) come from{package}/deep_dive.mdandexamples/*/README.md— see.claude/docs/docs-website.md- MCP config via env vars: The MCP server reads
OASTOOLS_*env vars for configuration (cache TTLs, walk limits, join strategies, etc.). The Go MCP SDK doesn't supportinitializationOptions, so env vars are used instead. MCP clients set these via theirenvfield in server config. - Component-name charset has one definition:
internal/namingowns it —ComponentNamePattern(string, for error messages),IsComponentNameChar(per-rune, for the fixer building replacements),IsValidComponentName(whole-name, for the validator). Never restate the pattern elsewhere; bothvalidatorandfixerconsumeinternal/naming. A drift-guard test compares the compiled pattern against the predicate over every rune through U+0700 - OAS versions disagree on schema-name legality, deliberately: OAS 3.x Components keys are an allowlist (
^[a-zA-Z0-9._-]+$), so a denylist can never keep up —pkg/Pet,Pet@v1,pet~summary,Pétare all illegal without sharing a character. OAS 2.0 places no charset constraint ondefinitionskeys, so those names are valid and renaming them would rewrite a valid document; the fixer's character denylist applies there only.fixer.charsetForVersionis the switch $reftokens: look up exact-first, decoded-second: Generators mix escaping conventions (e.g. percent-encoding brackets while leaving slashes raw — neither pure RFC 6901 nor pure percent-encoding).pathutil.DecodeRefTokenreverses both, but it's lossy: a component genuinely namedFoo%20Bardecodes toFoo Barand stops matching itself. Index rename maps viafixer.lookupRenamedRef(checks the exact ref first, decoded second), and register decoded keys in sorted order — two names can share a decoded form without either being it, making map-range order-dependent
What this file has done since we first saw it
Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.
- 3d ago First seen · 107 lines · 3,811 tokens per session scan A 806ed0db2074
oastools CLAUDE.md is an instructions file published in the GitHub repository erraggy/oastools (5 stars, last pushed 5d ago), licensed MIT. It adds 3,811 tokens to every session, about $0.0191 per session on Opus 5. A static security scan graded it A with 0 findings. No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-31.
Other instructions, from other repositories
scalar AGENTS.md
Instructions for scalar/scalar, covering agents.md - ai agent guide for scalar, project overview, prerequisites, first-time setup and commands.
jentic-public-apis AGENTS.md
Instructions for jentic/jentic-public-apis, covering agents.md, repository structure, for ai coding agents, contributing and related standards.
lathe CLAUDE.md
Instructions for lathe-cli/lathe, covering claude.md, project intent, product positioning, source of truth and project structure.
apifable AGENTS.md
Instructions for ycs77/apifable, covering agents.md, keep this file strict, constraints, commands and rules.
scalar CLAUDE.md
Instructions for scalar/scalar, a project described as: Scalar is an open-source API platform: 🌐 Modern REST API Client 📖 Beautiful API References ✨ 1st-Class OpenAPI/Swagger Support.
climate AGENTS.md
Instructions for disk0Dancer/climate, covering agents contribution workflow, required sequence for any feature, quality rules and feature checklist template.