Remote Compute
Per-worktree resource environments: how Daintree provisions, pauses, resumes, tears down and polls external compute as part of the worktree lifecycle, and what agents can and cannot do with it.
Remote Compute
A worktree can carry its own resource environment: an external compute target that Daintree provisions and tears down as a side effect of worktree actions.
It is a lifecycle-hook system, not a hosting product. You describe how to provision, pause, resume, tear down, check the status of and connect to something off-machine (a Docker stack, a cloud VM, an Akash lease, a remote sandbox), and Daintree runs those commands in response to worktree actions. The compute lifecycle is bound to the worktree lifecycle: creating a worktree on a non-local environment spins it up, deleting the worktree tears it down.
This sits above the project-level lifecycle scripts. Those cover local setup: installing dependencies, copying env files. Remote compute covers whatever lives off-machine.
Configuration
Resource configuration lives in .daintree/config.json. Daintree checks three locations in priority order and the first valid file wins: there is no merging between levels. A candidate is skipped when it is missing, unparseable, or fails schema validation, so a typo in the user-level file silently falls through to the repo-level one rather than breaking everything.
~/.daintree/projects/<sanitized-root>/config.json: user-level, outside the repo. For personal resources that shouldn't be committed.<worktreePath>/.daintree/config.json: worktree-level, scoped to one branch.<projectRootPath>/.daintree/config.json: repo-level, the usual home for team-shared environments.
.daintree/ directory into the new worktree, so candidate 2 normally exists as a copy of candidate 3 and shadows it from then on. Editing the repo-level file leaves every worktree that already exists on its stale copy. Edit the worktree's own copy, or delete it so the worktree falls through to the repo again.Use the singular resource key for a single environment:
{
"resource": {
"provision": ["docker compose up -d"],
"teardown": ["docker compose down -v"],
"resume": ["docker compose start"],
"pause": ["docker compose stop"],
"status": "docker compose ps --format json | jq -s '{status: (if length > 0 then \"running\" else \"paused\" end)}'",
"connect": "docker compose exec app bash",
"timeouts": { "provision": 300, "teardown": 300 },
"statusInterval": 60,
"provider": "docker"
}
} Or the plural resources key for several named environments:
{
"resources": {
"docker-local": { "provision": [...], "teardown": [...] },
"akash": { "provision": [...], "teardown": [...] }
}
} Resolution walks: the exact environmentId requested, then resources["default"], then the first entry in the map. A non-empty resources map short-circuits the singular resource key, so if both are present resource is never reached. Teardown resolves default or first-entry regardless of which environment id was asked for.
A second chain sits behind that one: when config.json yields nothing and the worktree's mode is non-local, Daintree falls back to resourceEnvironments in settings.json, at two locations only — user-level and <projectRoot>/.daintree/settings.json. That is what makes environments defined in the settings GUI work without a config file.
Lifecycle Phases
Six phases map to six actions in the action palette and the worktree card menu. The order is provision → (use) → pause ↔ resume → teardown, with status as a side-channel poll and connect as an interactive handoff.
| Phase | Action | Default timeout | Confirmation |
|---|---|---|---|
| provision | worktree.resource.provision | 300s | None |
| teardown | worktree.resource.teardown | 300s | Confirm |
| resume | worktree.resource.resume | 120s | None |
| pause | worktree.resource.pause | 120s | None |
| status | worktree.resource.status | 120s | None |
| connect | worktree.resource.connect | n/a: opens a terminal panel | None |
Two more actions, worktree.resource.config.get and worktree.resource.config.set, never touch config.json. They read and write resourceEnvironments in project settings — the fallback chain above, not the primary one. .set replaces the whole map rather than merging into it, and is classified as safe, so it runs without a confirmation step.
Timeouts are per-phase, set in the timeouts block. On timeout Daintree sends SIGTERM to the process group and escalates to SIGKILL after 5 seconds on Unix, or runs taskkill /F /T on Windows. Results carry a structured exit code and signal name, not just a boolean.
Actions for one worktree are serialized (a queue of concurrency 1, with an abort controller), so a provision and a teardown can't interleave against the same resource. Auto-poll runs are dropped while a manual action is queued.
Provision is idempotent
Provisioning something already ready, running, healthy or up is a no-op that reports Resource is already <status> rather than running the commands again — the word is whichever status the last poll returned, so you will see already ready, already up, already healthy. Provisioning something paused or stopped routes to resume instead. Any other state (unknown, error, never configured) falls through to a real provision.
Pause and resume
A successful resume or pause stamps resumedAt or pausedAt onto the worktree's resource status. Nothing else writes them: a status poll reports the current state but not when it last changed.
Status
The status command must print JSON with at least a status field, and that field must be a string. Two more are read if present: a string endpoint, and an object meta (an array is rejected).
Three things go wrong here, and they don't all land the same way:
- Non-JSON output on a zero exit becomes
unknown, a neutral state rather than a failure. A script that reports a live resource in a shape Daintree can't parse should not be called unhealthy. - A non-zero exit becomes
unhealthy, whatever it printed. - JSON that parses but whose
statusis not a string also becomesunhealthy.{"status": 1}and{"state": "ok"}both land here: valid JSON, red light, nothing in the output explaining why. It is the easiest of the three to write by accident.
"stopped" is deprecated in favor of "paused" and is accepted only as a graceful fallback.
A status script for the Docker stack above, written to satisfy all three rules:
#!/usr/bin/env bash
set -euo pipefail
cd "$DAINTREE_WORKTREE_PATH"
if [ -z "$(docker compose ps -q --status running)" ]; then
echo '{"status": "paused"}'
else
echo '{"status": "ready", "endpoint": "http://localhost:3000"}'
fi Point the config at it with "status": "./scripts/resource-status.sh". Reporting ready with an endpoint is also what triggers the wrapper described below.
Setting statusInterval (in seconds) pins the polling cadence and disables focus tiering. Leave it unset and polling is focus-tiered instead: roughly 30 seconds for the active worktree, 300 seconds in the background, with up to 10% jitter so a project full of environments doesn't poll in lockstep.
Teardown
A failed teardown does not block worktree deletion. If the remote is unreachable or already gone you should still be able to delete the worktree locally and clean up separately.
But a failed cloud teardown is billing-critical, so it raises its own toast ("Cloud resource may still be running") on a dedicated rate-limit bucket, so an unrelated burst of errors can't absorb it into a generic overflow row. Local cleanup failures deliberately do not get this treatment: the directory is about to be removed and there is nothing you would do differently.
Teardown is also the only phase whose full log is persisted — but only on the worktree-deletion path. Deleting a worktree runs the resource teardown and then the project's local teardown commands, and each writes its own scrubbed log to ~/.daintree/projects/<sanitized-root>/teardown-logs/<worktree>/<timestamp>.log, pruned to the ten most recent per worktree, with the path surfaced on the lifecycle status.
A teardown you invoke on its own — from the card menu, the action palette or the assistant — writes no log and surfaces no path. If you want the artifact, delete the worktree; if you only want the resource gone, expect the truncated tail and nothing behind it.
Captured output
All captured output is scrubbed for secrets. Notifications and status details carry the last 8192 bytes: a shared budget across the whole command array, not per command. When output is truncated the snippet is prefixed with a marker naming the byte count and where the rest went: ...(truncated — omitted N bytes; full log: <path>). Where no log was written — which, per above, is every teardown you invoke yourself — the marker ends full log unavailable instead.
The Shell Environment
Lifecycle commands run under a tight allowlist, not your interactive shell. Anything you rely on from .zshrc or .bashrc — aliases, tool paths, secrets — is not there.
What passes through:
PATH(a reduced system path when the parent has none),HOME,LANG,LANGUAGE- Any
LC_*orDAINTREE_*variable from the parent process - On Windows, additionally
SYSTEMROOT,USERPROFILE,TEMP,TMPandPATHEXT - Fixed values:
TERM=dumb,CI=true,NONINTERACTIVE=1,GIT_TERMINAL_PROMPT=0,DEBIAN_FRONTEND=noninteractive
NODE_EXTRA_CA_CERTS are not on the allowlist. On a corporate network behind a proxy or a TLS-inspecting middlebox, a provision command that reaches the network will fail here even though the same command works in your terminal. See Adding your own variables for the fix.On top of the allowlist, Daintree injects:
DAINTREE_WORKTREE_PATH: absolute path to the worktreeDAINTREE_PROJECT_ROOT: absolute path to the project rootDAINTREE_WORKTREE_NAME: short name of the worktreeDAINTREE_BRANCH: branch name, when there is oneDAINTREE_RESOURCE_PROVIDER: the config'sprovidervalueDAINTREE_RESOURCE_ENDPOINT: endpoint from the last status callDAINTREE_RESOURCE_STATUS: raw output of the last status command
Adding your own variables
config.json has no env field, and there is no per-environment place to declare one. What fills the gap is Daintree's ordinary environment-variable settings: the global set and the project set are merged — project wins on a name collision — and passed into every lifecycle command, applied over the allowlist rather than under it. That is where an API token, a registry credential or HTTPS_PROXY belongs.
Set them in Settings › Environment to cover every project, or the project Variables tab for one. Environment variables across scopes covers how the three env-var surfaces differ.
The fixed values and the DAINTREE_* set are applied last, so they win over anything you define under the same name.
Template Variables
Commands support two placeholder syntaxes. Both resolve against the same table and lowercase the name; single-brace additionally matches hyphens. So {{branch}}, {{BRANCH}} and {branch} are the same variable, and {branch-slug} needs the single-brace form.
Single-brace substitution skips shell parameter expansion: ${FOO} is left alone.
| Variable | Value |
|---|---|
branch | Branch name on the worktree |
branch-slug | Branch name sanitized to lowercase [a-z0-9-] |
worktree_path | Absolute path to the worktree directory |
worktree_name | Short name of the worktree |
project_root | Absolute path to the project root |
endpoint | Last known endpoint from the status command |
repo-name | Repository folder name |
base-folder | Base folder of the worktree path |
parent-dir | Parent directory of the worktree |
Every substitution is shell-escaped for you (single-quote wrapping on Unix, double-quote on Windows), except branch-slug, which is left unquoted when its charset really is [a-z0-9-] and escaped otherwise. Unresolved placeholders are left in place so a typo fails loudly instead of expanding to nothing. See Security > Lifecycle Command Injection for the threat model.
Named Environments
Named environments let one project support several targets: a cheap local Docker stack for day-to-day work, a remote lease for longer integration runs. Define them under resources in the config, or manage them in Project Settings > Worktree Setup > Resource Environments.
The settings UI gives you an environment selector, an icon picker (Server, Cloud, Container, CPU, Globe, Rocket, Database, Terminal, Box, Layers), per-environment command lists with numbered rows and up/down reorder buttons, an in-UI variable reference, and a Default Worktree Mode selector that sets what the New Worktree dialog pre-selects.
config.json schema diverge. The UI has icon, which the config file has no field for; the config file has timeouts, statusInterval and provider, which the UI cannot set. To pin a timeout or a poll interval, edit config.json directly.Settings are stored as resourceEnvironments, activeResourceEnvironment and defaultWorktreeMode in either ~/.daintree/projects/<sanitized-root>/settings.json or <projectRoot>/.daintree/settings.json, matching the storage mode you chose for the project.
Provisioning a Worktree
There is no "Remote" mode. The New Worktree dialog renders an Environment radio group of Local plus one button per configured environment key, and the whole control is hidden when the project has none. Choosing anything other than Local provisions that environment after worktree setup completes. The helper text under the group says so.
Provisioning is also reachable without the dialog:
- Provision from the worktree card menu or the action palette, for a worktree created as local.
worktree.createWithRecipe, which takesprovisionResourceandworktreeModearguments.
Once a resource exists, the worktree card carries an environment popover: an icon in the row's status cluster (the environment's own icon when it has one) whose color tracks the resource state. Green for running, healthy, ready or up; amber for starting or provisioning; red for unhealthy, down, error or failed; neutral for paused, stopped, stopping or unknown. While a lifecycle action is in flight the icon pulses and the popover synthesizes the in-flight phase (provisioning, starting, paused, stopping) rather than showing the stale previous status.
Clicking it opens a detail panel with the status label, the endpoint, when it was last checked, the last command output, and a control to re-run the status check.
The daintree-remote wrapper
Once a status call returns ready with an endpoint and the config has a connect command, Daintree writes an executable wrapper to <worktree>/.daintree/daintree-remote (mode 0755). It forwards its arguments to the resolved connect command, which gives agents and scripts a stable invocation path that survives endpoint changes: they call ./.daintree/daintree-remote <command> rather than reconstructing an SSH or exec line.
A connect of:
"connect": "ssh -o StrictHostKeyChecking=accept-new deploy@{endpoint}" turns ./.daintree/daintree-remote npm test into that SSH invocation, and keeps working when the endpoint moves and the next status poll rewrites the file.
.daintree/daintree-remote to your project's .gitignore. The rest of .daintree/ is meant to be committed — recipes especially — but this file is generated per worktree and carries a machine-specific endpoint.What Agents Can Do With This
Remote compute is not an external tool surface. No worktree.resource.* action is exposed to external MCP clients, so a connected client cannot provision, pause, resume or tear down your infrastructure.
Reach is limited to the in-app assistant's tiers:
| Surface | What it can call |
|---|---|
| External MCP clients | Nothing. worktree.createWithRecipe is on the external surface, so a client can ask for a worktree on a named environment, but it cannot drive the resource lifecycle afterwards. |
| Assistant workbench tier | worktree.resource.status, read only. |
| Action tier | Adds provision, pause and resume. |
| System tier | Adds teardown. |
| No tier | connect: it opens an interactive terminal panel, which is a human gesture. |
Failures are surfaced rather than swallowed. A worktree.resource action that fails raises a high-priority error notification carrying the message and a Copy details action, and then rethrows, so a provision that silently did nothing is no longer a possible outcome.
See MCP Server for the tier model and how to configure what a given client is allowed to reach.