Publishing a Mod
Hands-on walkthrough from zero to a live, installable mod in the Maker Studio Marketplace. Should take about 25 minutes the first time. Subsequent mods take 10.
The example mod — YOUR_MOD (com.YOUR_USERNAME.YOUR_MOD) — tracks how many days in a row you’ve opened the editor.
Substitute your own GitHub handle, mod id, and name as you go.
Already know the flow and just want the spec? Jump to Publishing — the reference below.
Before you start
You’ll need:
- A public GitHub account.
- Maker Studio installed locally (to test the mod).
- Git on your machine. PowerShell on Windows, or bash on macOS/Linux.
Clone the registry repo locally — you’ll copy from its examples/ and grab the workflow template:
gh repo clone Toskan4134/maker-studio-modscd maker-studio-mods(No gh? git clone https://github.com/Toskan4134/maker-studio-mods.git works the same.)
Step 1 — Create your mod folder
A mod is a folder with three files: manifest.json (metadata), index.js (your code), and README.md. The fastest start is to copy one of the registry’s examples/mods/ and edit it. Here we’ll build it from scratch so you see every piece.
Create the folder anywhere outside the registry clone — your mod needs its own repo. On your Desktop:
mkdir ms-YOUR_MODcd ms-YOUR_MODAdd the three files.
manifest.json
{ "id": "com.YOUR_USERNAME.YOUR_MOD", "name": "YOUR_MOD", "version": "1.0.0", "authors": [{ "name": "YOUR_USERNAME", "url": "https://github.com/YOUR_USERNAME" }], "description": "Tracks how many days in a row you've opened the editor.", "tags": ["tools", "ui"], "apiVersion": "1.0.0", "main": "index.js", "permissions": ["ui.toasts"]}The id is reverse-DNS (must contain a .). permissions declares every API capability your code uses — reviewers check this against the code. In authors, url is optional, and a plain "author": "YOUR_USERNAME" string works too.
tags are the Marketplace’s search and filter keywords, and they are yours to choose — up to 8. Write them however reads best ("Terrain Tags", "Essentials 19.1"); publishing slugs them to the ^[a-z0-9-]+$ the registry stores (terrain-tags, essentials-19-1). Omit the field and the registry maintainer picks tags for you on review.
index.js
A starter that logs, shows a toast, and adds a menu item:
export function activate(ctx) { ctx.log.info("YOUR_MOD activated");
ctx.ui.showToast({ message: "Hello from YOUR_MOD!", level: "info", });
ctx.menu.registerMenuItem({ menu: "Mods", label: "YOUR_MOD — Say Hi", handler: () => { ctx.ui.showToast({ message: "Hi from YOUR_MOD!" }); }, });}
export function deactivate() { // Disposables registered through ctx are auto-cleaned. Nothing else to do.}README.md
A short note for yourself — anything works. The build + release steps are covered later in this guide (see Publishing below).
You should now have:
ms-YOUR_MOD/├── manifest.json├── index.js└── README.mdStep 2 — Write your mod logic
Open index.js and replace the starter with your real feature. For YOUR_MOD it might look like:
const STORAGE_KEY = "streak";
export async function activate(ctx) { const today = new Date().toISOString().slice(0, 10); const prev = await ctx.storage.get(STORAGE_KEY, { lastDay: null, count: 0 });
let count = prev.count; if (prev.lastDay !== today) { const yesterday = new Date(Date.now() - 86_400_000).toISOString().slice(0, 10); count = prev.lastDay === yesterday ? prev.count + 1 : 1; await ctx.storage.set(STORAGE_KEY, { lastDay: today, count }); }
ctx.ui.showToast({ message: `🔥 ${count}-day streak — keep going!`, level: "info", durationMs: 4000, });}The full Mod API surface (every method on ctx) is in this docs/ folder — see api-reference.md for the narrative reference, quick-reference.md for the one-page cheat sheet, and mod-api.d.ts for TypeScript types you can drop into your editor for autocomplete.
Open manifest.json and declare the permissions you actually use. YOUR_MOD uses ctx.storage (no permission needed — it’s mod-local) and ctx.ui.showToast (needs ui.toasts):
"permissions": ["ui.toasts"]Step 3 — Test locally
Drop your mod folder into the global mods directory and restart the editor.
Windows:
Copy-Item -Recurse C:\Users\YOUR_USERNAME\Desktop\ms-YOUR_MOD "$env:APPDATA\maker-studio\Mods\YOUR_MOD"macOS: ~/Library/Application Support/maker-studio/Mods/YOUR_MOD/
Linux: ~/.local/share/maker-studio/Mods/YOUR_MOD/
Boot Maker Studio → Mods → Mod Manager. Your mod appears in the Installed tab with the global badge. Open any project — the toast should fire.
If the mod fails to load, expand its row in Mod Manager to see error logs. Fix, then click Reload on the row (no full restart needed).
Repeat edit → reload until it works.
When done testing, delete the test copy:
Remove-Item -Recurse "$env:APPDATA\maker-studio\Mods\YOUR_MOD"Step 4 — Drop in the GitHub Actions workflow (recommended)
The registry ships a workflow at templates/publish.yml. It takes care of zipping the mod, computing the SHA-256, creating the GitHub Release, and printing the exact registry-PR diff. Copy it into your mod repo:
mkdir .github\workflowsCopy-Item C:\path\to\maker-studio-mods\templates\publish.yml .github\workflows\publish.ymlCommit it now along with your code. No secrets to configure — the default GITHUB_TOKEN is enough to publish a release on your own repo.
If you’d rather build releases manually, skip this step and do Compress-Archive + Get-FileHash by hand in Step 6. The rest of the flow is identical.
Step 5 — Create the GitHub repo for your mod
- Owner: your account
- Repository name:
ms-YOUR_MOD(or anything you like) - Visibility: Public
- Don’t initialize with README / .gitignore / license — you’re pushing your own.
Click Create repository. Copy the repo URL (e.g. https://github.com/YOUR_USERNAME/ms-YOUR_MOD.git).
Step 6 — Push and tag the release
cd C:\Users\YOUR_USERNAME\Desktop\ms-YOUR_MOD
git initgit add .git commit -m "v1.0.0 — initial release"git branch -M maingit remote add origin https://github.com/YOUR_USERNAME/ms-YOUR_MOD.gitgit push -u origin main
# Tag the release. Tag must match manifest version exactly — without 'v'.git tag v1.0.0git push --tagsIf you copied the template Action
Pushing the tag fires .github/workflows/publish.yml. Open the Actions tab on your mod repo:
- Watch the run. It zips, hashes, and creates the GitHub Release automatically.
- Open the final
Print registry PR blockstep. It prints something like:"version": "1.0.0","assetName": "com.YOUR_USERNAME.YOUR_MOD-1.0.0.zip","sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - Copy those three lines. You’ll paste them into the registry PR in Step 7.
If you’re doing it manually
$zip = "com.YOUR_USERNAME.YOUR_MOD-1.0.0.zip"Compress-Archive -Path manifest.json,index.js,README.md -DestinationPath $zip$sha = (Get-FileHash $zip -Algorithm SHA256).Hash.ToLower()Write-Host "sha256: $sha"
gh release create v1.0.0 $zip ` --title "v1.0.0" ` --notes "Initial release. Tracks daily editor-open streak."Save $sha somewhere — you’ll need it in Step 7.
Step 7 — Submit to the registry
This is the only step that touches the maker-studio-mods repo. Two paths.
Path A — GitHub web UI (zero local git)
- Go to https://github.com/Toskan4134/maker-studio-mods/blob/main/index.json
- Click the pencil icon (top right of the file view) — Edit this file.
- GitHub auto-forks the registry to your account behind the scenes. No
gitcommands. - Add your mod entry inside the
"mods": [ ... ]array:Replace{"id": "com.YOUR_USERNAME.YOUR_MOD","name": "YOUR_MOD","authors": [{ "name": "YOUR_USERNAME", "url": "https://github.com/YOUR_USERNAME" }],"repo": "YOUR_USERNAME/ms-YOUR_MOD","version": "1.0.0","assetName": "com.YOUR_USERNAME.YOUR_MOD-1.0.0.zip","sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","description": "Tracks how many days in a row you've opened the editor.","tags": ["stats", "motivation"],"homepage": "https://github.com/YOUR_USERNAME/ms-YOUR_MOD","apiVersion": "1.0.0","permissions": ["ui.toasts"]}version,assetName, andsha256with what Step 6 printed. If there are already entries, add yours as the last item (don’t forget the comma after the previous one). - Also update
updatedAtto the current ISO-8601 timestamp. - Scroll down to Propose changes. Commit message can be anything —
add: com.YOUR_USERNAME.YOUR_MODworks. - Create pull request → fill in the PR template (it auto-loads). Submit.
Path B — Local fork (for power users / multiple mods)
gh repo fork Toskan4134/maker-studio-mods --clone --remotecd maker-studio-modsgit checkout -b add-YOUR_MOD# edit index.json same as abovegit add index.jsongit commit -m "add: com.YOUR_USERNAME.YOUR_MOD"git push origin add-YOUR_MODgh pr create --fillStep 8 — Wait for CI, respond to review
When the PR opens, the registry’s CI workflow runs these checks:
| Check | What it does |
|---|---|
| Schema validation | index.json matches schema/index.schema.json (required fields, valid id pattern, semver shape, 64-hex sha256) |
| Duplicate id check | No two entries share the same id |
| Release asset check | The repo you listed has a release at the pinned v{version} tag containing an asset matching assetName |
| Hash check | The maintainer (or CI, if available) verifies the listed sha256 matches the actual bytes on the release |
All green? The maintainer reviews and merges. If something’s red, click the failed check for the exact error message. Common fixes:
| Error | Cause | Fix |
|---|---|---|
should match pattern "^[a-zA-Z0-9._-]+$" for id | Used spaces or unusual chars in the id | Edit to only letters/digits/._- |
repo has no release at tag v{version} | You forgot to publish the Release in Step 6 | Publish the release, push the PR again |
release has no asset named '<assetName>' | Zip is named differently from what your PR claims | Re-upload the zip with the right name, or fix assetName in the PR |
sha256 doesn't match release asset | You edited or rebuilt the zip after computing the hash | Re-download from the release, recompute sha256sum, update the PR |
Push fixes to the same branch — CI re-runs automatically. Once merged, you’re live within ~1 hour (editor caches the index for that long).
Step 9 — Verify in the editor
Open Maker Studio → Mods → Mod Manager → Marketplace → click Refresh (forces an immediate fetch).
- Your card should appear, pinned to
v1.0.0. - Click Install. A consent dialog lists your declared permissions. Accept.
- The editor downloads from your release, verifies the SHA-256 matches the registry, and installs.
- Wait for the “Installed YOUR_MOD v1.0.0” toast.
- Switch to the Installed tab — your mod is loaded and active.
That’s it. You shipped a mod.
Releasing updates
Every subsequent release repeats the same loop — bump version, tag, push, then a registry PR that only changes version, assetName, and sha256. See Releasing an Update for the canonical steps.
Common first-time pitfalls
- Forgot to flip the repo to public. Default for new GitHub repos can be Private. Settings → General → Visibility → Change → Public.
The rest — tag/version mismatch, deep-folder zips, stale hashes, permission mismatches — are covered in Common Mistakes and Rejection reasons.
Where to ask for help
Open a discussion on the registry for general questions, or file an issue for a specific bug in the docs or schema.
Publishing a Mod
End-to-end reference for getting your mod into the Maker Studio Marketplace. Skip ahead to whichever section you need:
- At a glance
- Your mod repo
- Release convention
- Automating releases
- Registry submission
- Updating
- Removing or renaming
- What users see
- Common mistakes
- Rejection reasons
The Marketplace is server-less but not trust-on-first-use. The registry’s index.json pins each mod to an exact version and the SHA-256 of its release asset. The editor downloads the pinned tag (/releases/tags/v{version}), hashes the bytes locally, and refuses to install on mismatch. So you keep ownership of your mod repo, but every new release needs a PR here — that PR is the security boundary.
At a glance
Your mod must:
- Live in a public GitHub repo you own.
- Have a
manifest.jsonat the root with at least these fields:id(reverse-DNS, e.g.com.yourname.modname) — permanent identityname(human-readable)version(semver, must match release tag)apiVersion(the minimum Mod API your mod needs, e.g."1.0.0")main(relative path to JS entry file, e.g."index.js")
- Tag each release
vX.Y.Zmatchingmanifest.json#versionexactly (without the leadingv). - Attach a zip asset named
<modId>-<version>.zipto each GitHub Release, withmanifest.jsonat the zip root (or wrapped in one top-level folder — installer auto-strips it). - Be loadable cleanly in the latest version of Maker Studio (test by dropping the unzipped folder into
%APPDATA%/maker-studio/Mods/and restarting).
Recommended:
- Drop
templates/publish.ymlinto your mod repo at.github/workflows/publish.yml. It builds the zip, computes the SHA-256, attaches both as release assets, and prints the exactindex.jsonblock you need to PR. - Start from one of the
examples/mods/folders — fastest way to learn the API and avoid common mistakes. - Provide a clear release-notes body — it becomes the changelog shown to users.
- Provide an icon (64×64 PNG) hosted in your repo or as a release asset.
- Declare every capability you use in the manifest’s
permissionsarray. Users see the list before installing — undeclared usage is a red flag.
1 — Your Mod Repo
Your mod needs a manifest.json and at least one JavaScript file. A typical repo layout:
ms-my-mod/├── manifest.json├── index.js├── README.md└── (any assets you need)manifest.json minimum
{ "id": "com.yourname.mymod", "name": "My Mod", "version": "1.0.0", "apiVersion": "1.0.0", "main": "index.js", "authors": [{ "name": "Your Name", "url": "https://your-site-or-handle" }], "description": "What it does in one sentence.", "permissions": ["ui.toasts"]}Field rules:
| Field | Required | Notes |
|---|---|---|
id | yes | Reverse-DNS, permanent. Cannot change once published |
name | yes | Shown in Mod Manager and Marketplace cards |
version | yes | Semver. Must match release tag (without leading v) |
apiVersion | yes | Minimum Mod API your mod needs — the version that introduced the newest API it calls. Editors older than this refuse to install or load it |
main | yes | Relative path to JS entry. No .. or absolute paths |
authors | recommended | Array of { name, url? }. Shown as clickable links |
description | recommended | Long descriptions get truncated in the card |
homepage | optional | Mod homepage, separate from author URL |
dependencies | optional | { "other.mod.id": "^1.0.0" } — loader topo-sorts |
permissions | optional | See Permissions below |
Mod entry file
A mod exports an activate(ctx) function and optionally deactivate():
export function activate(ctx) { ctx.log.info("My Mod loaded");
ctx.ui.showToast({ message: "Hello!" });
ctx.menu.registerMenuItem({ menu: "Mods", label: "My Mod — Do Thing", handler: () => ctx.ui.showToast({ message: "Did the thing." }), });}
export function deactivate() { // Disposables registered through ctx are auto-cleaned. Nothing else to do.}The ctx argument exposes the full editor API: editor, map, tileset, events, tools, menu, commands, ui, bus, fs, storage, log, lifecycle, stats, keybinds, selectors, projectData.
Two ways to learn it:
- Read the bundled example mods under
examples/mods/— every folder ships with a walkthrough README explaining what API surface it uses. - Read the API reference docs in this folder — api-reference.md, events-reference.md, quick-reference.md. For IDE autocomplete, drop mod-api.d.ts next to your
index.js.
Permissions
Declared in manifest.json#permissions. Users see the list before installing.
| Permission | What it grants |
|---|---|
fs.mod | Read/write inside the mod’s own folder |
fs.project | Read project assets (game folder) |
fs.write.project | Write inside the project (modify game data) |
events.cancel.save | Cancel save operations via the save.before event |
ui.dialogs | Show dialogs |
ui.toasts | Show toasts |
Declare every permission you actually use. Undeclared usage is a red flag for reviewers and users.
2 — Release Convention
Each release on your repo must follow these rules:
- Git tag:
vX.Y.Z(semver). Example:v1.2.0. manifest.json#versionmust match the tag without the leadingv. If they disagree the editor refuses to install.- Zip asset: named
<modId>-<version>.zip. Example:com.yourname.mymod-1.2.0.zip. The registry pins this exact filename — typos are a hard install failure. - Zip layout:
manifest.jsonlives at the zip root. Either zip the contents of your mod folder directly, or zip the folder itself — the installer auto-strips a single top-level folder. - SHA-256: the editor refuses to install bytes that don’t match the SHA-256 your registry entry pins. Compute it after the zip is built —
sha256sum <modId>-<version>.zipon macOS/Linux,Get-FileHash <modId>-<version>.zip -Algorithm SHA256on Windows. - Release body: this markdown becomes the changelog shown in the Marketplace card.
Building the zip on Windows
Compress-Archive -Path manifest.json,index.js,README.md -DestinationPath com.yourname.mymod-1.2.0.zipGet-FileHash com.yourname.mymod-1.2.0.zip -Algorithm SHA256Building the zip on macOS/Linux
zip -r com.yourname.mymod-1.2.0.zip manifest.json index.js README.mdsha256sum com.yourname.mymod-1.2.0.zipTest before publishing: unzip into %APPDATA%/maker-studio/Mods/com.yourname.mymod/, restart the editor, confirm the mod loads in the Mod Manager.
3 — Automating Releases with GitHub Actions
The registry ships a ready-to-use workflow at templates/publish.yml. Drop it into your mod repo at .github/workflows/publish.yml and it does the whole release-side dance for you:
- Triggered when you push a tag matching
v*.*.*. - Reads
manifest.json#versionand refuses to continue if it doesn’t match the tag. - Zips the repo (excluding
.git,.github, existing zips) as<modId>-<version>.zip. - Computes
sha256sum, writes aSHA256SUMS.txt. - Creates the GitHub Release for the tag and uploads both files as assets.
- Prints the exact 3-line block (
"version","assetName","sha256") you paste into your registry entry’s PR.
The default GITHUB_TOKEN is enough — no extra secrets to configure. There’s an optional commented-out follow-up job in the template that auto-opens the registry PR for you if you set a REGISTRY_PAT secret — see step 4.
4 — Get Listed in the Registry
Publish the first GitHub Release on your mod repo (push tag v1.0.0). If you copied the template Action, it builds the zip and computes the SHA-256 automatically; otherwise run sha256sum <modId>-1.0.0.zip yourself.
Then open a PR that adds an entry to the mods array in index.json:
{ "id": "com.yourname.mymod", "name": "My Mod", "authors": [{ "name": "Your Name", "url": "https://your-site-or-twitter" }], "repo": "your-github-handle/ms-my-mod", "version": "1.0.0", "assetName": "com.yourname.mymod-1.0.0.zip", "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "description": "One-line pitch of what the mod does.", "tags": ["tools", "ui"], "icon": "https://raw.githubusercontent.com/your-github-handle/ms-my-mod/main/icon.png", "homepage": "https://your-site-or-twitter", "minStudioVersion": "2.0.0", "apiVersion": "1.0.1"}Drop icon, homepage, minStudioVersion, tags if not applicable. id, name, authors, repo, version, assetName, sha256 are mandatory — the editor will not install otherwise.
Keep apiVersion identical to your manifest.json (the automatic PR job copies it for you). The editor reads it off the card and blocks Install / Update when it provides an older Mod API, so a too-high value hides your mod from editors that could actually run it, and a too-low one lets the install through only for the mod to land as error in the Mod Manager.
Three ways to open the PR:
-
Automatic: enable the
registry-prjob in the template Action (fork the registry, add aREGISTRY_PATsecret, setFORK_OWNER). Every release then opens the PR for you — adding this entry the first time, bumpingversion/assetName/sha256after that. It fills every field from yourmanifest.json, includingtags;iconandminStudioVersionare ours to add on review. -
Easiest (no local git): open
index.jsonon github.com → pencil icon → paste your entry into themodsarray (including theversion,sha256, andassetNamefrom your release) → Propose changes → cross-repo PR. Also updateupdatedAtto the current ISO-8601 timestamp. -
Local fork (power users / multiple mods):
Terminal window gh repo fork Toskan4134/maker-studio-mods --clone --remotecd maker-studio-modsgit checkout -b add-my-mod# edit index.json same as abovegit add index.jsongit commit -m "add: com.yourname.mymod"git push origin add-my-modgh pr create --fill
The PR template asks you to confirm: tested on the latest editor, no obfuscated code, permissions justified, hash matches the released zip. Fill it in.
Once merged, your mod is live within one hour for everyone using the Marketplace.
5 — Releasing an Update
Every new version needs a registry PR — this is the security boundary (see the intro for why the pin matters).
Per new version:
- Bump
manifest.json#version(e.g.1.2.0→1.2.1). - Commit, tag
v1.2.1, push the tag. If you copied the template Action, it builds, hashes, and publishes the GitHub Release automatically. - Open a PR here that only changes
version,assetName, andsha256on your entry. The Action prints the exact diff to copy. - After merge, users see the update on their next check (within ~1 hour). Until merge, users keep installing the previously approved version — that’s the security guarantee, not a bug.
6 — Removing or Renaming a Mod
If you abandon a mod or want to take it down: open a PR to this repo removing your index.json entry. The mod stops appearing in the Marketplace, but anyone who already installed it keeps it until they manually uninstall.
If you want to rename or restructure: you cannot change id. Publish under a new id and ask users to migrate. Old installs of the old id are unaffected.
7 — What Users See
When someone clicks Install on your mod they see:
- Your icon, name, and author.
- A Verified chip if the maintainers of this registry have flagged the mod as curated (independent of integrity — the SHA-256 pin always applies).
- The exact list of capabilities your manifest’s
permissionsarray declares. - A Cancel / Install button.
Keep your permissions minimal — every extra permission scares users away.
8 — Common Mistakes
- Tag doesn’t match manifest version → installer refuses.
manifest.json#versionmust equal the tag without thev. - Zip wraps everything in a deep folder → only single-level wrapping is auto-stripped. Don’t zip
Documents/my-mod/manifest.json. assetNamein the registry doesn’t match the actual asset on the release → install fails with “release has no asset named …”.- SHA-256 in the registry doesn’t match the uploaded zip → install fails with “sha256 mismatch”. Recompute after every rebuild.
- Pushed a new tag but forgot the registry PR → users won’t see the new version. By design.
manifest.iddoesn’t match what’s inindex.json→ installer rejects with “manifest id mismatch”.
Rejection reasons
PRs may be rejected for:
- Mod doesn’t load on latest Maker Studio.
sha256in the PR doesn’t match the actual release asset on GitHub.- Obfuscated or minified JS without source.
- Hidden network calls to unknown domains.
- Permissions in manifest don’t match what the code actually uses.
- Misleading name/description.
- License conflicts (your mod repo must be openly available; the mod itself can be any OSI license).