Build a module with an agent
A recipe for having Claude Code, Cursor or any coding agent write a Spillway module: the prompt to paste, what the agent should produce, and the checks to run before you trust it. Read modules.md once yourself; the agent will read it too.
Decide the tier first
| Trusted | Sandboxed | |
|---|---|---|
| Runs | in the server process (JavaScript) | in a WebAssembly sandbox with declared capabilities |
| Can | register blocks, field types, admin pages, providers, AI tools; hook into saves and renders; page builder blocks rendered with React in blocks.js | run beforeSave, beforeRender, afterX hooks and AI tools over JSON; call declared HTTP hosts; page builder blocks rendered as sanitised HTML through render_block |
| Registry | human review before it is listed | automated checks, listed at once |
| Install | needs SPILLWAY_TRUSTED_MODULE_INSTALL=1 on the site | any admin |
Ask for sandboxed whenever the module only validates, transforms or enriches content, or offers a block whose markup is prose (text, links, lists, images: the sandbox's HTML is sanitised to that). Ask for trusted when a block needs its own markup and classes, or the module needs an admin screen or a field type.
The prompt
Copy, fill the brackets, paste into the agent in a checkout of the module's own repository (or an empty folder with Node 22):
Build a Spillway CMS module named "[name]" (tier: [trusted|sandboxed]).
Purpose: [one paragraph: what it does, for whom, and what it must not do].
Follow the module contract in docs/modules.md and the ModuleManifest type in packages/shared/src/index.ts
of https://github.com/kwall-llc/spillway-cms (read both before writing code). Scaffold with
`npx spillway-module create [name] --tier [tier]` from @spillway-cms/sdk, then implement it.
Requirements:
- manifest.json: name, semver version 0.1.0, tier, description, a `spillway` range of "^0.1.0",
the smallest set of capabilities the code uses ([entity.read, http:api.example.org, ...]),
and only the hooks it handles.
- [Trusted] export default defineModule({ manifest, setup(api) }) from @spillway-cms/sdk; bundle every
dependency into the entry file (there is no node_modules at runtime); escape every string you put
into HTML; blocks are server-safe (no browser APIs).
- [Sandboxed] export memory, alloc and handle; stay under 32 MB of memory and 100 ms per call;
only call hosts declared as http:<host>; abort with a message rather than throwing.
- [If it offers a page builder block] declare it under `blocks` in the manifest (key, label, fields of
the kinds docs/modules.md lists, defaults) with the `blocks` capability; trusted: render it in
blocks.js with the host's createElement (defineBlock/defineBlocks from @spillway-cms/sdk), escaping
every string; sandboxed: answer `{ "hook": "render_block" }` calls with `{ "payload": "<html>" }`
using only the prose markup the sanitiser keeps (no div, class, style or script).
- [If its block has looks to choose from] declare `"styles": "<core block>"` (reuse the brand's styles,
e.g. "Cards") or `styleSlots` (its own kebab-case values) instead of a color or variant field, and
render from `props.style` and `props.styleValues` (docs/modules.md, "Block styles in a module block").
- A README.md with: what it does, settings, capabilities and why each is needed, how to install.
- A CHANGELOG.md with a 0.1.0 entry.
- Tests that run with `npm test` without a Spillway server.
- No secrets in the repository; settings that need one read it from the module settings.
When you are done, list the files, the capabilities and anything you could not verify. Do not publish it.
What to check
- Manifest. Open
manifest.json: the capabilities are the ones the code needs and no more;hookslists only handled hooks;mainpoints at the entry; thespillwayrange is right. - Tests.
npm testpasses without a server. - A real load. Start a local Spillway (
pnpm startfrom a checkout, or the Docker image), copy the folder to<DATA_DIR>/modules/<name>/, restart, and enable it under System → Modules (for a trusted module, start the server withSPILLWAY_TRUSTED_MODULE_INSTALL=1or enable from disk as an admin).lastErrormust be null and Logs clean. Trigger the hook (save or publish an entity, render a page) and check the effect. - Sandbox limits (sandboxed): the smoke load passes; a hook that does real work stays well under the 100 ms budget on a sample entity.
Blocks:
GET /api/builder/configlists the block, dropping it in the page builder shows the server's rendering, and the public page renders it; disable the module and the page shows the placeholder instead of an error. - Read the code once with the capabilities in mind: every outbound request goes to a declared host, nothing writes outside its settings, nothing logs content it should not.
- Spillway's own checks still gate everything: a
beforeSavehook can abort a save with a message but cannot publish, and the module runs with the permissions of the request that triggered it.
Then hand it to a person to review, and publish it with publish.md when it is theirs to publish.
Examples to point the agent at
examples/modules/hello-banner(trusted: a block and a hook, plus a manifest block rendered byblocks.js).examples/modules/profanity-guard(sandboxed: abeforeSavescanner built with the SDK's Wasm assembler, plus arender_blockanswer for its notice block).
Spillway 0.1.0 · Edit this page on GitHub