Skip to main content
A service is anything that listens on a port, such as a web app, an API, or a worker with a health endpoint. Tesserc learns about your services from TOML files in .claude/skills/devboxes/, one per service. The filename is the service name. .claude/skills/tesser/, .agents/skills/devboxes/ and .agents/skills/tesser/ are read too; manifests in two of them is an error.

The smallest manifest

ports lists the ports the app listens on. setup prepares the box and runs before every dev, so it needs to be safe to run repeatedly, and dev starts the server. With this file in place, tesser dev web does everything else. The manifest reference describes every field. The ones most projects use are:
  • root = "apps/web", for a monorepo, so commands run inside the package directory.
  • [health] path = "/healthz", when a port being open is not enough to know the app is ready.
  • [deps], when this service calls another one. See Connect a frontend to a backend.
  • [env], for env files and the names of required secrets, described below.

Starting, restarting, and stopping

tesser dev web creates or reuses the box for this worktree and service, syncs the worktree, runs setup, starts dev, waits until the port answers, and prints the box id. Running it again leaves a running server alone and only syncs, because the server’s hot reload picks up your edits. It starts the server again when the server is not running or its app crashed under a file watcher, which is how you recover from a crash, or when the manifest, the env, or a file setup depends on has changed. Those files are the env files the manifest lists, any package.json, lockfiles, toolchain pins such as .nvmrc, a Dockerfile or compose file, and any file the setup command names. The box runs the manifest it was synced and nothing else; a one-off command is tesser exec. tesser sync <box_id> pushes your edits so the server’s hot reload can pick them up. tesser sync <box_id> --restart pushes the edits and then runs setup and dev again, which you need for changes the running server cannot take on its own, such as env changes, config changes, or a new dependency. tesser dev does not return until the primary port is healthy, and it always exits on its own, so run it in the foreground or wait for it to exit instead of polling. While it waits, it prints the last line of the server’s log every 30 seconds. It exits 1 and prints the end of the log when the run dies before the port listens, when the app crashes under a file watcher such as nodemon, nest --watch, node --watch, or tsx watch (the watcher keeps running but waits for a file change, so the port never opens), or when another process already holds the port. When the health check keeps returning the same 5xx status, the progress line says how long it has done so. When setup prints nothing new for 5 minutes, tesser dev prints a warning with its last line and keeps waiting. Raise [health] timeout in the manifest only when the last line still shows progress, not an error. tesser dev --detach and tesser sync <box_id> --restart --detach return once the run starts instead of waiting for the port. To wait for that run afterwards, run tesser wait <box_id>. It waits the same way tesser dev does, including the services the box is wired to, and exits 0 once the primary port is healthy or exits 1 with the reason and the end of the log. It also picks up a run whose tesser dev timed out. With no argument it waits on the worktree’s one dev box, and with a service name it waits on this worktree’s box for that service. --timeout (for example --timeout 10m) replaces the manifest’s [health] timeout. Use tesser wait rather than a curl loop on the box’s URL, which cannot tell a failed setup from a slow one. tesser logs <box_id> -f follows the server’s output.

Env files

Sync follows .gitignore, so a file like .env.local is not copied to the box. When a dev server fails because a variable is missing, this is usually the reason. There are two ways to handle it. The first is to list the file in the manifest, after which every sync includes it:
The second is to push it once with tesser env push <box_id>, which sends every gitignored .env* file at the worktree root, or the specific files you name. After a push, run tesser sync <box_id> --restart so the server starts with the new values.

Secrets for shared instances

A shared instance is the org’s copy of a service that teammates’ boxes connect to (see Connect a frontend to a backend). A shared instance has no laptop attached, so its env values come from the org instead:
A secret typed on the command line ends up in shell history. Name the key bare to read the value from stdin instead, or load a whole file:
Tesserc stores the values and puts them in the environment of every shell and dev server on the service’s boxes. A workbench made from the same repository gets every value the repository’s services agree on, so a private package install works there too; a key two services set differently is left out. The values never appear in the manifest, which is committed to the repository. Listing a variable in the manifest as env.required = ["DATABASE_URL"] makes a missing value a clear error at start time instead of a crash later. Names that can run code on the box are refused, both by env set and in env.required: PATH, HOME, SHELL, NODE_OPTIONS, NODE_PATH, HTTP_PROXY, HTTPS_PROXY, CC, RUSTFLAGS, GOPROXY, and similar interpreter, compiler, and proxy settings, plus anything starting with LD_, DYLD_, BASH_, GIT_, SSH_, NPM_CONFIG_, PNPM_, YARN_, BUN_, COREPACK_, PYTHON, PERL, PIP_, BUNDLE_, or CARGO_. A few harmless Python settings such as PYTHONUNBUFFERED are allowed.

Several services in one repository

Each service gets its own file. tesser dev api and tesser dev web then create two boxes from the same worktree, and if web.toml lists api as a dependency, tesser wire <web_box> api <api_box> points the web box’s api port at your api box instead of the org’s shared instance (see Connect a frontend to a backend). When a repository has more than one manifest, dev requires the service name; when it has exactly one, that service is the default. make without a service name always creates a workbench. Service names are shared across the whole org, so api refers to the same service in every repository. If a second repository tries to register a name that the first already owns, the registration fails with an error.