Docs
Connect a site to Tusk.
You keep the codebase and the hosting. You add one attribute per editable thing, Tusk builds the editor from it, and a small build step (or a deploy hook) brings the published words and photos into the site. Nothing of Tusk runs between a visitor and the site. It works for any site whose build can run a script or that has a deploy hook: static, Next, Astro, Eleventy, plain HTML. Sites that need a login to view cannot be previewed.
Connect a site with your AI tool
Three steps for you, then your agent does the work. It runs over a token that can reach one site and only the actions you approve; it never gets your hosting credentials and Tusk never hosts the site.
1. Add the Tusk MCP server to Claude Code, Codex, Cursor or any tool that speaks MCP. It is on npm; nothing to install.
{ "mcpServers": { "tusk": { "command": "npx", "args": ["-y", "@tuskcms/mcp"] } } }2. Ask the agent to connect the site. Something like “Connect this site to Tusk CMS” is enough. It calls connect_site with the site’s name and address and replies with one link.
3. Approve the link. Open it in a browser where you are signed in to Tusk and press Approve & connect. That one tap adds the site to your studio and connects the agent to it; if the site is already in Tusk, it simply connects the agent. The approval screen names the site and the exact actions being granted. The token is saved on your machine and never shown. From there the agent marks the editable elements in your own files with data-tusk attributes (attributes only: no layout, style or wording changes), adds the two build scripts, creates a deploy hook in your own host and records it in Tusk, publishes once, and checks that the publish reached the live site.
Agent without the connect_site tool? Add the site in the dashboard, then run this in the repository with its slug and approve the link it prints, before you ask:
npx @tuskcms/mcp login --site <your-site-slug> --scopes scan,fields,content,publish,deploy,inviteThen invite the client. On the manage page, People › Give a client access emails them a link; they continue with Google or Microsoft, land in the editor on their site, and see only the fields that were marked. If you granted invite, the agent can do this for you with invite_client once the site goes live.
Expect a few minutes for a small site, most of it your host’s first build. The agent will still stop for things only you can do: signing in to your host, and setting TUSK_TOKEN in your host’s environment if it cannot reach your host’s CLI. It has no way to create a Tusk account or to pay for anything.
What the token can and cannot do: it is bound to the one site named on the approval screen and to the listed actions. It cannot reach your other sites, cannot mint another token, and cannot touch your code or hosting on its own. It expires by itself after 30 days (--ttl up to 90); that limits only the agent’s access, never the site, its build or the client’s login; you get an email the moment it is approved, and you can revoke it earlier in the dashboard, under Studio settings > Connected tools & devices. The build token is a different secret: the MCP writes it straight into a git-ignored .env and never shows it to the agent. It lets a build read published content and nothing else, and lives in .env and your host’s environment variables only.
Connect a site by hand
The MCP does exactly these four steps. Do them yourself when there is no agent in the loop, or to understand what the agent did. Four steps: mark, deploy, scan, invite. About half an hour for a site that is already marked up.
1. Mark the elements. Put data-tusk="page.field" on the element whose content the client edits. Tusk infers the kind from the tag; set data-tusk-kind when it guesses wrong.
<body data-tusk-page="Home">
<h1 data-tusk="home.headline">A small studio.</h1> text
<p data-tusk="home.intro">Thoughtful spaces, made for everyday
life. We work with families and small businesses.</p> textarea
<img data-tusk="home.hero" data-tusk-label="Hero photo"
data-tusk-aspect="16:9" src="/img/hero.jpg" alt="A bright room"> photo
<a data-tusk="home.brochure" href="/downloads/capability.pdf">
Capability brochure</a> file
<div data-tusk-list="home.team" data-tusk-label="Team" data-tusk-max="8">
<article data-tusk-item>
<img data-tusk-field="photo" data-tusk-aspect="1:1" src="/img/mia.jpg" alt="">
<h3 data-tusk-field="name">Mia Chen</h3>
<p data-tusk-field="bio" data-tusk-kind="textarea">Mia set up the studio in 2019.</p>
</article>
<article data-tusk-item>…</article>
</div> listOne field, one element. Never nest a mark inside another mark, and never mark the same field twice on a page. Text that appears on every page, like a footer line, goes under the page key globals and is marked once. Leave navigation, form controls and legal text alone.
data-tusk-kind="html" marks an element whose inline markup the client may edit, not just its text. Allowed inline tags: a, span, b, strong, i, em, u, s, small, sup, sub, mark, code, abbr, time, q, br, wbr, del, ins; safe attributes only; script/style/svg/handlers/style attribute and non-http(s)/mailto/tel links are stripped. For a heading that merely contains a styled span, a br, or a leading inline svg icon, you do not need this: mark it as normal text and Tusk edits only its own words, leaving the decorative children in place.
2. Deploy the site as you normally would. Tusk reads the rendered HTML, so the marks must be in what the server sends.
3. Scan. In Tusk, add the site, open its manage page and paste the page addresses into Scan for editable fields. Tusk fetches each page, reads the marked elements, builds the editor and takes the words already on the page as starting content. A rescan refreshes pages nobody has published yet; anything a client has published is kept. You can also click any text in the manage page’s preview to make it editable without marking the code at all.
4. Invite the client. Give a client access on the manage page sends an invitation link. They continue with Google or Microsoft, land in the editor on their site, and see only the fields you marked.
The scan reports, per address, how many fields it found on which page, or why it could not read the page. Open the editor as yourself and check the fields against the page before you invite anyone.
An agent without an MCP
If your tool cannot use MCP servers, paste this one line into it instead. It reads the public guide and follows the same four steps by hand, driving your browser as well as your repository. Budget 30–40 minutes for a small site, and expect it to stop a few times for a click only you can make: your host’s sign-in, the build token, the domain.
Connect this site to Tusk CMS. Read https://tuskcms.com/llms-full.txt first and follow it exactly.The guide is public and unauthenticated: the attribute grammar with an example per kind, the field kinds, the end-to-end sequence, how to wire the build, how to render the pulled content, an endpoint table, and a verification checklist the agent has to pass before it may report success. It contains no token, key, deploy hook or account identifier, and it tells the agent in one place it cannot miss to keep the build token in .env and the host’s environment variables and never to print, echo or commit it. The same text is at GET /api/connect and indexed from /llms.txt.
Once the site is in Tusk, Manage > Connect with your AI tool generates the same instruction for that site, so the keys the agent writes into the HTML are the keys Tusk already holds. It never contains the build token, only where the token lives. The same schema is available as JSON at GET /api/studio/sites/:slug/schema while signed in.
Get content into your build
Every publish writes a snapshot. Your build takes a copy before it compiles, and the finished site serves its own words and photos. Two single-file scripts do the work: plain Node 18 or newer, no packages, nothing to install. Download them into the site’s repository and commit them.
curl -O https://tuskcms.com/sdk/tusk-pull.mjs
curl -O https://tuskcms.com/sdk/tusk-apply.mjs1. Pull the snapshot before the build
tusk-pull.mjs downloads the snapshot to .tusk/published.json and every photo to public/tusk/, rewriting the photo URLs to /tusk/<filename>. Run it as prebuild with three variables set on the host, never in the repo. The build token is under Going live › Advanced on the manage page (or from the MCP’s rotate_build_token). The script prints one summary line and exits non-zero, with the reason, when the token, the slug or the network is wrong; it never prints the token.
TUSK_SITE=north-studio
TUSK_TOKEN=<build token, from Manage > Going live > Advanced>
TUSK_URL=https://tuskcms.com
{ "scripts": { "prebuild": "node tusk-pull.mjs", "build": "next build" } }2a. Template from the JSON
If your site has templates, read .tusk/published.json in them. No network at render time. pages is keyed by page key and each page has content; media is keyed by id. A text field is a string, a textarea is a string with blank lines between paragraphs, a photos field is a list of { photo: id, alt } entries (or bare ids), a file field is a media id or { url, name } for a link kept elsewhere, and a list field is an array of entries.
import { readFile } from 'node:fs/promises'
const site = JSON.parse(await readFile('.tusk/published.json', 'utf8'))
const home = site.pages.home.content
home.headline // 'A small studio.'
home.intro.split(/\n\s*\n/) // paragraphs
const hero = home.hero?.[0] // { photo: 12, alt: '…' } or 12
site.media[hero?.photo ?? hero]?.url // '/tusk/hero.webp'
home.team.map((t) => t.name) // list entries2b. Or rewrite the built HTML
If the site is plain HTML, or you would rather not touch the templates, run tusk-apply.mjs over the folder of built pages after the build. It walks every .html file, works out the page key from the path (index.html is home, about/index.html is about, or data-tusk-page on body), and writes the published values into every data-tusk element: text, textareas as paragraphs, photos into src and alt, downloads into href, and lists by cloning the first item for each entry. When the snapshot carries the scanner’s schema, fields found without a mark are written by their recorded selector too. On the Free plan it also writes the “Edited with Tusk” link (see plans).
{ "scripts": {
"prebuild": "node tusk-pull.mjs",
"build": "eleventy",
"postbuild": "node tusk-apply.mjs dist" } }
# or by hand, from the site folder
node tusk-pull.mjs && node tusk-apply.mjs dist3. Let a publish reach the site
A publish only changes what the next build pulls, so something has to run that build. Under Publishing on the manage page you pick how a publish reaches the site:
- Deploy hook. An https address your host gives you (Vercel, Netlify, git-connected Cloudflare Pages and most others have one), or
vercel://<token>@<team>/<project>for a Vercel project with no git repository, which redeploys its last production build. Tusk calls it after every publish and shows the result on the site’s activity. The change is live as soon as the host finishes rebuilding; how long that takes is your deploy setup, not Tusk. - Feed. No hook. The build pulls the feed the next time it runs, or, with the public feed on (below), the live site fetches the feed itself and updates with no rebuild. This is the mode for hosts that cannot be rebuilt on publish, chiefly Cloudflare Pages direct upload, which has no deploy hook at all.
- Adapter. No hook, no build, no developer step: Tusk itself turns each publish into finished HTML (it applies the published words and photos to each page’s captured template) and uploads it straight to your host. The built-in adapters are Cloudflare Pages and Vercel direct upload: paste an API token scoped to that host, the account/team id and the project name, and every publish is live within a couple of minutes. A failed deploy never blanks the site, because the host keeps the previous deployment serving until a new one uploads whole, and Tusk retries. This is path A below. It uploads only the pages Tusk captured when you scanned and the images those pages reference, so it suits a small, fully-captured site; for a multi-page site with its own CSS, JS and images, use a deploy hook so the whole repository redeploys.
For every other host there is a signed webhook (path B): Tusk POSTs a signed payload to a URL you control, and a small receiver pulls the feed and deploys wherever you like.
4. The public, token-less feed (opt-in)
For a public marketing site you can skip the build token and the rebuild entirely. Turn on Public content feed under Publishing (off by default, per site) and the published snapshot becomes readable with no token at GET /api/public/<slug>/snapshot, images at /api/public/<slug>/asset/<id>. It carries published content only, never drafts, the build token, the deploy hook, or any other site, so it is safe to fetch from the open web. That is what lets a Cloudflare direct-upload site (or any static host) go live on publish with no developer step: fetch the feed at runtime and render it.
<script>
const SLUG = 'north-studio' // the Tusk slug, not a secret
fetch(`https://tuskcms.com/api/public/${SLUG}/snapshot`)
.then((r) => r.json())
.then((site) => {
const get = (ref, fb = '') => {
const [p, id] = ref.includes('.') ? ref.split('.') : ['home', ref]
const v = site.pages?.[p]?.content?.[id]
return v === undefined || v === '' ? fb : v
}
document.querySelectorAll('[data-tusk]').forEach((el) => {
const v = get(el.getAttribute('data-tusk'))
if (typeof v === 'string' && v) el.textContent = v // keep built-in text as the fallback
})
})
.catch(() => {}) // a failed fetch leaves the built page untouched
</script>Do the same in a Cloudflare Worker or Pages Function to inject values server-side: same JSON, same helper. No token ever appears in the site, the repo or the browser. The feed is cached at the edge with s-maxage=60, stale-while-revalidate, so a publish shows within about a minute (usually sooner). The response includes feedVersion (the publish time in epoch ms); to make a known publish appear instantly, re-fetch ?v=<feedVersion>, since a version never seen before is always served fresh, with no purge to wait on.
Path A: the managed Cloudflare Pages adapter
Set the mode to Adapter and Tusk does the whole deploy itself, so a host with no build and no git link still updates on Publish with nothing for anyone to do. On each publish Tusk loads every page’s template (the raw HTML captured when you scanned the site), applies the published values and photos to it with the same applier the SDK uses, resolves the images the pages reference, and uploads the finished files to your Cloudflare Pages project through its direct-upload API. Applying to the captured template, never to the live page, is what keeps a re-publish from duplicating list entries; re-scan a site to refresh its templates after you change the markup.
What you provide, under Publishing: a Cloudflare API token scoped to Account · Cloudflare Pages · Edit and nothing more (least privilege), your account id, and the project name. The token is stored encrypted, shown masked, and decrypted only server-side at deploy time, so it never reaches the browser, a client, a log, or the deployed files. Safety: Tusk builds the entire file set in memory first and only creates the Cloudflare deployment once every asset has uploaded, so a failure leaves the previous deployment serving untouched; Tusk records the failure and retries.
Path B: a signed webhook to any host
For any other host, set a webhook URL under Publishing. On each publish Tusk POSTs a signed JSON payload to it; a small receiver you control verifies the signature, pulls the public feed, and deploys however it likes. Tusk mints a signing secret when you set the URL (shown once; rotate it any time).
The payload names the feed and its version:
POST <your webhook URL>
X-Tusk-Event: publish
X-Tusk-Timestamp: 1757577600000
X-Tusk-Signature: sha256=<hex hmac>
{
"event": "publish",
"site": "North Studio",
"slug": "north-studio",
"publishedAt": "2026-09-11T09:00:00.000Z",
"feedVersion": 1757577600000,
"feedUrl": "https://tuskcms.com/api/public/north-studio/snapshot"
}Verify every request before acting on it. The signature is "sha256=" + HMAC-SHA256(secret, <X-Tusk-Timestamp> + "." + rawBody), computed over the raw body bytes before any parsing, and compared in constant time. Signing the timestamp with the body defeats replay: reject a timestamp outside a few minutes’ tolerance, and a captured request cannot be re-sent. A copy-paste Cloudflare Worker that does the verification, pulls the feed and triggers a deploy is at /sdk/tusk-deploy-worker.js. Because the receiver reads the token-less public feed, no Tusk credential ever lives in your host; the only secret it holds is the signing secret, used to check the request came from Tusk.
The raw build feed
If you would rather fetch it yourself, the feed is one request with the site’s build token as a Bearer token. /snapshot returns the stored copy from the last publish; add ?assets=1 for the list of files to download and ?history=1 for the kept versions. /asset/:id serves one photo by its token, with ?size=thumb|card|hero.
curl -H "Authorization: Bearer $TUSK_TOKEN" \
https://tuskcms.com/api/published/north-studio/snapshot?assets=1
# the live feed, and one photo at hero size
curl -H "Authorization: Bearer $TUSK_TOKEN" https://tuskcms.com/api/published/north-studio
curl -H "Authorization: Bearer $TUSK_TOKEN" -o hero.webp \
"https://tuskcms.com/api/published/north-studio/asset/12?size=hero"The JSON has site, publishedAt, pages keyed by page key (each with content) and media keyed by id. Photo fields hold media ids; list fields hold arrays of entries.
The public feed (/api/public/<slug>/snapshot, no token, only when the site opted in) returns the same shape with two additions, feed: "public" and feedVersion (publish time in epoch ms), and its media URLs already point at the token-less /api/public/<slug>/asset/… route. A security-conscious team can treat this as the contract and write their own apply step against it: read pages[key].content[fieldId] by kind (string; textarea as blank-line-separated paragraphs; photos as { photo, alt } or bare ids resolved through media; file as a media id or { url, name }; list as an array of entries), with the built-in text as the fallback so an unpublished site still renders.
On npm: @tuskcms/mcp, the MCP server described above. Not on npm: @tuskcms/sdk and tusk-connect are written but not published, so npm install cannot find them; the two files above do the same job with no dependencies. GitHub push mode, where Tusk commits .tusk/published.json and new photos to your repository on every publish so an ordinary git deploy does the rest, needs the Tusk CMS GitHub App, which is not installed yet. The Publish to GitHub panel on the manage page stays inactive until it is.
What clients can and cannot do
- Change any text you marked, one line or paragraphs.
- Replace photos, with a crop at the aspect you set.
- Swap a download for a new PDF or document.
- Add, remove and reorder entries in a list, up to your maximum.
- Open History and restore an earlier publish as a draft.
- Save drafts as they go and press Publish when ready.
- Move, add or remove sections, or change the layout.
- Touch styles, classes, scripts or any code.
- Edit navigation, forms or anything you did not mark.
- See your repository, host, deploy hook or build token.
- See any site they were not invited to.
Plans, the Free badge, what happens if you stop paying
Every studio starts on Free: one live site, one client login, no card, no clock. Agency is US$50 a month for five live sites with one client login per site; Operator is US$150 for fifteen with unlimited client logins and white label; Enterprise is US$10 a site a month from sixteen sites. Pay yearly and save 10%.
The badge. On Free, the snapshot carries badge: { text, href } and tusk-apply.mjs (and the SDK’s apply) write one small fixed “Edited with Tusk” link before </body> on every page they rewrite: inline markup, no script, nothing fetched at page-view time. If you template from the JSON yourself (2a above), render that link once per page. It is the studio’s own referral link, so a developer who signs up from it earns you a commission. On a paid plan badge is absent and the next build removes it.
Stopping. Cancel a paid plan and a studio with one site drops back to Free and keeps editing; a studio with more becomes read-only until it chooses a plan or is down to one site, and every site keeps serving what was last published. Nothing is deleted for ninety days, and an export is one click.
Chrome extension
The Tusk extension puts an Edit with Tusk button on any site you manage, from the page you are looking at, and lets you open the editor, manage the site or publish from the toolbar. It recognises your sites by domain using the session you already have at tuskcms.com; no page content is read and nothing runs on other sites.
- Download the extension and unzip it.
- Open
chrome://extensions, turn on Developer mode, choose Load unpacked and pick the unzipped folder. - Open one of your sites. The toolbar icon says EDIT and the button appears bottom-right. Works in Chrome, Edge, Brave and Arc.
Clients can use it too: signed in as a client, it lights up on the site they are allowed to edit and opens their editor.
Refer & earn
Every studio has a referral link on its dashboard (Refer & earn). A developer who signs up on it within ninety days earns you 20% of everything they pay for their first twelve months. Sites you have not white-labelled carry the link on the Tusk mark in the editor, so a client who goes on to build their own sites counts too. The full programme is at /affiliates.
Attribute reference
| Attribute | Where | What it does |
|---|---|---|
data-tusk="page.field" | The editable element | page.field, or a bare field for the page the URL implies (/ is home, /about/ is about). Letters, digits, underscore. Site-wide text uses the page key globals. |
data-tusk-label | Any marked element | The name the client sees in the editor. Without it Tusk humanises the field id. |
data-tusk-kind | Any marked element | text, textarea, link, embed, toggle, photos, file or html. Overrides the guess: photos for img (or an inline background-image), link for an a with an href, embed for iframe/video, file for a link to .pdf/.docx/.xlsx/.zip, textarea for p tags or over 140 characters, text otherwise (a title or meta too). toggle and html are never guessed. Inside a list: text, textarea, link or photo. |
data-tusk-page | body | Names the page and sets its key when the URL is not enough. |
data-tusk-aspect | A photo | The crop the editor suggests, as w:h, for example 16:9 or 1:1. |
data-tusk-list="page.field" | The wrapper of a repeated group | One list field. Anything marked inside it belongs to the list. |
data-tusk-item | Each repeated element | One entry. The client can add, remove and reorder entries. |
data-tusk-field="name" | A part inside an item | A sub-field. The spec comes from the first item, the content from every item. |
data-tusk-max | The list wrapper | The most entries a client may have. |
Reserved field ids: id, key, kind, title, url, order, state, portal, updatedAt, tenant. A complete marked page is at /examples/marked-site.html.