Zest

Threat model

On this page

What Zest protects, from whom, and how. Each line names the code that does it, so a reviewer can check the claim.

What is worth protecting

Who might try

ThreatWhat they could tryWhat stops itWhere
Another website open in your browserCall the local server to write files, read pages, spend credit, or press SubmitThe server answers only to localhost host names (stops DNS rebinding); every write needs a token made when the server starts, given only to the editor; a write from another origin is refused; no CORS headers, so other sites cannot read answersserver.mjs (host check, X-Zest-Token, origin check)
A website, through your app's dev server (it rebinds its own name to your app's port, which forwards /__zest to the Zest service)Get an editing session, or read or write a page through the app's proxyA session needs an Origin that is the page's own, on this machine, mapped by a repo you registered (an attacker's page sends its own name); Referer and X-Forwarded-Host must agree; every editor call, read or write, needs the session's token; a session touches one pageservice.mjs (session, sessionFor), server.mjs (pin)
A website, at the Zest service directlyOpen files, mint sessions, read pages it servesHost must be the service's own address; pages it serves sit at an address made from a secret in Zest's config file; only the zest command (which can read that secret) may ask it to serve a file; no CORS headersservice.mjs (host check, /__zest/f/, /__zest/serve)
A freeze framePoint Chrome somewhere else, or run the page's scripts againChrome loads only the page's own address on this machine and port, with scripts off, over a pipe (no port); refresh tags and scripts are removed from the markupfreeze.mjs (checkUrl, clean), shoot.mjs (pipe)
A repository you clonedShip a zest.json whose build, check or extension runs code as youNothing a zest.json names runs until you approve that exact command in that folder, from a terminal or Claude Code; a changed command or extension file needs approving again; approvals live outside the repoconfig.mjs (fingerprints, zest trust)
A page you openIts own scripts act through the editor (they run on the editor's address, or the editor runs in theirs with Edit here)The token is removed from the page after the editor starts; a session touches only its one page; a run it could start edits only that page's folder, cannot reach the web, and stops at your cap. Pages you do not trust: ?zest=off, or zest shooteditor.js, runner.mjs, service.mjs
Text in a page or a note (prompt injection)Tell your agent to do something else during a roundClaude Code enforces the round's permissions, not the prompt: edits only inside the page's folder (and the folders an approved extension's listener names), Bash only for zest, no web fetch or search, no MCP servers unless an approved extension names one; chained commands are checked part by part; spend and turn capsrunner.mjs (--permission-mode dontAsk, allow list, --strict-mcp-config, caps)
A picked image addressMake Zest fetch from your own network (SSRF), hang it, or fill the diskEvery address and every redirect is resolved and refused if private or local; one deadline for the whole answer; a size cap; SVGs with script are refusedimages.mjs (assertPublic, fetchCapped, saveAsset)
A crafted pathRead outside the served folder, or a hidden file such as .envPaths are resolved and must stay inside the root; any path segment starting with a dot is refusedserver.mjs (safePath)
Another local processRead your keysKeys live in your environment, .env beside the page, or Zest's config file at mode 600; the browser only ever learns that a key is set and its last four characterskeys.mjs
A broken saveLose or corrupt your pageEvery write is atomic, keeps the file's mode, goes through symlinks, keeps a version first, and is refused if the tab is stale; non-UTF-8 pages are read-onlystore.mjs, server.mjs
An organisation's rulesSomeone turns on what IT has not approvedA policy file outside any repo turns Run, generation, network, extensions or the release check off for everyone on the machine; a broken policy locks everything it couldegress.mjs
The release checkLearn something about you from the daily request, or push a fake releaseOne GET to GitHub's public releases list with no cookies, keys, file names or page content; Zest only reads a version number from it and never downloads or runs anything; updating stays a command you runupdate.mjs
The buttonA site uses it to open filesIt opens a page only when a repo you registered maps that address, never a hidden path, and only for a browser carrying the cookie you setservice.mjs (/__zest/open)

What Zest does not try to stop

Suggest a change to this page