Docs

Publish from Claude Code or Codex

Medal Play ships as one plugin for Claude Code and Codex. Its medal-play skill prepares a web game, adds Medal clips and scores when you ask for them, and publishes the game with the Medal Play CLI. You can also run the CLI yourself.

Before you start

  • Node.js 22 or newer. The skill runs the CLI through npx.
  • Claude Code or the Codex CLI.
  • A web game that runs from static files, with an index.html.

1. Install the plugin

The plugin and its skill are the same in both agents. Install it in the one you use. Both install it from medal.games, with no GitHub or npm account.

Claude Code

From a terminal:

claude plugin marketplace add https://medal.games/marketplace.json
claude plugin install medal-play@medal-play

Or inside Claude Code:

/plugin marketplace add https://medal.games/marketplace.json
/plugin install medal-play@medal-play

Plugins load when a session starts, so start a new one. claude plugin list shows medal-play@medal-play as enabled.

If you added the marketplace from GitHub during early access, run claude plugin marketplace remove medal-play first. Claude Code won't add a marketplace of the same name from another source.

Codex

codex plugin marketplace add https://medal.games/marketplace.git
codex plugin add medal-play@medal-play

codex plugin list shows medal-play@medal-play as installed and enabled. Start a new Codex session to load the skill. If you added the marketplace from GitHub during early access, this replaces it.

Publishing uploads files, so it needs the network. When Codex runs commands in a sandbox without network access, it asks to run the publish command outside the sandbox. Approve it.

2. Publish a game

Open your agent in the game's folder and ask in plain words:

publish this on Medal

Ask for more when you want it:

publish this on Medal, clip when the player wins, and post the high score

The agent then:

  1. Runs the project's build, if it has one, and finds the folder with index.html: the first of dist/, build/, out/, the project root and public/.
  2. Adds window.medal calls only where you asked for them. Asked only to publish, it leaves your code alone. The clip button works on every game anyway.
  3. Writes medal.json with the game's name, description, clip events and leaderboard order.
  4. Runs npx --yes --@get-wrecked:registry=https://medal.games/npm @get-wrecked/medal-play@latest publish --json.
  5. Tells you the live URL, the claim link and which calls it added.

On success the CLI prints:

{
  "url": "https://star-dodge.medal.games",
  "claimUrl": "https://medal.games/claim/…",
  "slug": "star-dodge",
  "version": "…"
}

The first publish creates a publisher key for this machine and saves it in ~/.config/medal-play/credentials.json. For now only Medal employees can publish, so a new key first gets medal_login_required and then publisher_not_approved.

3. Claim your game

While only Medal employees can publish, every key is linked to a Medal account before its first game, so games start claimed and claimUrl is null. The rest of this step is for when publishing opens up.

A new game is live but not yet listed, and it expires after 7 days unless you claim it. Open the claim link, sign in with Medal and press Claim. The game then stays up under your Medal account and appears on the games page.

To skip claim links, link this machine to your Medal account once:

npx --yes --@get-wrecked:registry=https://medal.games/npm @get-wrecked/medal-play@latest login

It prints a code and a link to the approval page. Sign in there and approve the code, and the CLI finishes on its own. The games this machine already published become yours, and later ones start claimed, with a claimUrl of null. Only approve a code you started yourself.

4. Update a game

Publish again from the same folder on the same machine, through your agent or the CLI. The CLI wrote the game's slug into medal.json on the first publish, and the publisher key shows the game is yours, so the new version replaces the old one at the same address. Keep medal.json in the project and keep the credentials file.

A claimed game is listed on the games page once someone at Medal approves the listing, and the approval lasts when you publish again. To keep one off it, set "visibility": "unlisted" in medal.json and publish again; setting "public" brings it back without a new approval.

Publish from the terminal

The skill runs the same CLI you can run yourself, from the project root:

npx --yes --@get-wrecked:registry=https://medal.games/npm @get-wrecked/medal-play@latest publish
npx --yes --@get-wrecked:registry=https://medal.games/npm @get-wrecked/medal-play@latest login

Medal Play serves the CLI from its own npm registry, which the --@get-wrecked:registry flag points npm at, so it needs no npm account or ~/.npmrc entry.

--dir <folder>
The folder with the built index.html. Default: the first of dist/, build/, out/, . and public/ that has one.
--json
Prints the result as JSON. On failure it prints {"error": {"code", "message", "hint"}} and exits non-zero.
--env prod|staging|test
The Medal Play environment to publish to. Default: prod, which is medal.games.

Add clips and scores

Medal Play injects its SDK into every page of a hosted game as window.medal. Call it through window.medal?. so the game still works anywhere else, and don't bundle the SDK or add a script tag for it. The overlay draws the clip button, sign-in and the leaderboard, so the game doesn't need its own.

// Clip the moment the player wins, with a marker on the clip's timeline.
window.medal?.event('win', { title: 'Level 3 cleared' })

// Save the last 25 seconds after a big play.
window.medal?.clip({ title: 'Five-hit combo', seconds: 25 })

// At the end of a run, post the score and show the leaderboard.
window.medal?.scores.submit(score)
window.medal?.scores.show()
event(name, { title, seconds, clip })
Marks a moment. It clips when medal.json lists the name in clipEvents, when clip is true, or when there is no clipEvents list.
clip({ title, seconds })
Saves the last seconds of play, 15 by default, and shows the clip toast.
scores.submit(score)
Posts a score under the signed-in player and keeps each player's best. A signed-out player is asked to sign in first.
scores.top({ limit }), scores.show()
Reads the top scores, or opens the leaderboard.
init({ canvas, bufferSeconds, overlay, hotkey, capture })
Optional. Picks the canvas to record when the page has several, keeps 5 to 30 seconds of video (30 by default), and turns off the overlay or the F8 clip hotkey. capture: 'page' lets players clip the whole page, HTML included.
user(), signIn()
The signed-in Medal player, or a sign-in popup. Call signIn() from a click or key press.

The recorder captures the game's canvas and its Web Audio at up to 1280×720 and 30 fps. With capture: 'page', the overlay offers a button that shares the tab, and from then on it captures the whole page without the overlay, in desktop Chromium. Nothing leaves the player's browser unless they choose Post to Medal. TypeScript projects get the types from the CLI package with import type {} from '@get-wrecked/medal-play/sdk'.

medal.json

Optional, in the project root. The CLI fills in anything missing and writes the slug back after the first publish.

{
  "name": "Star Dodge",
  "description": "Dodge the falling stars for as long as you can.",
  "clipEvents": ["win"],
  "leaderboard": { "order": "desc" },
  "input": ["keyboard", "mouse", "touch"],
  "orientation": "portrait",
  "visibility": "public"
}
name
At most 60 characters. Default: the page's <title>, else the folder name.
slug
The game's address: 3 to 25 characters of a-z, 0-9 and -. Default: made from the name.
description
At most 280 characters. Default: the page's meta description.
thumbnail
A PNG, JPEG or WebP in the build, the game's art on medal.games.
icon
A square PNG, JPEG or WebP in the build, the box art on medal.games' featured cards. Default: the thumbnail.
clipEvents
The medal.event() names that clip. Without the list, every event clips.
leaderboard.order
"desc" when a higher score is better, the default. "asc" for times and other scores where lower wins.
leaderboard.season
Default 1. Raise it to start an empty board when the build goes live; earlier seasons stay readable with ?season=N. leaderboard.seasonName names it.
input
Any of keyboard, mouse, touch and gamepad. Default: read from the game's event listeners.
orientation
landscape, portrait or any, the default.
visibility
public, the default, or unlisted. A claimed public game is listed on medal.games once someone at Medal approves it. unlisted keeps it off, and public shows it again.

Limits

  • 100 MB and 2,000 files per build.
  • 25 MB per file, and 5 MB per HTML file.
  • Dotfiles, node_modules/ and .git/ are never uploaded.
  • Static files only: no server code, databases or secrets.

Troubleshooting

npx fails with 401, 403 or 404 for @get-wrecked/medal-play
npm looked for the package somewhere other than medal.games. Keep the --@get-wrecked:registry=https://medal.games/npm flag in the command.
Claude Code won't add the marketplace
A marketplace named medal-play from another source, such as the GitHub repository from early access, is already there. Remove it with claude plugin marketplace remove medal-play and add it again.
Publishing fails with medal_login_required
Only Medal employees can publish for now, and each machine's key is linked to a Medal account first. Run the login command from step 3, then publish again.
Publishing fails with publisher_not_approved
The key is linked, and a Medal employee approves it next. Open the link in the error's hint, sign in with your Medal work account, check the Medal username and approve the key, then publish again. Each machine has its own key, so each one is approved once.
Publishing fails with slug_taken
The slug in medal.json belongs to another game, or to one published from another machine. Use the slug the error's hint suggests, or remove slug to get a free one.
Clips fail with no_canvas
The game has no <canvas> yet. When a page has several canvases, call window.medal?.init({ canvas }) with the main one. A game without one can be clipped only with capture: 'page', once the player shares the tab.

Keep the plugin current

Every publish runs the latest CLI, so CLI fixes need no update. To pick up a newer skill in Claude Code, then restart it:

claude plugin marketplace update medal-play
claude plugin update medal-play@medal-play

In Codex:

codex plugin marketplace upgrade
codex plugin add medal-play@medal-play