Scaffold a new FoundryVTT v13 module repo (Vite + TS + bun + biome, CI, release-please) with basic/app/libwrapper variants. Use when bootstrapping or init-ing a foundry module.
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Scaffold a new FoundryVTT v13 module repo (Vite + TS + bun + biome, CI, release-please) with basic/app/libwrapper variants. Use when bootstrapping or init-ing a foundry module.
--name foundryvtt-x --display "X" --desc "โฆ" --variant basic
foundryvtt-module-scaffold
Bootstrap a new FoundryVTT v13 module repo built with Vite + TypeScript +
bun + biome, leaving only the actual module logic to implement. The generated
repo passes just check (typecheck + build + lint + test) from the first commit
and distributes via the GitHub-release manifest-URL convention.
When to Use This Skill
Use this skill when...
Use the alternative when...
Starting a new FoundryVTT module repo โ CI-green TS toolchain, release-please, and a basic/app/libwrapper skeleton before writing module logic
You want the full pipeline (repo created + seeded + gitops-adopted) โ foundryvtt-module
Spinning up a FoundryVTT module backlog idea
Adding a feature to an existing module โ this creates a new repo
The architecture it scaffolds
TypeScript source in src/ (entry src/module.ts), built to dist/<id>.mjs
via Vite library mode.vite-plugin-static-copy places module.json, lang/,
styles/ (and templates/ for the app variant) into dist/, which Foundry
serves as the module root. tsc --noEmit type-checks; Vite emits โ decoupled.
Type gate: bun run typecheck โ tsc --noEmit. Foundry globals are typed
by loose local ambient shims (src/foundry-shims.d.ts), so the build is
self-contained and CI-green without the (beta, git-only) fvtt-types. Verify
the real Foundry API before relying on a shape โ the shims are deliberately
loose. Opt into fvtt-types later if you want richer types.
Build: bun run build โ vite build โ dist/<id>.mjs + copied assets.
Dev: bun run dev โ Vite dev server on :30001 proxying everything to
Foundry on :30000 except the module's own files (served with HMR).
Distribute via GitHub release: module.jsonmanifest โ
, โ
. release-please bumps in both
and ; the release job builds, zips , and
attaches the assets. No foundryvtt.com submission is needed to install by URL
(only to be in the in-app package browser).
releases/latest/download/module.json
download
releases/latest/download/<id>.zip
$.version
package.json
module.json
dist/
listed
Three variants
Variant
Use when
Adds on top of basic
basic (default)
Settings + lifecycle behavior โ the minimal well-formed module.
init/ready hooks, a registered world setting, i18n, scoped CSS.
app
The module has a UI panel.
An ApplicationV2/HandlebarsApplicationMixin window (src/app.ts + templates/app.hbs) and a game.settings.registerMenu button that opens it.
libwrapper
The module patches a core/system method.
src/patches.ts with a libWrapper.register(...) call and a manual monkey-patch fallback when lib-wrapper is absent; a relationships.recommends entry for lib-wrapper.
Decision rule:basic for behavior driven by hooks/settings; app when the
module surfaces a window or dialog; libwrapper when it overrides a core method
(the conflict-safe way to do that on Foundry). Variants compose conceptually โ
start from the closest one and add the rest by hand.
How to run
scaffold.py is stdlib-only. Run from the workspace where the module should
land (e.g. repos/laurigates/foundryvtt-dev/).
Basic settings+hooks module:
python3 ${CLAUDE_SKILL_DIR}/scaffold.py --name foundryvtt-initiative-tweaks --display "Initiative Tweaks" --desc "Small quality-of-life tweaks to the combat initiative tracker."
Module with an ApplicationV2 UI panel:
python3 ${CLAUDE_SKILL_DIR}/scaffold.py --name foundryvtt-party-overview --display "Party Overview" --desc "A dockable party status panel for GMs." --variant app
Module that patches a core method via libWrapper:
python3 ${CLAUDE_SKILL_DIR}/scaffold.py --name foundryvtt-token-vision-tweak --display "Token Vision Tweak" --desc "Adjusts token vision drawing via a libWrapper-guarded patch." --variant libwrapper
Flags: --name (GitHub repo, e.g. foundryvtt-x), --id (Foundry module id;
default = --name minus the leading foundryvtt-), --display (title),
--desc, --variant {basic,app,libwrapper}, --fvtt-min / --fvtt-verified
(compatibility, default 12 / 13), --publisher (default laurigates),
--author, --dir (parent dir, default cwd).
Re-runs the finishing-pass audit against an existing module and emits a
machine verdict (MODULE_ID_MATCH=, ESMODULES=, MANIFEST_URL=,
DOWNLOAD_URL=, RELEASE_ZIP=, DECLARED_ASSETS=, LOCKFILE=, then
ISSUE_COUNT= and STATUS=). Exit 1 on ERROR.
STATUS=
Meaning
ERROR
Foundry cannot install, load, or update the published module โ the id disagrees across module.json / vite.config.ts / src/constants.ts, esmodules names a file the build never emits, or an install URL resolves to a release asset nobody uploads.
WARN
It loads, but something is unfinished (a declared asset that 404s, no bun.lock yet). A fresh scaffold is WARN until bun install runs.
OK
Nothing outstanding.
The generated repo asserts the same invariants in tests/manifest.test.ts, which
its own test CI job runs on every PR โ so a rename that drifts one of the five
places the module id appears fails a PR, not a user's install.
Alternative: the cargo-generate template (pilot)
templates/foundryvtt-module/ is a cargo-generate port of scaffold.py whose
emitted files are real files (so tsc/biome/actionlint can check them)
rather than Python strings. Output is byte-identical, enforced by
scripts/tests/test-template-parity.sh.
scaffold.py remains the default. Reach for the template to edit the
scaffold itself, or to try the flow before it is promoted:
Needs cargo-generate locally โ not in the base image, and the main cost of the
port. CI installs it from the release tarball so the parity gate actually runs
(#2221). See templates/README.md
for the comparison, the one deliberate divergence (a non-kebab-case name), the
Liquid brace-collision fixes, and what promoting the template would take.
What you get
A repo where just check passes from the first commit: a real module.json
manifest, package.json (bun scripts), vite.config.ts, strict tsconfig.json,
biome.json, vitest.config.ts + a green Vitest smoke test (Foundry globals
stubbed in tests/setup.ts) and tests/manifest.test.ts (the manifest-vs-build
gate above), .github/workflows/ (ci.yml,
release-please.yml, renovate.yml), release-please-config.json + manifest,
renovate.json, a justfile, src/module.ts + src/settings.ts +
src/constants.ts + src/foundry-shims.d.ts, lang/en.json,
styles/<id>.css, CLAUDE.md, README.md, LICENSE, and an ADR recording the
toolchain decision. The app variant adds src/app.ts + templates/app.hbs;
libwrapper adds src/patches.ts.
After scaffolding
The generator prints the exact next steps. In order:
cd foundryvtt-<name>
git init -b main
bun install
just check
Seed main directly (the repo is unprotected until gitops adopts it) โ pushing
a feature branch first would leave main missing on origin and force a rename +
default-branch fixup later. bun install writes bun.lock, which the seed
commit must include (CI uses --frozen-lockfile).
Then implement, and wire up infra:
Implement the module โ for basic, src/module.ts + src/settings.ts;
for app, src/app.ts + templates/app.hbs; for libwrapper, replace the
Token._draw example in src/patches.ts with the real target.
Add the repo to gitops/repositories.tf with release_please = true and
a foundryvtt topic (mirror the foundryvtt-mcp entry). On apply, gitops
pushes the release-please App credentials.
Or skip steps entirely: run the /foundryvtt-module orchestrator, which
chains scaffold โ gh repo create โ seed main โ the gitops PR.
Hard rules baked into the output
id is the single source of truth.module.jsonid, the install
folder, and the release zip name all derive from --id. Lowercase kebab-case
only.
ESM-only, paths byte-match the manifest.esmodules references
<id>.mjs; the Vite output filename is pinned to match. A mismatch is a silent
load failure โ tests/manifest.test.ts fails the PR that introduces it.
Target the harness-pinned Foundry version. Keep module.jsoncompatibility.{minimum,verified} in sync with what you test against. Verify
the Foundry API against https://foundryvtt.com/api/ or the live console โ not
memory.
Do not commit dist/. It is git-ignored and rebuilt; CI builds it for
releases.
Scoped CSS. Every selector is prefixed with the module id โ keep it that
way so styles never clobber core or other modules.
Never hand-edit CHANGELOG.md or the version fields โ release-please
owns them (it bumps both package.json and module.json).
Agentic Optimizations
Context
Command
Scaffold a basic module
python3 ${CLAUDE_SKILL_DIR}/scaffold.py --name foundryvtt-X --display "X" --desc "โฆ"
Scaffold an app module
python3 ${CLAUDE_SKILL_DIR}/scaffold.py --name foundryvtt-X --display "X" --desc "โฆ" --variant app
The biome pin is single-sourced in scaffold.py's BIOME_VERSION constant so
biome.json and the CI setup-biome step never drift.
Action/tool versions in the generated workflows are current as of scaffolding;
the generated renovate.yml (laurigates reusable workflow) bumps them.
The generated module uses local ambient shims, not fvtt-types. This keeps
the build green and self-contained; switch tsconfigtypes to fvtt-types
(github:League-of-Foundry-Developers/foundry-vtt-types#main) for full API
types once you need them.
Quench (in-Foundry Mocha runner) and Playwright integration tests against the
harness are not scaffolded โ add them when the module warrants runtime
coverage.