Desde

Reference

Editor configuration

The two Editor config files, which keys belong in each, what every key defaults to, and why a key in the wrong file does nothing at all.

The Editor reads configuration from two JSON files in your prototype, plus a standard .mcp.json for extra MCP servers. They have different names, different loaders, and no knowledge of each other. Putting a key in the wrong one is the single most common way to spend an afternoon wondering why a setting has no effect: nothing warns you, because each loader only looks for the keys it owns and ignores everything else in the file.

This page is the map. Read the first table before you edit either file.

Which file takes which key#

Both files live in the directory you pass to the CLI (the root of your prototype), with one exception noted below.

Note One exception, and it only bites monorepos. The boot-time blocks resolve against the path you passed the CLI. The per-turn blocks (webFetch, webSearch, figma and .mcp.json) resolve against the git root instead. When your prototype is the repository, those are the same directory and this does not matter. When your prototype is a package inside a larger repository, put those blocks at the repository root, not beside the prototype's package.json.

Key File
version .desde/config.json
project, projectSlug, projectId, platformBaseUrl .desde/config.json
chat (including chat.detachedSessions) .desde/config.json
editor (including editor.canvas) .desde/config.json
conventions .desde/config.json
retention .desde/config.json
readRoots desde.config.json
webFetch, webSearch desde.config.json
figma desde.config.json
designSystems desde.config.json
lanes desde.config.json
hosts desde.config.json
mcpServers .mcp.json

.mcp.json is the standard MCP server file, read fresh on every chat turn. Each entry under mcpServers is an id mapping to { command, args, env }: same shape and same ${VAR} interpolation as the figma block below, which is really just a named shortcut for one of these. The id editor is reserved and refused; a figma id declared in both files is taken from .mcp.json.

Warning A key in the wrong file fails silently. readRoots inside .desde/config.json is an unknown key that the project-config parser ignores; chat inside desde.config.json is never looked for. In both cases the file parses, the Editor boots, and the setting simply does not exist.

Neither file is created for you with content. Both are optional: the Editor runs with neither. .desde/config.json is written by the Editor itself when you create or link a project, and that write preserves every other key you authored.

Note Both files are parsed with strict JSON. Comments and trailing commas are syntax errors. The annotated examples below are valid JSON; the explanations live in the tables beside them.

.desde/config.json#

Project identity, chat behaviour, and on-disk housekeeping. Read once, at boot. Changing it requires restarting the CLI.

{
  "version": 2,
  "project": {
    "id": "b6b1c0d2-4a1e-4a55-9c4a-2b0e8f6b1e33",
    "name": "Checkout redesign",
    "slug": "checkout-redesign"
  },
  "projectId": "b6b1c0d2-4a1e-4a55-9c4a-2b0e8f6b1e33",
  "projectSlug": "checkout-redesign",
  "platformBaseUrl": "https://viewer.internal.example.com",
  "chat": {
    "costCeilingUsd": 40,
    "detachedSessions": true
  },
  "conventions": {
    "useRepoConventions": true,
    "excludeFiles": ["docs/legacy-styleguide.md"]
  },
  "editor": {
    "reloadBackstop": true,
    "canvas": false
  },
  "retention": {
    "backups": { "keepNewest": 200, "maxAgeDays": 14 },
    "chatSessionTurns": { "maxTurns": 500 }
  }
}
Key Type Default Notes
version number none (required) Must be 1 or 2. Any other value is rejected.
project object absent Written by the Editor when you create a project. { id, name, slug }, plus an optional viewerUrl. Hand-editing is not expected.
projectSlug string absent Lowercase letters, digits and internal hyphens only (my-app). Optional since version 2.
projectId string absent The Viewer's project id. Letters, digits, hyphens and underscores.
platformBaseUrl string absent Absolute http: or https: URL of the Viewer you sync with.

The project block is the one exception to the blast-radius rule below: a malformed identity block degrades to "no identity" and leaves the rest of the file working.

chat#

Key Type Default Notes
chat.detachedSessions boolean true Tabbed chat sessions. Opt out by setting false.
chat.costCeilingUsd number or null none (unlimited) Cumulative spend ceiling per chat session. The agent refuses a new turn once the running estimate crosses it. Omitting the key, or setting it to null or 0, all mean the same thing: no ceiling. Set a positive number to turn one on.
chat.maxModelCallsPerTurn number N/A Accepted and validated (must be a positive number), but see the note below.
chat.maxToolCallsPerTurn number N/A Same.

Note maxModelCallsPerTurn and maxToolCallsPerTurn are parsed and carried through to the chat handler, but nothing currently enforces them: only costCeilingUsd reaches the agent runtime. Setting them is harmless and does nothing.

Warning There is no default ceiling. If you omit chat.costCeilingUsd, a chat session can spend without limit. A $20 default existed until 2026-08-14, when it was removed at Mo's request: the number was never shown anywhere in the product (no cost meter, no remaining-budget chip, no setting), so it only ever reached you as a refusal after a long session stopped working. A limit nobody is told about reads as a bug, not a budget. If you want a ceiling, you now have to ask for one by setting this key.

This ceiling covers foreground chat only. The small repair turn that runs when a deterministic edit cannot be applied has its own, much tighter budget that this block does not change.

conventions#

Controls whether the Editor's AI grounds itself in the instruction files your repository already has. Discovery covers five repo-root files (CLAUDE.md, AGENTS.md, .cursorrules, .windsurfrules and .github/copilot-instructions.md) plus .cursor/rules/*.md(c) and your docs/ tree.

Key Type Default Notes
conventions.useRepoConventions boolean true Set false to skip discovery entirely.
conventions.excludeFiles string[] [] Repository-relative paths, forward slashes, to drop from discovery.

editor#

Key Type Default Notes
editor.reloadBackstop boolean true Whether the workspace reloads the preview after each successful edit, as a backstop for a hot-reload miss. Set false to keep panel state and scroll position across edits, at the cost of an occasional manual refresh.
editor.canvas boolean false The canvas and screenshot-plan surface. Off by default. See feature switches.
editor.codeView boolean false The in-app code view reached by "Open in editor". Off by default. See feature switches.
editor.notes boolean false The Notes surface, DOM-anchored annotations alongside comments. Off by default. See feature switches.
editor.vscodeLink boolean false The "Open in VS Code" item in the prototype's right-click menu. Off by default. See feature switches.

retention#

Housekeeping for two things that would otherwise grow without limit: the per-edit backup journal that powers Undo, and the saved chat transcripts.

Key Type Default Notes
retention.backups.keepNewest positive integer 200 Keep at most this many backup directories.
retention.backups.maxAgeDays positive number 14 Delete backups older than this.
retention.chatSessionTurns.maxTurns positive integer 500 Cap on turns kept in a session file. Older turns move to an archive sidecar and are never deleted.

The backups sweep runs at boot and after each Commit.

The blast-radius rule#

This file validates as one unit. The parser returns on the first problem it finds anywhere in the file, and the caller then treats the whole file as unusable, not just the offending block.

So a typo in retention.backups.keepNewest does not merely disable that sweep. It also drops your projectSlug, your projectId, your chat settings, your conventions settings and your editor settings, all at once. The Editor still boots, in degraded mode, having printed the specific validation message as a warning line in the terminal.

Warning If a setting in this file stopped working, do not debug that setting. Read the terminal output from boot: one line names the exact key that failed validation, and that one key is disabling everything else.

A missing file is not an error. It produces one informational line saying the repository has no project association.

desde.config.json#

External read access, web access, Figma, and design systems. This file is at your prototype's root, not inside .desde/.

{
  "readRoots": {
    "production": {
      "path": "../billing-app",
      "description": "The production app this prototype mirrors."
    }
  },
  "webFetch": {
    "allowedHosts": ["docs.example.com", "raw.githubusercontent.com"]
  },
  "webSearch": {
    "enabled": false
  },
  "designSystems": [
    { "kind": "installed", "package": "@acme/ui" },
    { "kind": "npm", "spec": "@acme/icons@^2", "designSystem": "acme" },
    {
      "kind": "repo",
      "url": "https://github.com/acme/design-system",
      "ref": "main",
      "subdir": "packages/ui",
      "allowBuild": true
    }
  ],
  "figma": {
    "enabled": true,
    "mcpServer": {
      "command": "npx",
      "args": ["-y", "figma-developer-mcp", "--stdio"],
      "env": { "FIGMA_API_KEY": "${FIGMA_API_KEY}" }
    }
  },
  "lanes": {
    "detach": true,
    "swap": true
  },
  "hosts": {
    "astro": true,
    "next": false
  }
}

When each block is read#

The blocks in this file are loaded at different times by different code, which is also why they fail differently.

Block Read A problem in it causes
readRoots Boot, plus live when you use the in-editor dialog A config mistake (bad name, missing path, pointing at your own prototype) refuses the boot. A path that no longer resolves (a moved folder, an unplugged drive) is a warning instead: that one root is skipped and the CLI still starts. See readRoots below.
designSystems Boot, in the background An error printed to the terminal; design-system setup is skipped, the session continues.
lanes Boot A warning line in the terminal; the lane stays dormant and the session continues.
hosts Boot, before the dev server starts A warning line in the terminal; the shipped defaults stand and the session continues.
webFetch / webSearch Every chat turn An error message in the chat stream; web tools stay disabled for that turn.
figma Every chat turn An error message in the chat stream; Figma stays unavailable for that turn.

Two consequences worth internalising. First, a JSON syntax error anywhere in this file stops the Editor from starting at all, because the read-roots loader parses the whole file at boot and a broken file is worse to fail silently than loudly. Second, webFetch, webSearch, figma, and readRoots changes made through the settings dialog all take effect on your next chat message: you do not need to restart the CLI. This last one has one exception: if you hand-edit readRoots in the JSON file directly, instead of using the dialog, the CLI does not notice until you restart it. Only writes made through the dialog (or the wizard, at project creation) update the running session live.

readRoots#

Additional folders the chat agent is allowed to read. The typical use is pointing it at the production codebase your prototype imitates, so it can see how a component is really used. A read root does not have to be a git repository. Any folder works: an exported design kit, a docs dump, a directory you unzipped somewhere.

The value is an object whose keys are the names the agent refers to, each mapping to { "path": "...", "description": "..." }.

You do not have to hand-edit this block. Once a project is open, the settings menu (the ⋮ icon) has a Reference folders item that opens a dialog to add or remove them. The launcher's per-project settings page has its own Reference folders tab that does the same thing, without opening the editor.

The New Project wizard used to have an "Add reference folders" step. It was removed 2026-08-31, to keep project creation simple. Reference folders now live only in the two settings surfaces above.

Folders added or removed through either surface apply to your next chat message. You do not need to restart the CLI. Hand-editing the JSON file directly still works, but the CLI only picks that up on the next restart.

Two separate kinds of problem can happen with a declared root, and they are handled differently on purpose. A config mistake (something you typed wrong) stops the whole session from starting, because it is a mistake you need to see and fix. An environment problem (the folder moved, or a drive is unplugged) only skips that one folder. The rest of your session, and every other read root, works normally.

Rule Kind of problem What happens
Name format Config Must match ^[a-z][a-z0-9-]{0,30}$. A bad name fails boot.
Reserved name Config worktree is taken: it is the implicit root for your prototype itself. Using it fails boot.
path Config Required. Absolute, or relative to your prototype root. Symlinks are resolved once at load. A missing path field fails boot.
Cannot be the prototype Config Pointing a root at your own prototype root fails boot; use the implicit worktree root instead.
Must resolve Environment A path that does not resolve (moved folder, unmounted drive) is a warning. That one root is skipped; the session still starts. Before 2026-08-13 this was a fatal error that blocked the whole CLI from opening.
Must be a directory Config A path that resolves to a file, not a directory, fails boot.
Duplicates Environment Two names for the same path are allowed, with a warning.

Whether a root is a git repository is recorded, not required. Each root reports isGit: true or isGit: false, and that flag decides which agent tools work on it:

Tool Git root Plain directory (isGit: false)
Read a file Reads the file at a commit (default HEAD). Reads the file live from disk. The sha you pass is ignored, and the result says so.
Search files git grep, scoped to tracked files at HEAD. A filesystem walk. It skips node_modules, .git, dist, build, and dot-directories, and skips binary files.
List commits Works. Refuses, naming the root. There is no commit history to list.
Diff a file between refs Works. Refuses, naming the root. There are no refs to diff between.

Read roots are read-only. Nothing in this block grants write access.

webFetch and webSearch#

Both web tools are denied by default. The reason is exfiltration: the agent can see your prototype's page content and your read roots, so text injected into a page could otherwise talk it into fetching an attacker's URL with your data in the query string. Enabling web access should be a decision, not an inheritance.

Key Type Default Notes
webFetch.allowedHosts string[] [] Bare hostnames, matched exactly and case-insensitively. No scheme, no wildcards, no paths: an entry containing * or :// is rejected. An empty or absent list means WebFetch is denied outright.
webSearch.enabled boolean false Enables search. Lower risk than arbitrary fetch, but not zero: the query text does leave your machine.

figma#

The Editor does not bundle a Figma integration. You supply any stdio MCP server you trust, and the Editor registers it for the agent so you can ask it to build a screen from a Figma frame.

Key Type Required Notes
figma.enabled boolean Yes, when the figma block exists false disables everything below without deleting it.
figma.mcpServer.command string Yes, when enabled The executable name or path, on its own. Shell metacharacters (semicolon, pipe, ampersand, angle brackets, dollar, backtick, newline) and control characters are rejected. Pass everything else through args.
figma.mcpServer.args string[] No Each entry supports ${VAR} interpolation.
figma.mcpServer.env object No String-to-string. Values support ${VAR} interpolation.
figma.mcpServer.type string No Must be "stdio" if present. It is the only supported transport.
figma.mcpServer.alwaysLoad boolean No, default false Loads the server on every turn. Not recommended: it can add up to five seconds of startup to each message, and you get a warning if you set it.
figma.allowedToolPrefixes string[] No Overrides the read-only allowlist. An empty array is rejected; disable the block instead.

Read-only is enforced at runtime, not by convention. A Figma tool call is denied unless its name starts with one of get_, list_, read_, search_, fetch_ or find_. Override that list with allowedToolPrefixes only if the server you chose names its read tools differently.

Do not put a token in this file. Write "${FIGMA_API_KEY}" and set the variable in your shell. A ${VAR} that is not set in the environment is an error, not an empty string, so a missing token fails loudly. A literal value is accepted but produces a warning, because this file is usually committed.

Warning This file can start a subprocess of your choosing. Treat it with the same care as package.json scripts: the Editor validates its shape but does not sandbox what it spawns. Content inside Figma (layer names, text, comments) is untrusted input, not instructions.

designSystems#

An array of the design systems the Editor should extract component information from. Each entry is flat, and its shape depends on kind.

kind Fields Meaning
installed package (required) A package already in your prototype's node_modules.
npm spec (required) A package to install, e.g. @acme/ds@^2.
repo url (required), ref, subdir A git repository to clone.

Optional on any entry: designSystem, a display label, and allowBuild, a boolean that permits running the repository's own build (relevant to repo entries).

Declaring a system does no work by itself. A background pass after boot resolves declarations into extracted component data, so a fresh clone starts immediately and becomes grounded shortly after. See design-system grounding.

You often need no entries at all: installed component libraries that ship TypeScript declarations are discovered automatically.

lanes#

Turns dormant edit lanes back on. A dormant lane is one the Editor has stopped offering: its controls are absent from the workspace and the edit API refuses that kind of edit. Nothing is deleted: the applicators, and their tests, are intact and unchanged.

Key Type Default What it restores
lanes.detach boolean false Detach component in the Inspector's ⋮ menu and in the Structure tree's right-click menu.
lanes.swap boolean false Swap component in the Inspector's ⋮ menu, the swap dialog, and the icon picker.
{
  "lanes": {
    "detach": true,
    "swap": true
  }
}

Why these two are dormant. Both applicators rewrite Vue single-file-component syntax and neither has a React equivalent, so on a React project the Editor was offering an edit that could never land. On Vue, where they work, they had gone unused. Withdrawing the offering was cheaper than building and maintaining two React applicators for a capability with no users. They are separate keys because they come back at different times: detach needs a React applicator, swap needs that and a better matching model. It currently scores candidates by prop-name overlap alone.

The icon picker is on lanes.swap deliberately. Picking an icon is a swap: it goes through the same applicator with an empty prop mapping, so it carries the same Vue-only limit and the same refusal. There is no key that separates them.

Both ends are gated. With a lane dormant, the controls are not rendered and POST /api/editor/edit and the AI-fallback endpoint refuse that kind with a message naming the key. A stale browser tab cannot reach a dormant lane.

A badly shaped lanes block is a warning, never a boot failure: an unknown lane id, or a value that is not a boolean, prints one line naming the key and is then ignored. The fallback is the dormant default, which is the shipped product, and refusing to start over a typo in an opt-in flag would be the more damaging failure. (A JSON syntax error is a different matter: it stops the boot, because readRoots parses this same file first. See when each block is read.)

hosts#

Which dev servers the Editor may start in its own process. A host that is on is booted for you by desde .; a host that is off falls back to attach mode, where you start the dev server and the Editor connects to it.

Key Default What it means
hosts.vite On Vue 3 or React on a plain vite.config.
hosts.nuxt On Nuxt.
hosts["react-router"] On React Router v7 / v8.
hosts.next On Next.js, App Router on Turbopack.
hosts.astro Off Astro. Boots and stamps correctly; see the caveat below before turning it on.
{
  "hosts": {
    "astro": true,
    "next": false
  }
}

Nuxt, React Router and Next.js became default-on on 2026-08-11. Before that date every one of them required this block to say true, and the published instructions were to use attach mode. If you have {"hosts": {"nuxt": true}} in a config file, it is now redundant and harmless. You can delete it.

Why Astro is off. Not because it fails: its boot gate passes as completely as the other three. Markup written directly in a .astro page has no applicator, so those pages are inspect-only, while .jsx, .tsx and .vue islands on the same page edit normally. That split lives inside a single project and is invisible until an edit is refused. Turning it on gives you a working in-process Astro dev server with that limit.

Setting a host to false is the escape hatch. It is how you take back control of your own dev server for a framework the Editor would otherwise boot, for a container, a custom start script, or a remote machine. The framework then behaves exactly as it did before it was flipped on, including the refusal message naming your dev command and the --attach line to paste.

A badly shaped hosts block warns and is ignored, on the same reasoning as lanes: an unknown host id lists the ids that do exist, and a non-boolean value names the key. Neither stops the boot. Note that hosts.attach is not a valid key: attach mode is reached by passing --attach <url>, never by opting in here.

The old filename#

Repositories written before the tool was renamed may have desde-composer.config.json. That name is still read, indefinitely, and never written. If both files exist the current name wins outright and the legacy one is ignored: no merging.

Warning The fallback covers only webFetch, webSearch and figma. The readRoots and designSystems loaders open desde.config.json by name with no fallback, so a repository still on the old filename gets its web and Figma settings honoured while its read roots and design systems silently do not exist. Rename the file rather than relying on the fallback.

Feature switches#

Seven switches change what appears in the workspace, through a config key or an environment variable. They do not all live in the same file, and they do not all default the same way.

Switch Where File Default Turn it the other way
Tabbed chat sessions chat.detachedSessions .desde/config.json On "chat": { "detachedSessions": false }
Canvas and screenshot plans editor.canvas .desde/config.json Off "editor": { "canvas": true }, or the EDITOR_CANVAS=1 environment variable (either one enables it)
In-app code view ("Open in editor") editor.codeView .desde/config.json Off "editor": { "codeView": true }, or the EDITOR_CODE_VIEW=1 environment variable (either one enables it)
Notes editor.notes .desde/config.json Off "editor": { "notes": true }, or the EDITOR_NOTES=1 environment variable (either one enables it)
"Open in VS Code" editor.vscodeLink .desde/config.json Off "editor": { "vscodeLink": true }, or the EDITOR_VSCODE_LINK=1 environment variable (either one enables it)
Detach component lanes.detach desde.config.json Off "lanes": { "detach": true }
Swap component and the icon picker lanes.swap desde.config.json Off "lanes": { "swap": true }

All seven are read when the workspace page loads, so flipping any of them requires restarting the CLI and reloading the page.

There is an eighth switch, and it works differently from the seven above. EDITOR_REMEMBER_SCOPE_CHOICE gates a checkbox in two of the scope dialogs (iteration scope and style scope): the option to remember your choice for the rest of the session. It is hardcoded to false in the source. There is no config key for it and no environment variable. The only way to turn it on is to edit src/lib/editor-feature-flags.ts and change the constant by hand.

The canvas surface is off deliberately. It is present and intact (the components, the replay machinery, the agent's plan tools) but undertested, and turned off until it gets more investment. Expect rough edges if you enable it.

Code view, Notes and "Open in VS Code" are off for a third reason, separate from canvas and separate from the two lanes switches below. Each is a deliberate product decision, not an unfinished feature. Code view needs visual work before it ships. Notes and "Open in VS Code" were turned off outright, on 2026-08-14 and 2026-08-18.

Turning Notes off is also what makes the right rail's Comments tab honest. Comments and Notes used to render as one merged list, because the two behave identically at the UI layer. That is exactly why the tab ended up named after neither. With Notes off, that list holds only comments, so the tab can say what it holds.

Code view and Notes are gated at both ends. The client stops offering the surface, and the CLI's own API refuses it too while the switch is off (GET /api/editor/file for code view, /api/editor/notes/* for Notes), so a stale browser tab cannot reach either behind a dormant surface. "Open in VS Code" only has one end to gate: it calls no API. It sets the browser's location to a vscode:// URL built from the repo path the CLI already sends the page, so there is nothing left on the server to refuse.

The two lanes switches are off for a different reason: they work, but only on Vue, and they were unused there. lanes above has the detail. There is no environment variable for either.

Environment variables#

Most configuration is file-based. The environment variables below are the ones the Editor actually reads.

Variable Read by Effect
ANTHROPIC_API_KEY Model provider registry When set, model-backed features outside chat use the API with this key. Takes precedence over a key stored through the settings gear, which is never allowed to overwrite it.
EDITOR_USE_CLAUDE_SUBSCRIPTION Model provider registry 1, true, yes or on routes those same features through the bundled claude binary's subscription instead. Required when no API key is set: otherwise they refuse with Missing ANTHROPIC_API_KEY.
EDITOR_CANVAS Workspace bootstrap Exactly 1 enables the canvas surface. Any other value, including true, does nothing.
EDITOR_CODE_VIEW Workspace bootstrap, GET /api/editor/file Exactly 1 enables the in-app code view. Any other value, including true, does nothing. See feature switches.
EDITOR_NOTES Workspace bootstrap, /api/editor/notes/* Exactly 1 enables the Notes surface. Any other value, including true, does nothing. See feature switches.
EDITOR_VSCODE_LINK Workspace bootstrap Exactly 1 enables "Open in VS Code". Any other value, including true, does nothing. See feature switches.
EDITOR_REVIEW_SURFACE Agent review surface bridge, off, 0 or false disables the isolated headless browser the agent uses to check its own work, falling back to driving your live preview. Any other value, and absence, leaves it on.
EDITOR_PROTOTYPE_TSCONFIG Manifest extraction Absolute path to a TypeScript config to use instead of the auto-detected tsconfig.app.json or tsconfig.json.
EDITOR_STORYBOOK_URLS Manifest extraction Comma-separated Storybook base URLs to pull component information from. Each entry may add a display label and an import path: https://sb.example.com|acme|@acme/ui. Loopback and private hosts are rejected.
EDITOR_STORYBOOK_HOST_ALLOWLIST Manifest extraction Comma-separated hostnames exempt from that loopback and private-address rejection.

A key can also be stored from the editor's settings gear, under Anthropic API key. It lives in ~/.config/desde/llm-credentials.json (0600) and is injected into the CLI's environment at boot, so it reaches both the provider registry above and the claude binary chat spawns. An environment variable always wins; when one is set, the settings panel disables its controls rather than offering an action it cannot perform.

Note Chat itself does not use ANTHROPIC_API_KEY. It runs on the bundled claude binary and inherits whatever that binary is already signed in with. The two lanes can disagree: chat working perfectly while another action fails with Missing ANTHROPIC_API_KEY is expected behaviour, not a bug. See the quickstart.

Ports and asset paths are command line flags rather than environment variables. See the CLI reference.