From ai-plugin.json to SKILL.md
I shipped the same email-auth tool four times: as the SaaS emailauth.app, as a ChatGPT plugin in April 2023, as two Custom GPTs in late 2023, and now as a Claude Code skill. Each iteration deleted a layer of the stack. The agentic side won. The new customer is the agent, and the agent does not subscribe.
The earlier articles in this series (1, 2, 3, 4) argued that the buyer is changing, the product surface is changing, and the pricing model is changing. This piece is the receipts. The same small product, an email-authentication auditor, has existed in my portfolio in four shapes: first as a SaaS, then as a ChatGPT plugin, then as a pair of Custom GPTs, now as a Claude Code skill. Each new shape was triggered by a new integration layer underneath, and each new layer kept collapsing the one before it. I want to walk through the four shipping cycles with the actual filenames, because the filenames are the story.
Origin · SaaS
React front-end on a Cloudflare Worker. Type a domain, get a score across SPF, DKIM, DMARC and BIMI, with a history, an FAQ and a share link.
Before any of the AI-side shapes there was a SaaS. The page is built around DNS lookups, parsed against the RFCs, scored, painted into a results panel for a human visitor. It has a homepage, a /faq, a /privacy, a saved history in localStorage, a copyable share link. A normal product for a normal human visitor.
The hard part of building it was never the parsing. The parsing was maybe 600 lines of TypeScript. The hard part was every layer around it: the React routes, the auth, the rate limiter, the marketing page, the FAQ explaining what DMARC is to a CTO who searched the term on Google. The job was wrapped in a SaaS because in the pre-AI world a SaaS was the only way a piece of expertise reached someone who needed it. You had to publish a URL and hope a human visited.
Then the integration layer underneath started to change, and each change shed one of those wrapping layers.
April 2023 · ai-plugin.json
The same job, two weeks after OpenAI announced plugins. Manifest, OpenAPI spec, FastAPI backend, Dockerfile. A microservice the agent's vendor would phone home to.
OpenAI announced ChatGPT plugins on March 23, 2023. Two weeks later I pushed this repo. The createdAt field is 2023-04-06T19:04:03Z. It contains, in order of pain:
ai-plugin.json # the plugin manifest OpenAI's fetcher would hit
openapi.yaml # the OpenAPI 3.0 spec describing every endpoint
main.py # a FastAPI backend doing the actual work
Dockerfile
docker-compose.yml
requirements.txt
.env_example
README.md
To ship this plugin, the agent’s vendor (OpenAI) needed to read a manifest, fetch and parse an OpenAPI spec, present the human user with an installation prompt, store the consent, route every tool call back to my public URL with auth headers, and depend on me to keep that URL alive. I, in turn, needed a server, a domain, a TLS cert, a Docker setup, a logging pipeline, an SLO I could keep, and a plugin-store submission OpenAI would approve. The “tool” was maybe 200 lines of Python. Everything else was wrapper.
The good part of this era was that the wrapper was unambiguous: there was a single canonical manifest format and a single client. The bad part was that the wrapper was all I did. Adding a new endpoint meant edit main.py, edit openapi.yaml, redeploy, hope OpenAI’s fetcher re-pulled the spec. A typo in openapi.yaml was a production outage.
This was the first time it was possible at all to put a tool inside a model. The price was running a microservice for it.
November 2023 · Custom GPTs
System prompt plus an OpenAPI Action pasted into a textarea. No repo, no manifest, no plugin-store queue. The whole tool became a row in OpenAI's database.
A sibling Custom GPT for DNS lookups. Same shape: textarea, Action, avatar in a file picker. Same constraint: locked to chatgpt.com.
At OpenAI DevDay on November 6, 2023, plugins were de facto replaced by Custom GPTs. The store opened. The friction dropped through the floor. The system prompt was a textarea, the tool was an “Action” pasted as OpenAPI in a form field, the avatar was uploaded in a file picker. I still needed the backend behind the Action, but I no longer needed the manifest dance, the store submission, the consent dialog, or even Git.
This was the moment I stopped writing ai-plugin.json files entirely. One layer of the stack disappeared in eight months. The job stayed the same: take a domain, return its email-auth posture. The integration cost fell from “stand up a microservice and a submission” to “fill in a form.”
The bad part of this era was that the wrapper had migrated into the vendor’s UI. My GPT lived inside chatgpt.com. If OpenAI rolled it forward, fine; if they didn’t, fine. There was no portability. If I wanted a Claude or Gemini version, I started from scratch in their UI, with their fields.
2026 · SKILL.md
One Markdown file. No backend, no API key, no rate limit, no submission queue, no vendor UI. The model in the user's terminal runs the lookups itself, against its own resolver.
The thing I shipped today is one Markdown file long. The full structure of the plugin is:
email-auth/
├── .claude-plugin/plugin.json
└── skills/
└── check-email-auth/
└── SKILL.md
That is the entire artifact. The SKILL.md has YAML frontmatter declaring when the skill should trigger, and a body that teaches the agent which dig and curl commands to run and how to interpret the output. There is no backend. There is no server. The model itself, sitting in the user’s terminal, runs the lookups against whatever resolver the user has.
---
name: check-email-auth
description: Audit a domain's email authentication posture. Covers SPF,
DKIM, DMARC, BIMI, MTA-STS, TLS-RPT and DNSSEC. Trigger when the user
asks about email deliverability, DMARC records, SPF lookups, DKIM
selectors, BIMI logos, MTA-STS, or anything resembling "is my email
setup correct" for a domain.
---
Two install commands:
/plugin marketplace add emresavas/claude-code-plugins
/plugin install email-auth@emresavas
That is the whole shipping cycle. The repo is on GitHub, public, MIT, one commit. There is no second component anywhere. Side by side:
| Dimension | emailauth.app SaaS | Apr 2023 ChatGPT Plugin | Nov 2023 Custom GPT | May 2026 Claude Skill |
|---|---|---|---|---|
| Manifest | None (it is the app) | ai-plugin.json + openapi.yaml | OpenAPI in a textarea | plugin.json |
| Tool body | Cloudflare Worker | FastAPI service | FastAPI service | SKILL.md (instruction) |
| Hosting | Yes | Yes (Docker) | Yes (Action backend) | No |
| Reached by | Human via URL | OpenAI + a human installer | OpenAI + a human installer | Any agent with the plugin runtime |
| Install UX | Visit a website | Plugin store approval | Custom GPT row | /plugin install |
| Portability | The web | OpenAI only | OpenAI only | Anywhere with the Claude Code plugin runtime |
| Ship a fix | Deploy the Worker | Edit, deploy, wait for refetch | Edit form field, save | Edit Markdown, push |
- Manifest
- None (it is the app)
- Tool body
- Cloudflare Worker
- Hosting
- Yes
- Reached by
- Human via URL
- Install UX
- Visit a website
- Portability
- The web
- Ship a fix
- Deploy the Worker
- Manifest
- ai-plugin.json + openapi.yaml
- Tool body
- FastAPI service
- Hosting
- Yes (Docker)
- Reached by
- OpenAI + a human installer
- Install UX
- Plugin store approval
- Portability
- OpenAI only
- Ship a fix
- Edit, deploy, wait for refetch
- Manifest
- OpenAPI in a textarea
- Tool body
- FastAPI service
- Hosting
- Yes (Action backend)
- Reached by
- OpenAI + a human installer
- Install UX
- Custom GPT row
- Portability
- OpenAI only
- Ship a fix
- Edit form field, save
- Manifest
- plugin.json
- Tool body
- SKILL.md (instruction)
- Hosting
- No
- Reached by
- Any agent with the plugin runtime
- Install UX
- /plugin install
- Portability
- Anywhere with the Claude Code plugin runtime
- Ship a fix
- Edit Markdown, push
Four iterations, same job. Each iteration deleted a layer.
Why the layer keeps collapsing
The technical reason is that the model got better at executing tools and the runtime got better at exposing them.
In 2023 the model could not be trusted to issue arbitrary commands; the safe move was to constrain it to a fixed API surface. So the surface was a microservice with a known OpenAPI spec. The vendor’s job was to mediate every call. That is the world ai-plugin.json and the GPT Actions schema were designed for.
In 2026 the model in your terminal already has shell access through its agent runtime. It already has dig, host, curl, jq, whois. Giving it a tool is no longer “set up a network endpoint and document it”; giving it a tool is “tell it the right commands to run.” The instruction is the tool. SKILL.md is just the place where the instruction lives.
This changes the economics in three specific ways that matter for anyone building one of these things:
-
No HTTP round trip. When the SaaS answers, the answer is JSON behind a network boundary. The agent has to call out, wait, parse, re-enter its own context with the result. With the skill, the answer is text already inside the same conversation. The next reasoning step references it directly. The reasoning surface and the tool surface are the same surface, which is the point the tool-calling article was making in the abstract and which is the entire reason the layer keeps shedding in practice.
-
The
descriptionfield is more powerful than the endpoint path. A SaaS endpoint named/api/v1/dmarc/checkis matched by a brittle URL string and a fixed parameter schema. Thedescription:on a skill is prose the model reads in context, so the trigger condition can be semantic (“the user is asking whether their email is set up correctly, in any phrasing, including informal ones”). That is a far stronger contract for an LLM than any URL has ever been. -
No subscription seat to occupy. Per-seat pricing assumed there was a seat. The agent installing this skill is not occupying a seat; it is reading a file on disk. The per-seat article called this from the demand side; the supply side is what you are reading now.
The harder thing nobody is saying out loud
This is where I deviate from the “both still exist, pick the right tool for the job” softness that you usually hear in this kind of article.
The agentic side won.
The traditional SaaS business is over for tools at this scale. emailauth.app will stay up, because it is cheap to run and someone might still pay for the dashboard. But I am not building features for it anymore. The energy goes into the skill. Every tool-shaped product in my portfolio, a DNS lookup API, a WHOIS API, a DMARC parser API, an email intelligence API, an email deliverability API, the SERP APIs, every one of these built a small business around a thin layer of expertise on top of public data. Every one of them is being eaten in real time by the equivalent of a 300-line Markdown file installed by a single command.
The reason is not that the SaaS got bad. The SaaS is fine. The reason is the buyer changed. The first article in this series said it plainly: the consumer profile changed. A SaaS dashboard exists to give a human a place to read a score, copy a record, send a teammate a link. The agent does not need any of those things. The agent needs the result, in text, where it is already thinking. Everything else is gift wrap.
The buyer is the agent now. The agent does not subscribe. The agent does not click. The agent does not value your homepage, your onboarding, your branded PDF report, your /faq, your /privacy. The agent values: can I run this in my own context, with my own resolver, at my own latency, without an API key? If yes, the agent uses you. If no, the agent uses the next one.
The dashboard is the milkshake; I wrote about this earlier. The skill is what the agent buys instead, except “buys” is the wrong verb because there is no transaction. The skill is what the agent installs. The economics around what a tool is, who pays for it, and how it gets distributed are being rewritten in front of us, and the people running thin SaaS layers on top of public data are not going to like the next two years.
I have built six or seven of these myself. I am not making a prediction; I am telling you what I am doing about my own portfolio. The plan is to collapse every one of them into a skill, in this repo, in this shape, one at a time. The first is shipped.
The first one
emresavas/claude-code-plugins, plugin email-auth, skill check-email-auth.
What it does: takes a domain. Returns a structured Markdown report with PASS / WARN / FAIL per protocol (SPF, DKIM, DMARC, BIMI, MTA-STS, TLS-RPT, DNSSEC), a remediation section ordered by impact, and the raw records appended so the user can verify.
The non-obvious technical bits inside the SKILL.md:
- SPF lookup counter. The skill walks
include:chains recursively and counts each lookup against the RFC 7208 §4.6.4 limit of 10. Receiving MTAs will return PermError above that. Most SaaS auditors silently truncate; this one fails the audit and tells you which include pushed you over. - DKIM selector probe list. DKIM has no canonical record location, so I curated 24 selectors that cover Google Workspace, Microsoft 365, Fastmail, SparkPost, SendGrid, Mailgun, Mailchimp Transactional, Postmark, SocketLabs, Kinsta, Zoho, Proton, and the common legacy rotation names. The skill probes the list and reports both hits and tried-but-missing, so the user can see what was actually checked.
- DMARC severity ladder. Not a yes/no. Five rungs: missing >
p=none>p=quarantine; pct<100>p=quarantine; pct=100>p=reject. BIMI is reportedN/Arather thanFAILwhen DMARC is below quarantine, because BIMI literally cannot apply at that policy level and calling it a failure is misleading. - External
rua=authorization check. If the DMARCruamailbox is on a different domain, the skill does the second-order lookup at<audited>._report._dmarc.<reporter>to verify the reporter authorization, because most reporters silently drop unauthorized reports and most auditors do not check.
Install:
/plugin marketplace add emresavas/claude-code-plugins
/plugin install email-auth@emresavas
Pin the repo or do not. Open issues for weird domains. The DKIM selector list in particular is going to need a long tail of pull requests from anyone running mail through a provider I have not personally used.
The next plugins in this repo will be shaped the same way. Pure instruction. No service. No key. Each one removes a wrapper, the same way the manifest got removed, the same way the Custom GPT form got removed, until what is left is the actual expertise, written down once, runnable by an agent on a laptop with no network call to me.
The wrapper is the thing that goes away. The job stays. The receipts are in four shapes: a SaaS dashboard, an ai-plugin.json manifest, an OpenAPI textarea, a SKILL.md file. In that order, each smaller than the last.