A Claude Code plugin that teaches your AI the four things you actually do with SnapHost, so you can say /snaphost:update instead of explaining the tools every time.
The easiest way: paste this to the AI you already use (Claude Code, Cursor, or any MCP client). It sets SnapHost up the way your assistant supports: installing this plugin if it can, or connecting the hosted MCP server directly. No config files to edit by hand. There are no secrets baked in either: your API token lives on the MCP connection, and the prompt asks you for it at the end.
Set up SnapHost so you can publish, update, and share my sites for me, then confirm it works.
SnapHost is a hosted MCP server at https://app.snaphost.ai/api/mcp that signs in with OAuth, so no API token
or secret ever has to go into this chat.
Start by telling me which of these two situations you are in, then follow only that path:
A. You can add MCP servers yourself (an agentic client such as Claude Code or Cursor):
1. Register SnapHost as a remote MCP server; its URL is https://app.snaphost.ai/api/mcp. In Claude Code
prefer the plugin, which carries the connection and the /snaphost commands:
/plugin marketplace add snaphost-ai/snaphost, then /plugin install snaphost@snaphost.
In any other client, `npx snaphost connect` configures it in one command, or add the
URL by hand. Per-client steps: https://snaphost.ai/docs/mcp-server. Skip the plugin if the
concept does not exist in your environment; the server alone is enough.
2. Start the OAuth browser sign-in and give me exactly ONE thing to do: open the
authorization link and click Authorize (the connection lasts until I revoke it). Put the
https:// link on its own line so it is clickable. Never ask me to paste a token or to
copy any URL back to you.
3. If your environment only picks up new servers after a restart, tell me plainly to fully
restart you once (a reload is not enough), then wait for me to come back.
B. You cannot add MCP servers or connectors yourself (a chat app such as Claude on claude.ai
or the Claude desktop app, where connectors are added in Settings): do not improvise. Walk
me through these exact steps, one at a time, waiting for me after each:
1. Have me open https://claude.ai/customize/connectors in a web browser, not in the desktop
app. Connectors live on my Claude account, so one added there shows up in the desktop
app too, and the browser page is the one place the add control is reliably present.
Warn me that the list I land on holds only the connectors I already have; SnapHost will
not be in it, and that is expected.
2. Have me click the "+" next to the Connectors heading and choose "Add custom connector".
Tell me explicitly NOT to use the "Browse" button: that opens the directory of listed
connectors, SnapHost is not a listed connector, and pasting its URL into the directory
search box only returns unrelated results. Adding by URL is a different control.
3. Name it snaphost and set its URL to https://app.snaphost.ai/api/mcp. Claude then reads the URL and
fills in the rest of the form: under Authentication it selects "Sign in now" and under
OAuth client it selects "Register automatically", both marked "Detected". Tell me to
leave those as they are, not to pick "Use your own OAuth client" (there is no client ID
to enter), and not to add any Request headers. Then scroll to the bottom and click Add.
4. Click Connect next to snaphost and approve the Authorize page that opens in my browser.
5. Only if there is genuinely no "+" on that page, stop looking and ask me one question:
am I an Owner or admin of a Claude Team or Enterprise organization? Branch on my answer
and do not send me back to the Connectors page.
- Yes, I am the Owner: the control is elsewhere for me. Walk me through Settings, then
Organization settings, then Connectors, then Add, with the same name and URL. Everyone
else in the organization then enables it from their own Connectors page.
- No, I am a member: I cannot add a custom connector at all, and nothing on my side will
change that. Give me a short message I can forward to my Claude Owner, asking them to
add a connector named snaphost with the URL above, in Organization settings, then
Connectors, then Add; say it uses OAuth, so there is no shared key, and each person
reaches only their own workspaces. Tell me that meanwhile https://snaphost.ai/docs/mcp-server
has a desktop setup that needs no admin.
- No organization, this is my personal account: on a free Claude plan I may keep only
one CUSTOM connector at a time (connectors I got from the directory do not count), so
I have to remove the other one first. If I have both a personal and a work profile,
switching to the personal one often shows the "+" straight away.
6. New connectors are often only picked up by a fresh conversation, and the desktop app may
need a full restart to see one added in the browser. If you cannot see the snaphost
tools once I am done, tell me to restart the app, start a new chat, enable snaphost in
its tools menu, and paste this same message again.
Confirm it works in the only way that counts: call the SnapHost list_workspaces tool over MCP
and show me what it returns. The connection is mine, not a workspace's: it reaches my personal
space and every workspace I belong to. Then call list_sites in the workspace I name (pass its
workspace_id); an empty list is fine, it means you are connected. If the SnapHost tools
are not available to you, say exactly that and go back to the steps above. Never simulate
success or "test the connection" any other way: no writing code, no creating files or
artifacts, no calling an HTTP API directly. None of that verifies anything.
Throughout, give me one instruction at a time and wait for me between steps; never hand me a
list of things to do at once.
Only if OAuth genuinely cannot work in your environment: say so, and I will create an API token
in the SnapHost dashboard and add it myself; do not ask me to paste it here.The plugin is published as a marketplace on GitHub, so two slash commands install it, and it carries the MCP connection, so nothing else needs adding. The commands then work for every site you own; you name the target site when you invoke them.
# Inside Claude Code: add the marketplace, install the plugin.
# Installing also connects the SnapHost MCP server; authorize in the browser on first use.
/plugin marketplace add snaphost-ai/snaphost
/plugin install snaphost@snaphost
# Now invoke the commands, e.g.
/snaphost:update# Offline or pinned: unzip the archive, then add it from the local path instead:
/plugin marketplace add ./snaphost
/plugin install snaphost@snaphost# No /plugin command? Load it for a single session instead, no install:
claude --plugin-dir ./snaphost/plugins/snaphostThe /plugin commands need a recent Claude Code. If you see “/plugin isn’t available in this environment,” update with npm install -g @anthropic-ai/claude-code@latest and restart your terminal, or just use the --plugin-dir command above, which works without it.
Connect this AI to SnapHost over MCP and verify it can manage your sites.
---
name: connect
description: "Connect this AI to SnapHost over MCP and verify it can manage your sites."
---
Connect this AI client to SnapHost over MCP so it can publish, update, and share the user's
sites. Goal: **one browser click, no token in chat, no copy-paste.** Drive it as a single clear
step at a time, and never hand the user a list of actions to do at once.
## Talking to the user (follow exactly)
- **One step at a time.** Say the single next thing to do, then stop and wait.
- **Make links clickable.** Put any authorization URL on its **own line, raw**: just the
`https://…` with nothing else on the line (no markdown link syntax, backticks, or trailing
punctuation). That is what renders as a clickable link.
- **One live link only.** Each new authorization attempt invalidates the previous link. Show only
the newest URL and tell the user to ignore any earlier ones.
- **Restarts, in plain words.** A newly added MCP server loads only when Claude restarts. When that
is needed, say it exactly like this: *"Fully quit Claude and reopen it (a reload is not enough),
then tell me you're back."* Then stop and wait.
- **Never** ask the user to paste a secret or API token into the chat.
## Steps
1. Call `list_workspaces`. If it returns anything, you are already connected; go to step 4.
2. If SnapHost's tools are not available at all, the server is not registered yet. Register it
one of these ways, then have the user fully quit and reopen Claude:
- Installed from the plugin marketplace (`/plugin marketplace add snaphost-ai/snaphost`
then `/plugin install snaphost@snaphost`)? The plugin already carries the server; a restart
is all that is missing.
- Otherwise add it at user scope (so it works in every project):
```bash
claude mcp add --transport http snaphost https://app.snaphost.ai/api/mcp -s user
```
The same one-liner for any client: `npx snaphost connect` configures Claude Code, Cursor,
VS Code, Windsurf, Codex, and Gemini CLI in one go.
3. Once the server is loaded, start the browser flow. When the client surfaces an authorization
URL, present it as one step: put the raw `https://…` URL on its own line and tell the user to
**click Authorize** (the connection lasts until they revoke it). The credential is then
negotiated machine-to-machine; no token ever enters the chat.
4. Confirm with `list_workspaces`: it proves the connection AND shows its reach. The connection
belongs to the person and covers their personal space plus every workspace they are a member
of, whichever workspace was open when they authorized. Show the user what came back.
- More than one workspace, or a `note` in the result: hand off to `/snaphost:workspace`
before doing anything else, so work lands where the user means it to (a write without a
`workspace_id` is refused on such a connection; an unnamed read is the personal space).
- Exactly one workspace: call `list_sites` in it. An empty list means it works (no sites
yet); offer `/snaphost:publish`. Tell the user they're ready.
## If the Authorize page errors
The redirect lands on a `http://localhost` address handled by Claude Code itself (not SnapHost),
so a "this site can't be reached" page is usually a loopback IPv4/IPv6 mismatch or a timed-out
listener, not a SnapHost failure. Recover in order:
1. Have the user fully quit and reopen Claude, then click the fresh link **promptly**; these links
expire within a minute or two.
2. If it still errors, the URL now in their address bar
(`http://localhost:<port>/callback?code=…&state=…`) is still valid; ask them to paste that one
URL so the client can finish. It is a single-use code, not a reusable secret.
## Token fallback (last resort, only if the browser flow is truly unavailable)
The user creates an API token in the dashboard under **API tokens** (shown once) and runs this in
*their own* terminal, replacing `<token>`, so the secret stays in their shell, never in this chat:
```bash
claude mcp add --transport http snaphost https://app.snaphost.ai/api/mcp --header "Authorization: Bearer <token>" -s user
```
`-s user` stores it in the user's own Claude config; never echo, write, or commit it.
## Notes
- The hosted endpoint is `https://app.snaphost.ai/api/mcp`. Any client that supports HTTP (Streamable) MCP with
OAuth works: point it at that URL and approve the browser prompt.
- After setup the credential lives on the MCP connection; `/snaphost:publish`,
`/snaphost:update`, and `/snaphost:share` never ask for it again.
Pick which SnapHost workspace (the personal space or any workspace the person belongs to) to work in, then create or choose a site.
---
name: workspace
description: "Pick which SnapHost workspace (the personal space or any workspace the person belongs to) to work in, then create or choose a site."
---
Use this skill to choose which SnapHost workspace you are working in, then create a new site or
pick an existing one to work on. The connection belongs to the person: it reaches their personal
space and every workspace they are a member of, so this is a choice of where to work, never of
what the connection can see.
## Steps
1. Call `list_workspaces`. The connection belongs to the person who authorized it and reaches
their personal space plus every workspace they are a member of, whichever workspace was open
when they connected. Each entry has a `workspace_id`, a `handle`, a `name`, a `kind`
(`personal` or `team`), and your `role` (owner, admin, member, or viewer).
2. If there is more than one, show them to the user and ask which to work in. Remember the chosen
`workspace_id` and pass it as `workspace_id` on every following tool call (`list_sites`,
`publish_site`, `update_site`, `set_visibility`, and so on). If there is only one, use it
without asking.
3. Call `list_sites` with the chosen `workspace_id` and show the user the site titles.
4. Ask what they want to do:
- **Create a new site** → use `/snaphost:publish` (pass the same `workspace_id`).
- **Work on an existing one** → confirm which title, then use `/snaphost:update` with its site
id (and the same `workspace_id`), or `/snaphost:share` to change who can view it.
- **Add a data feature** (contact form, waitlist, RSVP, guestbook, counter, clock-in, leaderboard) → use
`/snaphost:data` to give a site a backend it can save and read data from.
## Notes
- Always carry the chosen `workspace_id` through the rest of the session so work lands in the
right workspace. Omit it and a read acts in the person's personal space; a write without it is
refused when the connection reaches more than one workspace.
- You can only act in workspaces `list_workspaces` returns; a `forbidden` error for another id
means the person is not a member of it. A workspace the user expects but does not see is a
membership question, not a connection one: they need to be a member of it in SnapHost.
- If `list_workspaces` returns a `note`, the connection is an older, single-workspace one. Relay
the note: reconnecting SnapHost once gives a connection that covers every workspace.
- Re-run this skill any time to switch workspaces.
Publish the current document to SnapHost as a live, shareable site.
---
name: publish
description: "Publish the current document to SnapHost as a live, shareable site."
---
Use this skill to publish something the user just built or has to hand (an HTML file, a
report, a notebook, a Word document, a slide deck, a spreadsheet, a PDF, an image, a
bundle) to SnapHost as a live site with a stable, shareable link.
## Steps
1. Identify the document to publish. If it is a single self-contained HTML file, read it.
If it is a folder/bundle, zip it (entry document at the root, e.g. `index.html`). A single
file that is not HTML (Markdown, a notebook, a PDF, a .docx/.pptx/.xlsx, a CSV, an image,
a video, an audio, a Mermaid diagram or a text file, or a single React component as a
.tsx/.jsx with a default export) is sent as it is: see step 4. A component may import
only react, react-dom, recharts, lucide-react, d3, framer-motion and @radix-ui/react-*;
rewrite any other import (a local `./component`, a UI kit) into the file before sending it.
2. Choose a stable, human `slug` derived from the title (e.g. "Q3 Investor Update" →
`q3-investor-update`). The slug makes publishing idempotent.
3. Unless a workspace was already chosen in this session, call `list_workspaces`. If it returns
more than one workspace, or a `note`, run `/snaphost:workspace` to choose one, then come
back here: a publish without a `workspace_id` is refused on such a connection, and the
personal space is usually not where a team's sites live. Pass the chosen `workspace_id` on
every call below, the upload calls included: an upload belongs to the workspace it was
created in.
4. Call `publish_site` with a `title`, the `slug`, and the bundle. For a single HTML document
use `html`, whatever its size. For a single file of any other kind, call
`create_bundle_upload` with `filename` (its real name, extension included), send the
file's own bytes against the `upload_id` it returns, and pass that `upload_id` so it is
rendered as its kind. A Word, PowerPoint or Excel file is shown as its page alone: readers
can download the file itself only when `include_original` is true, since it holds speaker
notes, hidden sheets, formulas and comments the page does not show; ask the user before
turning that on. A workbook you wrote with a library carries no computed values, so its
formula cells show the formulas; the result says so in `render_notices`. Relay every
render notice to the user, and offer to open and save the file in Excel or Numbers first
when values matter. For a multi-file app (a built Vite/React site, an image-heavy
page) call `create_bundle_upload` first, send the .zip against the `upload_id` it returns,
then pass that `upload_id` instead. Size is not a concern. Two ways to send it:
- `upload_bundle_part`, one numbered slice per call (split the .zip into slices of at most
`tool_part_max_bytes`, base64 each on its own, send `part` 0, 1, 2, ... in order). These
are tool calls on the same connection as every other SnapHost tool, so they work with no
network of your own.
- PUT the .zip to `upload_url` (`Content-Type: application/zip`; above `max_part_bytes`
split it and PUT each piece to `upload_url/parts/0`, `/parts/1`, ... in order). Fewer,
larger requests, when your shell can reach the SnapHost app host.
Your tool calls and your shell do not share a network. If a PUT is refused (a 403 naming the
host, a connection that never leaves), switch to `upload_bundle_part` and carry on: that
route is unaffected, and nobody needs to change a network setting. Reach for `zipBase64`
only for a small multi-file bundle as a last resort: base64 has to be emitted byte for byte,
and a long string often is not, so it is checked and rejected when it does not verify.
5. Check the `Workspace:` line of the result. If the site landed in the wrong workspace, publish
it again with the right `workspace_id` and `delete_site` the stray copy: a site cannot be
moved between workspaces, and the stray copy is live until deleted.
6. Report the returned `share_url` to the user. Tell them the link is stable: re-running
this skill with the same slug updates the same site in place, and anyone already viewing
is offered a Refresh to the new version.
## Notes
- New sites default to private (allowlist) on Pro. Use `/snaphost:share` to open it
up or add viewers.
- To change an existing site instead of creating a new one, use `/snaphost:update`.
- Building a report, a presentation or a dashboard rather than publishing one that exists? Use
`/snaphost:report`, `/snaphost:presentation` or `/snaphost:dashboard`: they follow the
workspace brand kit and SnapHost's design guide.
- A multi-file app gets client-side routing for free: clean routes like `/about` deep-link
and survive a refresh. Build with `BrowserRouter` and a relative base (`base: './'`), and
derive the router basename from the injected `<base>` tag (`document.querySelector('base')`).
Update an existing SnapHost site in place, keeping its URL.
---
name: update
description: "Update an existing SnapHost site in place, keeping its URL."
---
Use this skill to change the content of an existing SnapHost site without changing its
URL, visibility, or allowlist.
## Steps
1. Determine the target site. Unless a workspace was already chosen in this session, call
`list_workspaces`; with more than one workspace, or a `note`, run `/snaphost:workspace`
and pass its `workspace_id` on every call here. If the user gave a site id, use it.
Otherwise call `list_sites` and match by title, confirming the right one with the user if
ambiguous. A site that is not listed is in another workspace: sites are looked up per
workspace, so pick the right one rather than concluding it is gone.
2. Fetch the current content with `get_site_content` (the site id). It returns the entry
document: `content` as UTF-8 text for HTML/text, or base64 for binary, with
`encoding` telling you which. A site rendered from a file (its `content_kind` is
markdown, notebook, document, presentation, spreadsheet, pdf, image, video, audio,
diagram, text or component) returns its rendered
page, which is not the thing to edit: change the source file and send it again with
`update_site` the way /snaphost:publish sends it (`upload_id` plus `filename`), so the
site keeps its kind. When `get_site` reports `original_included` true, the file itself
is fetched with `get_site_content` and `original: true`. A new version keeps the live
version's download choice unless `include_original` says otherwise.
3. Apply the user's requested change to that content. Edit the real current document; do
not regenerate it from scratch, so wording, structure, and styling are preserved.
4. Save with `update_site` (the site id) passing the edited bundle: `html` for a single
document (the safe default at any size), or an `upload_id` from `create_bundle_upload` for
a multi-file rebuild (send the .zip first, with `upload_bundle_part` on this connection or
a PUT to its `upload_url` where your shell can reach that host). `zipBase64` is a last
resort for a small multi-file bundle; never base64 a single document, and never retry a
base64 payload that was rejected, switch route instead.
5. Confirm: the share URL is unchanged and anyone already viewing it is offered the new
version with a Refresh button. Mention you can roll back via `list_versions` + `rollback_site`.
## Notes
- `get_site_content` is the read half of the round-trip; always read before you write so
you patch the live document rather than overwrite it blindly.
- The token is configured on the MCP connection; never request or paste it.
Control who can view a SnapHost site: visibility, allowlist, password, and link expiry.
---
name: share
description: "Control who can view a SnapHost site: visibility, allowlist, password, and link expiry."
---
Use this skill to manage who can view a SnapHost site and how the link behaves.
## Steps
1. Identify the site (a site id, or resolve by title with `list_sites`). Sites are looked up
per workspace: unless one was chosen this session, call `list_workspaces` and, with more
than one workspace or a `note`, run `/snaphost:workspace` and pass its `workspace_id`
on every call here.
2. Apply the change the user asked for:
- **Public vs private**: `set_visibility` with `public` (anyone with the link) or
`allowlist` (verified viewers only).
- **Share with more people**: `add_to_allowlist` adds emails and/or domains *without*
removing anyone already on the list; this is the default for "share with X". Check who is
already on it first with `list_allowlist` (or `get_site`). Use `remove_from_allowlist`
to take specific people off, and only reach for `set_allowlist` when the user explicitly
wants to replace the whole list (it overwrites everyone; an empty list clears it).
- **Password**: `set_view_password` to protect it, `clear_view_password` to remove it.
- **Expiry**: `set_link_expiry` with an ISO 8601 timestamp, or null to clear.
3. For private sites, manage pending requests with `list_access_requests` and then
`approve_access_request` / `deny_access_request`.
## Notes
- Private sharing, allowlists, passwords, and setting a link expiry are Pro
features; if a call returns a `plan_limit` error, tell the user which plan unlocks it.
Clearing a password or an expiry works on every plan.
- Visibility, allowlist, and password changes never change the share URL.
Add a data feature (contact form, waitlist, RSVP, guestbook, counter, leaderboard) to a SnapHost site so the published page can save and read data.
---
name: data
description: "Add a data feature (contact form, waitlist, RSVP, guestbook, counter, leaderboard) to a SnapHost site so the published page can save and read data."
---
Use this skill to give a SnapHost site a backend: the published page persists and reads data
through the injected `window.SNAPHOST` SDK, with no server to run. SnapHost Data needs
Pro; a `plan_limit` error means the owner needs to upgrade.
## Adding a ready-made feature (the fast path)
1. Identify the site (a site id, or resolve by title with `list_sites`). Sites are looked up
per workspace: unless one was chosen this session, call `list_workspaces` and, with more
than one workspace or a `note`, run `/snaphost:workspace` and pass its `workspace_id`
on every call here.
2. List the templates with `list_data_recipes` (contact form, waitlist/email signup, RSVP,
guestbook, shared counter, clock-in, leaderboard). Pick the one matching what the user asked for.
3. Call `apply_data_template` with the site id and recipe id. It provisions the collection (its
access mode and schema) and **returns an HTML snippet**.
4. Add that HTML to the user's page: fetch the current document with `get_site_content`, merge the
snippet into the right place (do not regenerate the page from scratch), then `update_site`.
Confirm the change is live.
## Building something custom
The injected SDK is global and needs no keys; every call returns a promise.
- **Append**: `await window.SNAPHOST.append('<collection>', { ...yourFields })`. The collection is
created on first write; never create it first. Records are immutable and append-only; an "edit" is
a new event carrying the same `key` field.
- **Read**: `const { records } = await window.SNAPHOST.query('<collection>', { order: 'desc', limit: 50 })`.
Each record is `{ id, value, created, actor }`: read the owner's fields from `record.value` and
the timestamp from `record.created`. Query options: `order` ('asc' | 'desc'), `limit`,
`where: [{ field, value }]` (equality filters), and `latest: true` to fold to the newest record
per `key` (current state instead of full history).
- **Who is viewing**: `const me = await window.SNAPHOST.viewer()` resolves (after `ready`) to the
current viewer's identity. A signed-in viewer of a private / allowlist site gives
`{ id: 'user@example.com', kind: 'viewer' }`; a public site or a password-only viewer gives
`{ kind: 'anonymous' }` (no `id`). `viewer` shows up in `Object.keys(window.SNAPHOST)`.
- **Read config (key-value)**: `await window.SNAPHOST.get('<namespace>', '<key>')` and
`await window.SNAPHOST.list('<namespace>')` read owner-set values (set via `set_site_data`).
- **Render safely**: write values with `textContent`, never `innerHTML`.
- **Access mode** (`set_collection_access`): `public_read` (anyone can read: a guestbook or
counter) or `append_only` (visitors write but only the owner reads: a contact form or
waitlist; submissions are never readable by other visitors). You can also pause writes.
Publish/update scans the page's code and pre-configures new collections to match it (one the
page `query()`s becomes `public_read`, one it only `append()`s to stays `append_only`) and
reports what it did in the tool result; an owner's explicit setting is never overridden.
- **Schema** (`define_collection_schema`): an optional JSON Schema validates every appended record
server-side. Set it for the user when their data has a fixed shape.
- **Inspect / moderate**: `list_site_collections`, `query_site_records`, `export_site_data`,
`delete_site_record`. Attribution on each record is server-stamped from the verified viewer on a
private site, never from the request body. Note: collections are per-site, so a page only sees its
own site's collections.
## Attributing a record to a person ("who did this")
For any feature that shows who did something (leaderboard, high scores, guestbook, comments,
clock-in, RSVP): capture the viewer with `window.SNAPHOST.viewer()` and **write the identity into
the record's `value`** (a `who` field). `actor` is server-stamped and visible to the owner in the
dashboard and exports, but it is **redacted in-page** (only `kind`, never the id), so never read
`actor` back to display who wrote a record, and never fall back to `window.prompt()` for a name.
**Aggregation rule** for a per-person board: records that HAVE an identity fold to that person's
best (or latest) row; records with NO identity are anonymous and must each stay their own row,
never merged into a single "Anonymous". Worked snippet (capture -> stamp -> aggregate, top N):
```html
<script>
let me = { kind: 'anonymous' }
window.SNAPHOST.viewer().then(v => { me = v })
async function submitScore(score) {
const entry = { score }
if (me.kind === 'viewer' && me.id) entry.who = me.id // stamp identity into the value
await window.SNAPHOST.append('leaderboard', entry)
render()
}
async function render() {
const { records } = await window.SNAPHOST.query('leaderboard', { order: 'desc', limit: 500 })
const best = new Map(), anonymous = []
for (const r of records) {
const who = (r.value.who || '').trim() // never read r.actor here (redacted in-page)
const score = Number(r.value.score) || 0
if (!who) { anonymous.push({ label: 'Anonymous', score }); continue }
const key = who.toLowerCase(), prior = best.get(key)
if (!prior || score > prior.score) best.set(key, { label: who, score })
}
const rows = [...best.values(), ...anonymous].sort((a, b) => b.score - a.score).slice(0, 10)
const list = document.getElementById('leaderboard')
list.replaceChildren()
for (const row of rows) {
const li = document.createElement('li')
li.textContent = row.label + ': ' + row.score // textContent, never innerHTML
list.appendChild(li)
}
}
</script>
```
## Notes
- Recommend the ready-made templates first; they are the simplest path for a non-technical owner.
- Always re-publish (`update_site`) after adding a snippet, or the feature will not be live.
Set up or update the workspace's brand kit (colours, fonts, logo, locale, tone) from a brand guideline or website, so every page your AI builds is on brand.
---
name: brand
description: "Set up or update the workspace's brand kit (colours, fonts, logo, locale, tone) from a brand guideline or website, so every page your AI builds is on brand."
---
Use this skill to give a SnapHost workspace its brand kit, once, so every page, report, deck and
dashboard built in it starts on brand: the palette, the fonts, the logo, the language and locale,
and how the brand writes. The person brings the brand; you do the extracting.
## Steps
1. **Workspace.** Call `list_workspaces` and, with more than one, run `/snaphost:workspace`. The
kit belongs to a team workspace on a paid plan, and only an owner or admin may save it.
2. **What exists.** Call `get_brand_kit`. If a kit exists, show it and ask what should change.
3. **Find the brand.** Ask for whichever they have, best first: a brand guideline (PDF), their
website, or a screenshot of a page they consider on brand. Read it and extract:
- colours for every role: primary, primaryStrong (a darker primary for hover and links),
secondary, accent, ink (body text), muted (captions), line (hairlines), surface (the page),
surfaceTint (a faint panel), onPrimary and onSecondary (text on those colours), all #rrggbb;
- the heading and body fonts, and whether they are Google Fonts or system fonts (a licensed
font that is neither becomes its nearest Google Fonts match, said out loud);
- the logo and a small mark or emblem: an https URL of a PNG, SVG or WebP file on their site
(open the site's HTML to find the real file, not a page that shows it), or, when the logo is
drawn inline in the page's HTML, the `<svg>` markup itself for `logo_svg`. A logo that only
exists as a JPEG or a photo is uploaded on the workspace's Brand page instead; say so and
save the kit once that is done;
- the language and locale (`is`, `is-IS`), a few sentences of tone, and any words to avoid;
- the look, five choices each from a small set: `radius` (sharp, soft or round, from the
buttons and cards), `density` (airy, regular or dense, from the white space), `emphasis`
(calm or bold headlines, from the display type), `cover` (dark, light or split, from the hero) and
`surfaces` (tinted panels or ruled sections, from how the site boxes its content). These
are what make two customers' pages look different before any AI choice; say which you read
and which you guessed.
4. **Confirm.** Show the proposed kit as a table with each colour's hex and role, the fonts, the
logo URL, the locale and the five look choices in plain words. Point out anything you
guessed. Wait for a yes.
5. **Save** with `set_brand_kit`. Relay every warning (low contrast is the usual one) and offer a
corrected colour.
6. **Use it.** Offer to rebuild one existing page with the kit, or to start a report with
`/snaphost:report`.
## Notes
- The logo is fetched and hosted by SnapHost; pages use the kit's own logo URL from
`get_brand_kit`, never the source URL.
- A kit with Google fonts hands pages `fontLinkHtml` to paste into `<head>`.
- A kit with Google fonts has those fonts allowed on every page it publishes.
- The kit can also be edited on the workspace's Brand page in the dashboard.
Turn a report, a PDF or a dataset into an on-brand, responsive web report with a table of contents, clear figures and a PDF that keeps the look.
---
name: report
description: "Turn a report, a PDF or a dataset into an on-brand, responsive web report with a table of contents, clear figures and a PDF that keeps the look."
---
Use this skill to build a web report the person can send as a link: on their brand, readable on a
phone and a desktop, and good enough on the first pass that they only check it, never redesign it.
The source can be anything: a PDF, a Word or PowerPoint file, a spreadsheet, figures pasted into
the chat, or an existing page.
## Steps
1. **Read the source first.** Work out what it is, who it is for, its language, its period and
its key numbers before asking anything. When it is a summary (a press release, a news item,
a slide someone forwarded), find the document it summarises and read that: the comparisons
that give a number meaning (the budget, last year, the limit) live only in the primary
document.
2. **Brief, in one message.** Call `get_design_guide` with `kind: "report"` (no traits
yet) and ask only the brief questions the source did not answer, all at once, each with the
default you will use. Accept "you choose". A report is `printable` unless the person says no: almost every report is printed or saved as a PDF by someone.
3. **Workspace.** Unless one was chosen this session, call `list_workspaces`; with more than one
workspace or a `note`, run `/snaphost:workspace` and pass its `workspace_id` on every call.
4. **Brand.** Call `get_brand_kit`. With a kit, paste its `tokens_css` first in the page's
style block and its `fontLinkHtml` (when present) into `<head>`, use its logo URL, its
fonts, its locale and its tone. With none, offer
`/snaphost:brand` once ("I can set up your brand from your guideline or website so every
page matches"); if they decline, use the neutral palette in the guide and carry on.
5. **Guide.** Call `get_design_guide` again with the traits the brief settled on, and read all
of it. It is the bar.
6. **Build** from `get_page_recipe "report-shell"` and the parts in `list_page_recipes`. Replace
every [bracketed] placeholder. Format every number and date for the kit locale.
Open with a cover (logo, title, period, author, three to five key figures designed for this
report) and a summary of the three findings; one finding per chapter; methodology and full
tables in an appendix. The sidebar contents shows group headings and marks the current
chapter; on a phone a top bar names the current section and opens the contents on tap.
7. **Review loop, before publishing.** Score the page against every rubric item in the guide, in
writing, as pass or fail, at 375, 768 and 1280 pixels wide (and in print preview when
printable). Fix every failure, then score again. If you can render the page (a browser or
screenshot tool), look at it at each width; otherwise read the CSS for each width.
Do not publish with a failing item. A slide or chapter that is a headline, one number and a
sentence fails however clean its CSS is.
8. **Publish** with `publish_site` (or `update_site` for an existing site), passing
`document_kind: "report"` and `printable` when it must print. Use a stable slug.
9. **Fix the warnings.** Fix every `brand_warnings` and `design_warnings` entry with
`update_site` until both are empty or every remaining one is a declared, deliberate exception.
10. **Hand over.** Give the `share_url` and ask for exactly one round: "Open it on your phone and
on your desktop and tell me anything that looks off." Mention `/snaphost:share` for who can
open it.
## Notes
- The guide and the rubric are served by SnapHost and may change; always fetch them, never work
from memory.
- Keep the person's words and numbers. Restructure and design, never invent data.
- Never paste a secret or token into the page.
Build an on-brand slide presentation as a live link: full screen, keyboard and swipe, and a PDF handout.
---
name: presentation
description: "Build an on-brand slide presentation as a live link: full screen, keyboard and swipe, and a PDF handout."
---
Use this skill to build a slide presentation the person can send as a link: on their brand, readable on a
phone and a desktop, and good enough on the first pass that they only check it, never redesign it.
The source can be anything: a PDF, a Word or PowerPoint file, a spreadsheet, figures pasted into
the chat, or an existing page.
## Steps
1. **Read the source first.** Work out what it is, who it is for, its language, its period and
its key numbers before asking anything. When it is a summary (a press release, a news item,
a slide someone forwarded), find the document it summarises and read that: the comparisons
that give a number meaning (the budget, last year, the limit) live only in the primary
document.
2. **Brief, in one message.** Call `get_design_guide` with `kind: "presentation"` (no traits
yet) and ask only the brief questions the source did not answer, all at once, each with the
default you will use. Accept "you choose". Add `printable` when they want a handout; the deck then prints one slide per landscape page.
3. **Workspace.** Unless one was chosen this session, call `list_workspaces`; with more than one
workspace or a `note`, run `/snaphost:workspace` and pass its `workspace_id` on every call.
4. **Brand.** Call `get_brand_kit`. With a kit, paste its `tokens_css` first in the page's
style block and its `fontLinkHtml` (when present) into `<head>`, use its logo URL, its
fonts, its locale and its tone. With none, offer
`/snaphost:brand` once ("I can set up your brand from your guideline or website so every
page matches"); if they decline, use the neutral palette in the guide and carry on.
5. **Guide.** Call `get_design_guide` again with the traits the brief settled on, and read all
of it. It is the bar.
6. **Build** from `get_page_recipe "presentation-shell"` and the parts in `list_page_recipes`. Replace
every [bracketed] placeholder. Format every number and date for the kit locale.
Eight slides is the shape of a deck of numbers: a cover with one visual and the sentence to
remember; the year in four key figures with change against budget and last year; plan to
outcome (the `waterfall-chart` recipe, with a toggle that takes the one-off out); where the
money comes from (`donut-chart`); where it goes; the trend over years against any limit
(`line-chart`); risks and the auditor's opinion (the shell's accordion beside its card); a
summary of four numbers. Reach a shorter deck by merging views, never by dropping the
comparisons. Every data slide has a sentence headline, a lede, one visual, its comparison and
a source line, in two columns on desktop, and one control where it adds understanding (a view
toggle, a one-off toggle, hover values), each a button with `aria-pressed`. Choose each
slide's layout from the shell's vocabulary by its content shape (grid, split, chart, compare,
statement, quote, list); never give two consecutive data slides the same layout, and open with
the cover treatment the kit's `style.cover` names. The subject decides the shape: the guide
names five. Speaker notes go in the hidden notes aside. A headline, a lone number and a
sentence is not a slide.
7. **Review loop, before publishing.** Score the page against every rubric item in the guide, in
writing, as pass or fail, at 375, 768 and 1280 pixels wide (and in print preview when
printable). Fix every failure, then score again. If you can render the page (a browser or
screenshot tool), look at it at each width; otherwise read the CSS for each width.
Do not publish with a failing item. A slide or chapter that is a headline, one number and a
sentence fails however clean its CSS is.
8. **Publish** with `publish_site` (or `update_site` for an existing site), passing
`document_kind: "presentation"` and `printable` when it must print. Use a stable slug.
9. **Fix the warnings.** Fix every `brand_warnings` and `design_warnings` entry with
`update_site` until both are empty or every remaining one is a declared, deliberate exception.
10. **Hand over.** Give the `share_url` and ask for exactly one round: "Open it on your phone and
on your desktop and tell me anything that looks off." Mention `/snaphost:share` for who can
open it.
## Notes
- The guide and the rubric are served by SnapHost and may change; always fetch them, never work
from memory.
- Keep the person's words and numbers. Restructure and design, never invent data.
- Never paste a secret or token into the page.
Build an on-brand dashboard that keeps itself current and refreshes open screens on its own, for a link or an office screen.
---
name: dashboard
description: "Build an on-brand dashboard that keeps itself current and refreshes open screens on its own, for a link or an office screen."
---
Use this skill to build a dashboard the person can send as a link: on their brand, readable on a
phone and a desktop, and good enough on the first pass that they only check it, never redesign it.
The source can be anything: a PDF, a Word or PowerPoint file, a spreadsheet, figures pasted into
the chat, or an existing page.
## Steps
1. **Read the source first.** Work out what it is, who it is for, its language, its period and
its key numbers before asking anything. When it is a summary (a press release, a news item,
a slide someone forwarded), find the document it summarises and read that: the comparisons
that give a number meaning (the budget, last year, the limit) live only in the primary
document.
2. **Brief, in one message.** Call `get_design_guide` with `kind: "dashboard"` (no traits
yet) and ask only the brief questions the source did not answer, all at once, each with the
default you will use. Accept "you choose". A dashboard is `live`; add `kiosk` when it runs on an office screen.
3. **Workspace.** Unless one was chosen this session, call `list_workspaces`; with more than one
workspace or a `note`, run `/snaphost:workspace` and pass its `workspace_id` on every call.
4. **Brand.** Call `get_brand_kit`. With a kit, paste its `tokens_css` first in the page's
style block and its `fontLinkHtml` (when present) into `<head>`, use its logo URL, its
fonts, its locale and its tone. With none, offer
`/snaphost:brand` once ("I can set up your brand from your guideline or website so every
page matches"); if they decline, use the neutral palette in the guide and carry on.
5. **Guide.** Call `get_design_guide` again with the traits the brief settled on, and read all
of it. It is the bar.
6. **Build** from `get_page_recipe "dashboard-shell"` and the parts in `list_page_recipes`. Replace
every [bracketed] placeholder. Format every number and date for the kit locale.
Pick the guide's republish model (a scheduled routine rebuilds the page) unless the numbers
move within the hour on a public page, in which case use the data model (a routine writes one
key-value item per tile with `set_site_data`, and the page polls them). A private dashboard
always uses the republish model. Say which you chose and set it up: after publishing, call
`set_render_options` with `auto_refresh: true` so open screens reload on their own, and
for the republish model offer to schedule the routine that updates it.
7. **Review loop, before publishing.** Score the page against every rubric item in the guide, in
writing, as pass or fail, at 375, 768 and 1280 pixels wide (and in print preview when
printable). Fix every failure, then score again. If you can render the page (a browser or
screenshot tool), look at it at each width; otherwise read the CSS for each width.
Do not publish with a failing item. A slide or chapter that is a headline, one number and a
sentence fails however clean its CSS is.
8. **Publish** with `publish_site` (or `update_site` for an existing site), passing
`document_kind: "dashboard"` and `printable` when it must print. Use a stable slug.
9. **Fix the warnings.** Fix every `brand_warnings` and `design_warnings` entry with
`update_site` until both are empty or every remaining one is a declared, deliberate exception.
10. **Hand over.** Give the `share_url` and ask for exactly one round: "Open it on your phone and
on your desktop and tell me anything that looks off." Mention `/snaphost:share` for who can
open it.
## Notes
- The guide and the rubric are served by SnapHost and may change; always fetch them, never work
from memory.
- Keep the person's words and numbers. Restructure and design, never invent data.
- Never paste a secret or token into the page.