Design notes

July 30, 2026 · View on GitHub

Why the plugin is shaped the way it is. Constraints and decisions, not a description of the files.

The problem it solves

An agent asked "how do I contact Twilio support" will answer. The answer will have the right shape and will frequently be wrong. Support routing details are exactly the kind of fact a language model reproduces confidently and cannot check: they are low-salience, they change without announcement, and the wrong answer costs a support window rather than failing loudly.

Two findings from verifying Twilio show how thoroughly this fails. help@twilio.com is guessable, plausible, widely repeated, and non-existent — the article whose whole job is explaining how to contact Twilio support carries no mailto: link at all. And spam@twilio.com, which Twilio does print, is not the address the link points at: the mailto: behind it is stopspam@twilio.com. So the wrong address is not merely a plausible invention, it is the one a correct reading of the visible page produces. Anything downstream of a text scrape inherits it.

So the registry is not a convenience cache. It is the thing that makes a confident answer permissible at all. Everything else follows from that.

Why every fact carries a date

An undated support address is indistinguishable from a guess after six months. last_verified plus revalidate_after_days makes staleness computable rather than something a reader is expected to notice, and scripts/check_registry.py computes it so that no skill has to remember to.

The corollary is the rule in support-revalidate: never restamp a claim you did not check. A blanket restamp converts an honest stale entry into a dishonest fresh one, which is strictly worse than leaving it stale.

Why confidence has three values

confirmed / referenced / inferred exists because bot walls and login gates make "reached it directly" impossible for a large share of real support routes. Without the middle value the choice is between overstating and discarding, and both lose information. referenced — the vendor names it, we could not reach it — is an honest and useful state, and roughly half the Twilio entry sits there.

evidence is required on anything claiming confirmed, enforced by the validator, because "confirmed" with nothing behind it is just a guess with better formatting.

Why known_dead exists

It is the highest-value field in a mature entry and the one most likely to be skipped, because recording a negative result feels like recording nothing.

The Twilio pass found nine dead routes, and the ones that matter are invisible to any automated link check: a 301 fronting an expired Discord invite, a 200 fronting an empty client-rendered shell, and an address that is printed correctly and linked wrongly. Without known_dead, every future session and every revalidation pass rediscovers those the same way, at the same cost. With it, the entry gets better with age instead of merely older.

This is also why scripts/check_registry.py does not check reachability. A status code is not a verdict, and a script that reported all nine as healthy would be worse than no script.

Why the research procedure insists on hrefs and archives

Two techniques earned their place in support-add-service and support-revalidate by being the only things that worked on Twilio.

Extract mailto: hrefs, not prose. A page's visible text and its link targets can disagree, and when they do, every text-based approach — search snippets, markdown converters, models — is wrong and every href-based approach is right. This is a one-line grep and it is the difference between a working address and a dead one.

A retired help centre is often more readable than its replacement. help.twilio.com gives a fetcher nothing but a <title>; the support.twilio.com Zendesk it replaced was server-rendered, article IDs survived the migration, and the Wayback Machine has it. That path yielded the plan/channel matrix, the priority definitions and the mailto: mismatch — all of which had been guesses. The cost is that a fact read from a snapshot is confirmed as of the snapshot's date, not today, so each one is dated individually in docs/verification-log.md.

Why user data lives outside the plugin

~/.claude-plugins/contact-support/ rather than anywhere under the plugin directory, because /plugin update replaces the plugin directory. Account profiles, ticket logs and user-authored entries would be destroyed by a routine update, silently, at the moment they were least expected to matter. This is the one structural rule of the plugin and the merge semantics in references/user-data-layout.md exist to serve it.

Why credentials are pointers

Support desks legitimately need account numbers — that is what identifiers is for, and those get sent. They never need credentials.

Storing a pointer instead of a value (op://Private/Twilio/credential, an env var name, a vault item title) means the agent can tell the user where to look without the secret entering a transcript, a draft, or a saved file. It is a functional decision about what goes into outbound messages, not a posture: the agent's job includes writing text that leaves the machine under the user's name, and the only reliable way to keep a secret out of that text is to never hold it.

secret: true on an identifier exists so a field can be recognised and refused. Twilio's Auth Token is listed for that reason alone.

Why drafting and sending are separate skills

Because sending is irreversible and drafting is not. support-request produces a draft and stops; support-send confirms the destination, quotes it back, and waits for a yes. Approval covers one send — not the next message, not a retry through a different channel after a failure.

The specific failure this guards against: a login-gated ticket form is the right channel and the hard one, while a public forum is the wrong channel and the easy one. An agent optimising for completion will reach for the reachable-but-wrong channel, and the result is the user's problem published, with identifiers attached, somewhere that cannot solve it. Hence not_for, hence agent_reachable as a first-class field, and hence the rule that a user-only channel produces a briefing rather than a substitution.

Why the ticket log exists

support-followup needs something to run on, and response_due is the field it runs on. A ticket nobody counts the days against is a ticket nobody answers — and the most effective escalation sentence available is the vendor's own commitment quoted back with the elapsed time, which requires having recorded both.

The log's ## Registry changes made as a result section is what closes the loop. An entry holding only what a vendor publishes about itself is worth much less than one holding what happened when you dealt with them: which queue bounced the category, what the first response actually took, which form field was really required.

Why it ships with one service

Twilio, verified properly, is a better artefact than twelve services verified by assumption. The entry is meant to be read as the standard for what an entry looks like — separate_desks, known_dead, graded confidence, evidence strings — and support-add-service is the path for everything else. A registry of guesses would defeat the plugin's only real purpose.

Deliberately absent

  • No reachability check in the validator. Reasoned above.
  • No probe emails. Verifying an address by mailing it creates a ticket someone has to close. Addresses are re-verified by confirming the vendor still publishes them.
  • No credential handling of any kind, including for browser logins. A browser-driven step requires the user to already be signed in.
  • No auto-send. There is no flag or setting that skips the confirmation.
  • No scraping of vendors' ticket queues. Ticket state is recorded from what the user and the vendor's own replies provide, not by polling an authenticated portal on a timer.