Skills
How ShipAgent's skills are built, how their check scripts prove each step, where they live, which agents can run them, and how to write and test your own.
A skill is a Markdown file that tells an AI agent how to do one job in this repository: what the result must be, which values to ask you for, which commands to run, how to prove it worked, and what to do when a check fails. ShipAgent ships one skill per integration and per deploy target. You can write more for your own integrations with the same structure.
Where skills live
| Path | What it is |
|---|---|
skills/<name>/SKILL.md |
the skills themselves, the only copy to edit |
skills/README.md |
the rules every skill follows and the index of skills |
skills/reference/ |
reference docs on how the code is organized; no checks to run |
.claude/skills/<name> |
links into skills/ so Claude Code finds them; never edit there |
AGENTS.md |
the entry point for Codex, Cursor and other agents: what to read first, and a table of which skill to use when |
CLAUDE.md |
the project map: every directory and the architecture rules. Each module has its own CLAUDE.md |
scripts/check-*.ts |
the check scripts the setup skills end with |
.claude/skills/ also holds general-purpose skills that are not links (for example debugging, code review and frontend design). They help Claude Code write code; they do not set anything up.
Which agents can run them
The files use the Agent Skills format: a folder with a SKILL.md that starts with name and description frontmatter.
- Claude Code loads skills from
.claude/skills/and picks one by itsdescription. - Codex and Cursor read
AGENTS.md, which names the skill file for each job. - Any other agent works if it reads
AGENTS.mdand can run shell commands. A skill lists every command it runs, so it does not depend on one vendor's features.
An agent without a shell cannot run the check scripts and so cannot finish a skill.
Kinds of skills
| Prefix | Job | Ends with |
|---|---|---|
setup-* |
connect one integration: database, auth, Stripe, email, storage, AI | a check script in scripts/check-*.ts that must print all checks passed |
deploy-* |
ship to one platform: Cloudflare Workers or Vercel | the deployed URL answering 200 on /api/health |
add-* |
build one more piece of product, such as a paid tool page | type check, unit tests, one end-to-end run |
reference/ |
how the code is organized, for changes the skills do not cover | nothing to run |
Rules every skill follows
From skills/README.md:
- State the outcome first and the command that proves it. "Done" means that command passed, never "I edited the files".
- Offer choices, never open questions. When a decision is yours (which database host, which sign-in methods, what to sell, which AI provider, which platform), the agent shows a short numbered list with the recommended option first and one line on what each means, and waits for a number. Every skill has a Choices section with its lists. In Claude Code the list appears as a question dialog; in Cursor and Codex it is a numbered list in the chat. One question at a time.
- Ask for a value only when the step that needs it is reached, one value at a time, naming the exact variable and the exact click path. Never print a key back, never commit one.
- Mark the human steps (
⚠ needs you): creating an account, pasting a key, clicking a setting the API cannot change, verifying a domain. Stop there, say what to do, wait for "done". - Prefer the project's scripts over hand-typed API calls:
pnpm stripe:bootstrap,scripts/check-*.ts,scripts/cf-new-site.mjs,drizzle-kit migrate. - Test mode first, live at launch. Every
setup-*skill ends by listing what changes for production. - When a check fails, read the table at the bottom of the skill before guessing.
How a skill is built
Here are the real sections of skills/setup-stripe/SKILL.md, shortened. Every setup skill has the same parts in the same order.
Frontmatter
The description says what the skill does and when to use it. Agents pick a skill by matching your request against it, so it names the symptoms too ("when STRIPE_SECRET_KEY is missing", "when config/pricing.ts still has price_REPLACE placeholders").
---
name: setup-stripe
description: Connect Stripe to this project and prove a payment works end to end — key, products and prices from config/pricing.ts, webhook endpoint and secret, one signed test event that becomes an order. Use when STRIPE_SECRET_KEY or STRIPE_WEBHOOK_SECRET is missing, when config/pricing.ts still has price_REPLACE placeholders, or when the user says "set up payments / Stripe".
---
Outcome first
One command, one line it must print. Then how long it takes and what the person does.
**Outcome:** `pnpm tsx scripts/check-stripe.ts --fire <userId>` prints `Stripe: all checks passed`. Nothing is done until it does.
**Takes:** about 10 minutes of agent time, and the user pastes two values and clicks once.
What you need from the user
Every value the person provides, where they get it, and where the agent writes it.
| Value | Where the user gets it | Where you put it |
|---|---|---|
| `STRIPE_SECRET_KEY` | Stripe Dashboard → Developers → API keys → Secret key. **Use the `sk_test_…` key until the whole flow passes**, switch to `sk_live_…` at launch | `.env.local` (local), the deployment env (production) |
| `STRIPE_WEBHOOK_SECRET` (local only) | Printed by `stripe listen` (step 4a) | `.env.local` |
| Payment methods | Stripe Dashboard → Settings → Payment methods (card is on by default; Alipay, WeChat Pay etc. are a click each). You cannot do this through the API | nothing to write |
Never print a key back to the user, never commit one, never paste one into a chat answer.
Steps, each ending in a check
Numbered steps. Most end by running the check script again and naming the line that must now appear.
### 1. The key
1. Read `.env.local`. If `STRIPE_SECRET_KEY` is empty, ask the user for it (table above) and write it in.
2. Run `pnpm tsx scripts/check-stripe.ts`. The first line must be `✓ STRIPE_SECRET_KEY valid`.
### 2. The plans
### 3. Products and prices in Stripe
### 4. The webhook
Verify
The last step proves the whole flow with a real event, then asks the person for one real test.
### 5. Verify: one payment, end to end
3. `pnpm tsx scripts/check-stripe.ts --fire <userId>` sends a signed `checkout.session.completed` for the first one-time plan to the app, expects `200`, and checks that an `orders` row with the right plan and amount exists (then deletes it again). All lines `✓` → done.
4. Then one real test payment through the pricing page with card `4242 4242 4242 4242`, any future date, any CVC.
What is left for launch, and where the code lives
### 6. Tell the user what is left for launch
- Switch `STRIPE_SECRET_KEY` to `sk_live_…`, run `pnpm stripe:bootstrap --write` once more (fills the `live` column), run step 4b and step 5 against the live URL with a real card.
## Where the code lives (for changes beyond setup)
- `config/pricing.ts` — plans, the only source of truth. Never delete a sold entry; retire with `active: false`.
- Full reference: `skills/reference/payments.md`.
When a check fails
A table keyed by the exact output line, so the agent looks up the fix instead of guessing.
| Output | Cause | Fix |
|---|---|---|
| `STRIPE_SECRET_KEY rejected` | wrong key, or a restricted key without read access | paste the standard Secret key |
| `price … not found in test mode` | the id in the file belongs to the other mode | run the bootstrap with the matching key |
| `--fire` → `400 Webhook Error` | `STRIPE_WEBHOOK_SECRET` is not the secret of the endpoint that is calling | local: the value printed by `stripe listen`; production: the registered endpoint's secret |
| `--fire` → `200` but `no order row` | the app runs against another database than `DATABASE_URL` here | point both at the same database |
The check scripts
Each setup skill ends with a script in scripts/ that tests the real thing: a real connection, a real API call, a real request to the running app. Run any of them yourself with pnpm tsx scripts/<name>.ts.
| Script | What it proves | Options |
|---|---|---|
check-database.ts |
connects with the app's own settings, every table and column of lib/db/schema.ts exists, migrations are applied, a write and read inside a rolled-back transaction, a superadmin exists |
--env <file> |
check-auth.ts |
secret and site URL are valid, each provider's keys come in pairs, /api/auth/ok answers, the Google redirect carries the right client_id and redirect_uri, a session signed with your secret is accepted by the running app |
--env-only, --url, --env, --keep |
check-stripe.ts |
the key works, every Stripe plan in config/pricing.ts has a price with the right amount, currency and interval, the webhook endpoint has every event the app handles |
--register, --fire <userId>, --url, --env, --keep |
check-email.ts |
the sender address, the Resend key, the sending domain is verified | --create-domain, --to <address> |
check-storage.ts |
the R2 values, the bucket is reachable, CORS allows the browser upload, a 1 MB file goes up through a presigned URL, comes back byte for byte, is served by the public URL, and is deleted | --apply-cors, --origin <url> |
check-ai.ts |
the model registry, the provider keys, one tiny chat call through the app's own lib/ai code, webhook URLs and Redis for video, every generation route charges credits and refunds failures |
--url, --image, --image-model, --write-secret, --env |
Output and exit code
Every line has one of four shapes. From scripts/check-stripe.ts:
const ok = (msg: string) => console.log(`✓ ${msg}`);
const bad = (msg: string) => {
failed += 1;
console.log(`✗ ${msg}`);
};
const you = (msg: string) => console.log(`⚠ needs you: ${msg}`);
const dim = (msg: string) => console.log(` ${msg}`);
The script ends with Stripe: all checks passed and exit code 0, or <n> check(s) failed and exit code 1. Every check script exits 1 until the integration really works. An agent cannot talk its way past a non-zero exit code, which is why a skill is done only when its check passes.
A first run, before any key is set:
$ pnpm tsx scripts/check-stripe.ts
✗ STRIPE_SECRET_KEY is not set in .env.local
⚠ needs you: Stripe Dashboard → Developers → API keys → copy the Secret key (sk_test_… while testing) and paste it into .env.local as STRIPE_SECRET_KEY
1 check(s) failed
The --fire idea: a cheap default, then the real thing
Without options, a check is safe to run any time: it reads settings and makes free, read-only calls. One option per script then does the thing a customer would trigger, for real:
| Option | What it really does |
|---|---|
check-stripe.ts --fire <userId> |
sends a signed checkout.session.completed to the running app and checks that an order row appears, then deletes it |
check-email.ts --to <address> |
renders the app's sign-in template and sends it through the app's mail code |
check-ai.ts --image |
generates one small image with a cheap model; costs a few cents |
check-ai.ts --url <app> |
calls the image route signed out and expects the sign-in wall |
check-auth.ts (with the app running) |
writes a probe user and session, signs the cookie with your secret, and expects the app to return that user |
Other options let the script fix what it found instead of only reporting: --register creates the Stripe webhook endpoint and writes its secret, --create-domain adds your sending domain to Resend and prints the DNS records, --apply-cors writes the R2 CORS rule, --write-secret saves Replicate's webhook signing secret.
Production
The same scripts check production. --env <file> points them at the production env file (cloudflare/.env.production.local or .env.production.local) and --url at the live site, for example:
pnpm tsx scripts/check-database.ts --env cloudflare/.env.production.local
pnpm tsx scripts/check-auth.ts --url https://your-domain.com --env cloudflare/.env.production.local
Write your own skill
Write a skill when you add an integration the agent should be able to set up again: another payment provider, a search service, an analytics tool, a second database. Copy the structure of setup-stripe; agents follow a structure they have seen before more reliably.
1. The skill file
Create skills/setup-<name>/SKILL.md:
---
name: setup-<name>
description: Connect <service> to this project and prove it works — <the pieces: key, resource, webhook, one real call>. Use when <ENV_VAR> is missing, when <symptom the user would see>, or when the user says "set up <service>".
---
# setup-<name>
**Outcome:** `pnpm tsx scripts/check-<name>.ts` prints `<Name>: all checks passed`. Nothing is done until it does.
**Takes:** about N minutes of agent time; the user <creates one account, pastes one key>.
**Needs first:** `setup-database` passed (if it stores anything).
## Choices (ask before step 1, as a numbered list)
**<The decision that is the user's>**
1. **<Option A>** — recommended. <What it means for them, one line.>
2. **<Option B>** — <when to pick it>.
## What you need from the user
Ask for these only when the step that needs them is reached, one at a time, and say exactly where to click:
| Value | Where the user gets it | Where you put it |
|---|---|---|
| `<ENV_VAR>` | <dashboard> → <menu> → <button>. Shown once | `.env.local` (local), the deployment env (production) |
| <a setting the API cannot change> | <dashboard> → <menu>. You cannot do this through the API | nothing to write |
Never print a key back to the user, never commit one, never paste one into a chat answer.
## Steps
### 1. The key
1. Read `.env.local`. If `<ENV_VAR>` is empty, ask the user for it (table above) and write it in.
2. Run `pnpm tsx scripts/check-<name>.ts`. The first line must be `✓ <ENV_VAR> valid`.
### 2. <The next piece> (⚠ needs you)
1. <What the agent does.>
2. <What the user does, exactly; wait for "done".>
3. Rerun the check: `✓ <line>` must appear.
### 3. Verify: one real <call / event / mail>
1. `pnpm tsx scripts/check-<name>.ts --fire` <does the real thing>.
2. Then one real test by the user: <what they do, what they should see>.
### 4. Tell the user what is left for launch
- <live keys, production URLs, quotas, spending limits>
## Where the code lives (for changes beyond setup)
- `lib/<name>/…` — <what>.
## When a check fails
| Output | Cause | Fix |
|---|---|---|
| `<exact line the check prints>` | <cause> | <fix> |
Write the click paths from the service's real dashboard, and say in the skill when you could not check one. The agent repeats them to the person word for word.
2. The check script
Create scripts/check-<name>.ts. Copy the helpers and the ending from an existing check so the output looks the same everywhere:
import { loadEnvConfig } from "@next/env";
loadEnvConfig(process.cwd()); // reads .env.local like the app does
const args = process.argv.slice(2);
const flag = (name: string) => args.includes(`--${name}`);
let failed = 0;
const ok = (msg: string) => console.log(`✓ ${msg}`);
const bad = (msg: string) => {
failed += 1;
console.log(`✗ ${msg}`);
};
const you = (msg: string) => console.log(`⚠ needs you: ${msg}`);
const dim = (msg: string) => console.log(` ${msg}`);
async function main() {
const key = process.env.EXAMPLE_API_KEY;
if (!key) {
bad("EXAMPLE_API_KEY is not set in .env.local");
you("example.com → Settings → API keys → Create → paste it into .env.local as EXAMPLE_API_KEY");
return;
}
// 1. one cheap, read-only call that proves the key works
// 2. every resource the app needs exists, with the right settings
// 3. --fire: the real thing, through the app's own code or routes; clean up afterwards
if (flag("fire")) {
dim("sending one real request …");
}
}
main()
.then(() => {
console.log(failed ? `\n${failed} check(s) failed` : "\nExample: all checks passed");
process.exit(failed ? 1 : 0);
})
.catch((error) => {
console.error(error);
process.exit(1);
});
What makes a check worth having:
- It calls the app's own code (the same helpers, the same routes), not a copy. A pass then means the app works, not only the key.
- Every
✗has a row in the skill's failure table, with the same wording. - It never prints a secret. Print the variable name, a host, or an account id.
- It cleans up test rows, files and users, unless asked to keep them (
--keep). - The default run is free and safe to repeat. Anything that costs money or sends something waits for an option.
3. Register it
- Link it for Claude Code:
ln -s ../../skills/setup-<name> .claude/skills/setup-<name>. - Add a row to the skill table in
AGENTS.md(when to use it) and to the index inskills/README.md(outcome and human steps). - Mention the new check script in the
scripts/line ofCLAUDE.md.
Test a skill with a weaker model
A skill is good when an agent that knows nothing about your project can follow it to a passing check. The strongest models fill gaps on their own; a smaller model shows you where the gaps are. So test with one.
- Start clean. A fresh clone, or a branch where the integration's variables in
.env.localare empty and its resources do not exist yet. - Run a less capable model in an agent that runs tools. For example
claude --model haikuin Claude Code, or a smaller model picked in Cursor's or Codex's model menu. The agent must be able to run shell commands. - Say only what a buyer would say: "set up
<service>". No hints, no file names. - Play the person. Do the
⚠ needs yousteps exactly as the agent words them, and nothing else. If its instructions are not enough for you to act, that is a finding. - Success is the check script passing:
<Name>: all checks passedwith exit code 0, plus the real test the skill asks for. The model saying "done" does not count. - Write down every place it got stuck: it asked for a value too early, guessed a click path, edited a file the skill did not name, skipped a check, misread a failure, or looped.
- Fix the skill, not the model. Make the step concrete, add the missing row to the failure table, or make the check script print a hint (an indented line) or the next command. Then run again from a clean state, until a full run passes without your help.
Reference docs
skills/reference/ explains how the code is organized, for changes the skills do not cover. These are reference docs, not skills: nothing in them ends with a check. When a doc and the code disagree, the code wins.
| File | What it covers |
|---|---|
ai.md |
AI features through the AI SDK: providers, chat, image and video generation, async webhooks |
auth.md |
Better Auth: sessions on server and client, route guards, social logins, roles and admin checks |
cms.md |
blog and CMS: post types, rich text editing, tags, visibility, public display |
database.md |
Drizzle ORM on Postgres: schema, queries, transactions, migrations |
db-schema-migration.md |
optional: move the tables into their own Postgres schema to share one database between products |
email.md |
transactional email templates and sending |
i18n.md |
next-intl: message files, translation hooks, locale-aware pages and formatting |
logging.md |
Pino logging and Sentry error capture on the server |
new-component.md |
React components: structure, props, client components, extending shadcn/ui |
new-page.md |
new pages: public, dashboard and admin pages, i18n routing, metadata, layouts |
payments.md |
Stripe, Creem and PayPal: pricing as code, checkout, webhooks, fulfillment, the credits ledger |
platform-differences.md |
what differs between Vercel and Cloudflare Workers in this repo |
quick-start.md |
turn the starter into your product: research, landing copy, SEO metadata, navigation, in every locale |
server-action.md |
Server Actions and API route handlers: response shape, validation, auth guards |
storage.md |
Cloudflare R2: server uploads, presigned client uploads, downloads |