# Seed Ah

> Seeds a database with data that looks like a real, lived-in production database: reads the real DB structure (migrations, ORM models, live introspection), builds a dependency-ordered seeding plan using the framework's own seeder mechanism, generates locale-correct identities, timestamps spread like history, power-law distributions and deliberate edge cases, and fills image fields with real licensed photos pulled from photo libraries (Unsplash, Pexels, Openverse, Wikimedia) instead of hand-drawn placeholders. Nothing in the seeded rows says "demo", "test", "sample" or "lorem" — provenance is kept in an invisible ledger so every row stays wipeable. Hard production gate: verifies the target database before writing a single row. Login accounts and credentials are produced ONLY when the user explicitly asks for them. Use when the user asks to seed demo/test/sample data, create fixtures, populate a dev database, or mentions seed-ah or /seed-ah.

- Skill: `silkyland/seed-ah` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add silkyland/seed-ah`
- Raw SKILL.md: https://api.skillmd.com/api/skills/silkyland/seed-ah/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: silkyland (https://skillmd.com/u/silkyland)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/silkyland/seed-ah

---


# Seed-ah!

An empty database makes every screenshot, demo and dev session lie — and a
database full of `Test User 3` / `lorem ipsum` / grey SVG placeholders lies
louder. This skill seeds a world that reads as **real**: locale-correct
names, timestamps that spread like history, a few heavy users and a long
tail of quiet ones, real photographs in the image fields. All of it
invented, none of it labelled, all of it wipeable through a channel the UI
never renders.

## The Prime Directive (family rule)

> **The schema is read, never assumed. Production is never touched. And the
> data never announces itself.**
> Every column seeded exists in evidence (migration/model/introspection,
> cited). The target database is verified and confirmed by the user before
> the first row is written. Every seeded row can be identified and wiped —
> by provenance metadata, never by a visible "demo" tell inside the data.

## Hard safety rules

1. **Production gate ⛔** — before seeding, resolve the actual connection
   (env/config, `file:line`), print host + database name + environment to
   the user, and require explicit confirmation. Names that smell like
   production (`prod`, a public host, a managed-DB hostname) require the
   user to type the database name back. When in doubt, refuse and suggest
   a local copy. The gate is checked TWICE: once here, and again at run
   time — immediately before executing, query the live connection for its
   own identity (`SELECT DATABASE()` / `current_database()` /
   `PRAGMA database_list`) and compare host + database name against what
   the user confirmed. Any mismatch = abort, zero rows written. If no
   user can respond (headless run), the gate cannot pass — write the
   scripts and stop; never execute a seed unattended.
2. **No tells in the data 🚫** — no seeded value a human or screenshot can
   see may contain `demo`, `test`, `sample`, `fake`, `placeholder`,
   `lorem ipsum`, `foo`/`bar`/`baz`, `John Doe`, `asdf`, `TBD`, `xxx`, or
   a sequential `User 1 / User 2` pattern. Titles are real sentences,
   descriptions are real paragraphs about the product's actual domain,
   prices are prices. The only sanctioned exceptions are non-routable
   contact values kept for safety (reserved e-mail domains, reserved
   phone ranges) — see rule 3 — and they are declared in the Seed Brief,
   not slipped in. Step 6 runs a **tell-scan** over the seeded text
   columns; a hit blocks the run.
3. **Contactability beats cosmetics** — seeded e-mail/phone values must be
   incapable of reaching a real human: IETF-reserved domains
   (`example.com`, `.example`, `.invalid`, `.test`) or a domain the
   project owns and null-routes. If the user wants screenshot-perfect
   addresses instead, they choose it explicitly in the Seed Brief, mail
   transport must be verified OFF in the target env (`file:line`), and the
   choice is recorded in the manifest.
4. **Wipeable by construction — and proven, not promised** — provenance
   lives in an invisible channel (seed-ledger table, tag column, reserved
   key range, or a pre-seed snapshot), chosen per
   [references/provenance-and-wipe.md](references/provenance-and-wipe.md).
   An **unseed script** ships alongside the seed script, and the wipe
   round-trip in Step 6 must pass on a one-row skeleton before any mass
   insert.
5. **No real humans, no real data** — no real names, e-mails, photos or
   copied production rows. Text is generated, never lifted from a
   production dump. Photos come from licensed libraries with the licence
   recorded (Step 5), and identifiable people are never presented as
   staff, testimonials or endorsers.
6. **No credentials unless asked** — user rows are seeded like any other
   entity, with a valid hash in the password column so auth code behaves,
   and **nothing is reported**: no password roster, no login testing, no
   credentials in the manifest. If the user wants to log in as a seeded
   user, they ask — then follow *"Add-on: login accounts"* at the bottom
   of this file.

## Progress checklist

Copy this into your response and check items off:

```
Seed-ah Progress:
- [ ] Step 1: Frame — target env confirmed ⛔, volume, locale, purpose
- [ ] Step 2: Schema census — Census Questions answered with evidence, FK graph, seed order
- [ ] Step 3: Realism plan — per-table shape, distributions, edge cases, provenance channel; Seed Brief confirmed
- [ ] Step 4: Seed scripts — framework-native, committed, idempotent, side effects muted or accepted
- [ ] Step 5: Images — real licensed photos for every image field, locked + credited
- [ ] Step 6: Run + verify — target re-checked ⛔, skeleton + wipe round-trip proven, tell-scan clean, then mass seed
- [ ] Step 7: Manifest — what was seeded, image credits, re-seed/wipe commands
```

## Step 1 — Frame

Volume (S: enough for screenshots / M: realistic dev / L: pagination and
performance testing), locale for the identities (Thai names for a Thai
product — locale mismatch screams "fake"), and purpose (feeding
love-me-love-my-docs screenshots? a customer walkthrough? dev fixtures?).
Purpose drives realism: data destined for screenshots must survive being
zoomed into. Then run the **production gate** from the hard rules —
nothing proceeds past this step without the confirmation.

**If no database is reachable** (connection fails, credentials missing),
stop here: write the seed scripts and manifest template as deliverables,
but execution cannot proceed — report the blocker and what's needed to
resolve it (connection string, credentials, network access).

## Step 2 — Schema census

Read the structure from evidence, per
[references/schema-census.md](references/schema-census.md): migrations and
ORM models first (they carry intent: enums, defaults, validation), live
introspection to confirm (information_schema / SHOW CREATE TABLE / PRAGMA),
FK relationships → a dependency graph that dictates seed order (parents
before children), and every constraint that can reject a row (NOT NULL,
UNIQUE, CHECK, enum values). Note which tables the app writes vs
reference/lookup tables that may already be populated.

The census exists to answer the numbered **Census Questions** in that
reference — constraints, FK order, what fires on insert, what already
lives in the target, where provenance can be stored — before a single row
is invented. Every answer carries evidence (`file:line` or pasted
introspection output). A question you cannot answer from evidence never
becomes a guess: it becomes a named **probe** (insert one throwaway row
inside a transaction, observe, roll back) or the table waits. The census
is complete when every question is answered or probed — not when every
file has been read.

## Step 3 — Realism plan

Per table, decide what "reads as real" means — rules in
[references/realism-guide.md](references/realism-guide.md): locale-correct
identities, domain-true text (an article about the product's actual
subject, not filler), timestamps spread over months with plausible
rhythms, power-law distributions (a few heavy users, a long tail, some
ghosts), statuses covering the whole enum including the ugly ones
(cancelled, banned, pending), and **deliberate edge cases** (longest
plausible name, unicode, empty optionals) that still look like real
records rather than QA fixtures.

Then pick the **provenance channel** per
[references/provenance-and-wipe.md](references/provenance-and-wipe.md) —
ledger table, tag column, key range or snapshot — and verify it is
invisible to the UI (grep the templates/serializers for `SELECT *`-style
exposure of a tag column, cite the check).

**Seed Brief Gate** — before writing a single script, present a compact
brief in chat: the target database (exactly as confirmed at the production
gate), per-table row counts, the provenance channel and how the wipe uses
it, the contact-value policy (rule 3), the image sources Step 5 will draw
from, and any side effects that will fire or be muted. Add one line:
*"no seeded user is loggable-in — say the word if you want access
accounts."* 10–20 lines total; ask for confirmation **once**. Changing a
row count here costs one message; changing it after the scripts and images
exist costs a rewrite. If the user cannot respond, write the scripts
anyway (files are reversible) — but execution stays behind the production
gate, which never passes unattended.

## Step 4 — Write the seed scripts

Use the framework's blessed mechanism — Laravel seeders/factories, Symfony
Foundry, Prisma seed, Django fixtures/factory_boy, plain SQL as last
resort — cited from the project's own conventions (`file:line` of an
existing example if one exists). Scripts are committed (`database/seeds/`
or the stack's home), deterministic (fixed random seed so re-runs produce
the same world), and paired with the unseed script. Provenance is written
as rows are created, not reconstructed afterwards. Every insert-time side
effect the census found (Census Question 4 — observers, signals,
callbacks, queued jobs) is handled IN the script: muted with the
framework's own switch (cite it) or left firing with a one-line
justification. Silence is not handling.

## Step 5 — Images and media

Image fields get **real photographs**, not generated placeholder graphics
— rules and the source ladder in
[references/images-and-media.md](references/images-and-media.md): the
project's own asset library first, then licensed photo libraries by API
(Unsplash, Pexels, Openverse, Wikimedia Commons) queried with terms drawn
from the product's actual domain, then AI generation for imagery no
library carries. Hand-rendered SVG/placeholder graphics are a
last-resort offline fallback only, taken with the user's knowledge and
labelled as a downgrade in the manifest.

Files are downloaded once, committed to the seed assets folder, and
recorded in an image lockfile (photo id + source + licence + author) so a
re-seed rebuilds the identical world and the manifest can credit every
file. Never hotlink a live URL from seeded rows — a demo that needs the
network to render an avatar is a demo that breaks on stage. Store files
where the app actually serves media from (`file:line` of the storage
config), at the dimensions the app expects.

## Step 6 — Run and verify

Re-check the target first (the run-time half of the production gate ⛔),
then prove the pipeline before trusting it with volume — in this order:

1. **Baseline** — record pre-seed row counts per table, and confirm the
   provenance channel is clean (ledger empty / tag column all NULL / key
   range unused). If it isn't, and the rows aren't a prior seed run per
   the manifest, resolve that before going on — a provenance collision
   turns the wipe into a data-loss tool.
2. **Walking skeleton** — seed exactly ONE row per table through the real
   seeder (including one image landing in real storage), then check FK
   integrity and that one page renders it. The thinnest slice that
   exercises the whole pipeline.
3. **Wipe round-trip** — run the unseed script for real against the
   skeleton. Re-query the counts: every table must equal the baseline,
   and the copied media files must be gone. A diff of zero is the proof
   that "wipeable" is true. Until this passes, mass seeding is forbidden
   — a wipe that fails on 1 row per table would have failed on thousands.
4. **Mass seed** — now run the full seed, and verify like it matters:
   - Row counts per table match the plan.
   - FK integrity: zero orphans (query it, don't assume it).
   - **Tell-scan**: query every seeded text column for the banned tokens
     of rule 2 (`demo`, `test`, `sample`, `lorem`, `placeholder`,
     `foo`, `asdf`, …). Expected result: zero rows, except the declared
     reserved contact values. A hit is a seed defect — fix and re-seed.
   - **Eyeball pass**: render one page per major entity and read it as a
     stranger would. Does it look like a product in use, or like a
     fixture file? Report what you saw.

## Step 7 — The manifest

Write the summary per
[references/manifest-template.md](references/manifest-template.md) to
`SEED_MANIFEST.md` (repo root or docs/): per-table seeded counts and shape
notes, the provenance channel and exact wipe/re-seed commands, the image
inventory with source + licence + author per batch, the tagged edge-case
rows, and any drift the census surfaced.

End by reporting the manifest inline: what was seeded (counts), where the
images came from, and the one-command re-seed and wipe.

## Add-on: login accounts (only when the user asks)

Not part of a normal run. When the user explicitly asks for accounts they
can log in with:

1. Read the app's roles/permissions from code or DB (cite `file:line`) —
   the roster comes from the app, not from a template.
2. Pick existing seeded users to promote where possible, so the accounts
   have lived-in history instead of being obvious plants.
3. Passwords: unique to this seed, policy-compliant, typeable on stage,
   never a credential reused from anywhere. Hash through the app's OWN
   hasher (`file:line` of the hashing call/config) — bcrypt/argon2 config
   differences WILL lock you out.
4. Log in as **every** account through the app's real auth (HTTP or the
   app's test client). An account that can't log in is a failure, not a
   footnote.
5. Report the roster in chat and append an **Access accounts** section to
   `SEED_MANIFEST.md` with a header warning that the file now contains
   credentials — keep it out of public repos unless the database is
   disposable.

## When things go wrong

| Situation | Response |
|-----------|----------|
| **Production gate fails (smells like prod)** | Refuse; suggest local copy or explicit database name confirmation typed back by user |
| **No database reachable** | Write scripts and manifest template as deliverables; report blocker (connection, credentials, network) — execution cannot proceed |
| **User cannot respond (headless run)** | Write scripts (reversible) but never execute — production gate cannot pass unattended |
| **Provenance channel already has rows** | Resolve before seeding (prior run? reused id range?) — a collision turns the wipe into a data-loss tool |
| **No provenance channel possible** (no new table, no column, no id range) | Fall back to snapshot-and-restore on a disposable DB; if even that is refused, write the scripts and refuse the mass seed |
| **Wipe round-trip fails on skeleton** | Fix wipe script before mass seed — a wipe that fails on 1 row/table would fail on thousands |
| **Tell-scan finds banned tokens** | Seed defect: fix the generator, re-seed the affected tables — never ship data that labels itself |
| **No image API key / no network** | Try keyless sources (Openverse, Wikimedia) first; if still blocked, ask before falling back to rendered placeholders and label the downgrade in the manifest |
| **Insert-time side effects discovered** | Mute with framework's own switch (cite) or accept with one-line justification — silence is not handling |
| **User asks "how do I log in?"** | That's the add-on above — do it properly (real hasher, real login test), not by inventing a password |

