Skip to main content

The .daintree Directory

The .daintree/ directory in your repository: shared project identity and settings, team presets, recipes, worktree lifecycle scripts, and the plugin state Daintree writes there at runtime.

Reviewed

The .daintree/ Directory

Daintree supports an optional .daintree/ directory at the root of your repository, in the same spirit as .vscode/ or .devcontainer/. When it is present, Daintree reads it on project open, so anyone who clones the repo picks up the same project identity, settings, recipes and lifecycle scripts without setting them up again.

Two things are worth knowing before you go further. Secrets are never written there: environment variable values live in local storage, and recipe env values are redacted on write. And a lightweight (non-git) workspace never gets a .daintree/ at all: Daintree deliberately skips writing project identity into a folder it was told not to modify.

Enabling In-Repository Settings

Open Project Settings > General (right-click the project name in the sidebar, or use the gear icon in the project toolbar) and scroll to In-Repository Settings. Toggling on Store settings in repository expands a confirmation panel listing the files Daintree will create. Click Confirm and enable.

From then on, Daintree writes .daintree/project.json and .daintree/settings.json and keeps them in sync as you change settings, and syncs existing project recipes into .daintree/recipes/. These are separate from the settings.json Daintree keeps in its own app data directory; the in-repo pair holds only the shareable subset.

Note

Disabling in-repository settings is non-destructive. Daintree stops writing to .daintree/, but the existing files stay in the repo, and a teammate cloning it still gets the identity and settings applied on open.

What lives in .daintree/

The contents split into two groups, and the split matters mostly because of what turns up in git status.

Things you commit

PathContains
.daintree/project.jsonProject identity: a version (always 1), the project id, plus name, emoji and color. The id is the anchor that lets Daintree recognize the project after its folder moves: don't strip it. A file with no valid integer version is rejected outright.
.daintree/settings.jsonThe shareable subset of project settings: run commands, dev server command and load timeout, excluded paths, CopyTree settings, worktree path pattern, and the shareable terminal settings.
.daintree/recipes/Per-project recipes, one JSON file each, synced from local recipe storage while in-repo mode is on. Env var values are redacted; the keys are preserved.
.daintree/presets/<agentId>/*.jsonTeam-shared agent presets, read per agent id. Read-only as far as Daintree is concerned: you write these, it never does. This is the one entry most teams want and most don't know about.
.daintree/config.jsonWorktree lifecycle scripts and resource environments. Also read-only: nothing in Daintree ever writes it.
.daintree/assistant/Content for the Daintree Assistant, including project-scoped custom commands at .daintree/assistant/.claude/commands/<name>.md. There is no .daintree/commands/ directory.

Things Daintree writes at runtime

PathContains
.daintree/plugin-settings/<pluginId>.jsonProject-scoped plugin settings, one file per plugin. Read and written by the plugin host.
.daintree/plugin-storage/<pluginId>.jsonProject-scoped plugin storage. Plugins can also request worktree-scoped storage, which lands at the same path inside the worktree.
.daintree/daintree-remoteA generated executable shell wrapper (mode 0755) that forwards arguments to a Remote Compute environment's connect command, so agents get a stable invocation path that survives endpoint changes. Written per worktree when a resource environment is active.

Whether the second group belongs in version control is a judgment call: plugin settings a team wants to share are worth committing, plugin storage and the generated remote wrapper usually are not.

What's shared vs. machine-local

The shareable/local split is a field-level classification inside settings.json, not a separate file. Every project settings field carries a classification, and only fields marked shareable are ever written to the committed file. These are never written:

  • Environment variables, both plain and secure. Values live locally; the secure map stores key names only, with the values keyed out to OS credential storage. There is no .daintree/env file.
  • Shell program path and other machine-specific terminal fields.
  • Dev server detection state: auto-detected and dismissed flags.
  • Project icon (SVG), the default worktree recipe, and the default worktree mode.
  • Forge remote overrides, branch prefix settings, preferred editor and image viewer.
  • Notification overrides, saved fleet scopes, resource environments, and the project's MCP tier.

Everything marked shareable is written: run commands, excluded paths, dev server command and load timeout, Turbopack flag, CopyTree settings, worktree path pattern, and the shareable terminal settings (shell args, default working directory, scrollback). The full field reference is on Project Settings.

.gitignore guidance

The enable confirmation panel shows a snippet you can copy. Daintree never modifies .gitignore on its own.

project.json, settings.json, recipes/ and presets/ are all safe to commit: committing them is the entire point of turning the feature on, so don't ignore them. The line worth adding is:

.daintree/*.local.json

If your project uses plugins with project-scoped storage, or Remote Compute, consider ignoring the generated pieces too:

.daintree/plugin-storage/
.daintree/daintree-remote

Worktree lifecycle scripts

.daintree/config.json defines shell commands that run automatically when worktrees are created or deleted: npm install, seeding a database, tearing down containers. Lifecycle scripts run for every worktree with no prompt. To launch specific agents or panels conditionally, use Recipes instead.

Create the file by hand. It is independent of the in-repository settings toggle, and Daintree never writes it:

{
  "setup": [
    "npm install",
    "cp .env.example .env"
  ],
  "teardown": [
    "docker compose down"
  ]
}

Setup commands run asynchronously after a new worktree is created, with the worktree root as the working directory. You can switch to the worktree and use it immediately, but tools that depend on setup finishing (npm-installed CLIs, for instance) won't be there until the spinner clears. Commands run in sequence; if one fails, the rest are skipped. Teardown commands run before git worktree remove, and teardown failures are logged but never block deletion.

Script priority order

Daintree checks three locations for config.json:

  1. ~/.daintree/projects/<sanitized-root>/config.json: user-level override, machine-local, not in the repo. <sanitized-root> is the absolute project path with the characters / \ : * ? " < > | replaced by underscores.
  2. <worktree-path>/.daintree/config.json: worktree-level.
  3. <project-root>/.daintree/config.json: the main repo config, shared with the team.

The user-level location is the place for machine-specific steps (activating a local virtualenv, say) without touching the shared repo config.

Resource environments

The same file carries Remote Compute configuration under resource (singular) or resources (a map keyed by environment id). Resolution runs in this order: resources[<environmentId>], then resources["default"], then the first entry in resources, then resource.

Resource environments read a separate, two-candidate chain for settings.json: the user-level file first, then the project root. There is no worktree level for that one.

Config validation

If a config.json is invalid JSON or doesn't match the schema, Daintree logs a warning, skips it, and tries the next location in the chain. The recognized top-level keys are exactly four: setup and teardown (string arrays), plus resource and resources. If all three levels are missing or invalid, no lifecycle scripts run.

When scripts don't run as expected, check the log file (Settings > Troubleshooting > Open Log File) for config parse warnings.

Template variables

Commands go through a substitution pass before they run. This is a separate mechanism from the environment variables below. It rewrites the command string itself:

VariableExpands to
{{branch}}The branch name, when known
{{worktree_path}}Absolute path to the worktree
{{worktree_name}}The worktree name
{{project_root}}Absolute path to the main repository root
{{endpoint}}The active resource environment's endpoint, when there is one
{parent-dir}Parent directory of the repository
{base-folder} / {repo-name}The repository folder name
{branch-slug}Sanitized branch name

Substituted values are shell-escaped, so a branch name with a space or a quote in it can't break out of the command.

Injected environment variables

Daintree sets these on every lifecycle command:

VariableValue
DAINTREE_WORKTREE_PATHAbsolute path to the worktree directory
DAINTREE_PROJECT_ROOTAbsolute path to the main repository root
DAINTREE_WORKTREE_NAMEThe worktree name
DAINTREE_BRANCHThe branch name, only when it is known
DAINTREE_RESOURCE_PROVIDERRemote Compute provider id, when a resource environment is active
DAINTREE_RESOURCE_ENDPOINTThe resource endpoint, when there is one
DAINTREE_RESOURCE_STATUSOutput of the resource's last status command
CItrue
NONINTERACTIVE1
GIT_TERMINAL_PROMPT0
DEBIAN_FRONTENDnoninteractive

Your project's environment variables are injected too. They are merged in first, and the DAINTREE_* set above overrides them on a name collision. Set them in Project Settings > Environment Variables.

The script environment is an allowlist

PlatformVariables that pass through
macOS and LinuxPATH, HOME, LANG, LANGUAGE
WindowsPATH, SYSTEMROOT, USERPROFILE, TEMP, TMP, PATHEXT, LANG, LANGUAGE
EverywhereAnything prefixed LC_ or DAINTREE_, plus your project's env vars and the injected set above

Everything else is gone: proxy variables, NVM_DIR, VOLTA_HOME, PYENV_ROOT, the XDG_* directories, and, deliberately, the loader-injection variables LD_PRELOAD, DYLD_INSERT_LIBRARIES, NODE_OPTIONS and ELECTRON_RUN_AS_NODE. The allowlist is the security boundary: the set of secret variable names is unbounded, so blocking known-bad names would be structurally insufficient. TERM is forced to dumb.

If PATH is somehow absent, Daintree substitutes a fallback, /usr/local/bin:/usr/bin:/bin on POSIX, the system directories on Windows. That fallback is a last resort, not a forced minimal PATH; normally your real PATH is passed through intact.

Shell profile files (.bashrc, .zshrc) are never loaded, because commands spawn non-interactively. Shell aliases and version-manager shims are therefore unavailable. Use absolute paths, or install tools where PATH already finds them.

Timeouts

PhaseBudget
Setup120 seconds, shared across the whole command set
Teardown (normal delete)120 seconds, shared
Teardown (force delete)15 seconds
Resource provision / teardown300 seconds, extendable by a timeouts block in config.json with no enforced ceiling

The budget is shared, not per command, and it is checked before each one: a command that would start after the deadline fails with Timed out before running command N: … rather than being launched and killed. On a real timeout Daintree terminates the process group: SIGTERM then SIGKILL after 5 seconds on macOS and Linux, taskkill /F /T on Windows.

Script output

Setup output is not surfaced in the UI. While commands run, the worktree card shows the active command name; on failure it shows a red Setup failed or Setup timed out label. To capture setup output, redirect it yourself:

npm install >> .daintree-setup.log 2>&1

Teardown is different: its output is persisted automatically, which is the more useful case anyway since the worktree directory is about to disappear. Logs land in:

~/.daintree/projects/<sanitized-root>/teardown-logs/<worktree-name>/

Up to 10 logs are kept per worktree, oldest pruned first. Output is scrubbed for secrets, and the tail shown inline is capped at 8 KB with a marker pointing at the full log on disk.

New worktrees inherit .daintree/

When Daintree creates a worktree, it copies .daintree/ from the main repo into the new worktree so shared config and lifecycle scripts are available immediately. Existing files in the destination are never overwritten: a worktree that already has a .daintree/config.json keeps it as a worktree-level override.

One consequence follows from the priority order: once a worktree exists, later edits to the repo-level config.json won't reach it, because the copied worktree-level file wins. Delete the worktree's .daintree/config.json if you want it to pick up the updated repo-level scripts.