Skip to content

daft.yml YAML reference

Complete reference for the daft.yml schema. For the conceptual framing, see Hooks Overview. For lifecycle-specific behavior (env vars, exit codes), see Lifecycle hooks.

Config file locations

daft searches for configuration files in the following order (first match wins):

FileLocation
daft.ymlRepo root
daft.yamlRepo root
.daft.ymlRepo root (hidden)
.daft.yamlRepo root (hidden)
.config/daft.ymlXDG-style config directory
.config/daft.yamlXDG-style config directory

Additionally:

  • Local overrides (daft-local.yml) — same directory as the main config, not committed to git. Useful for machine-specific settings.
  • Per-hook files (worktree-post-create.yml, post-clone.yml, etc.) — same directory as the main config. Each file defines a single hook and is merged into the main config.

Top-level keys

FieldTypeDescription
min_versionstringMinimum daft version required (e.g., "1.5.0")
colorsboolEnable/disable colored output
no_ttyboolDisable TTY detection
rcstringShell RC file to source before running hooks
outputbool / listfalse to suppress all output, or list of hook names to show output for
extendslistAdditional config files to merge (e.g., ["shared.yml"])
source_dirstringDirectory for script files (default: ".daft")
source_dir_localstringDirectory for local (gitignored) script files (default: ".daft-local")
hooksmapHook definitions, keyed by hook name
tasksmapNamed, user-invoked task definitions (see Tasks)
logobjectLog configuration (see Log configuration)
relationslistRelated repositories (see Relations)

Relations

The Graph pillar's relations manifest lives in daft.yml as a top-level relations: list — directed edges to the repositories this one coordinates with:

yaml
relations:
  - url: git@github.com:acme/api-client.git # required — the resolution key
    name: client # optional friendly label
    kind: consumer # optional, free-form
FieldTypeDescription
urlstringRemote URL of the related repo (required; normalized for matching)
namestringFriendly label used in output (optional)
kindstringFree-form relationship kind, e.g. client, library (optional)

Manage this list with daft repo link and daft repo unlink instead of editing it by hand — they resolve names, paths, or URLs to the portable remote URL and edit only the relations: block. Consumed by daft exec --related, daft start --with-related, and daft repo info. Older daft versions ignore the key.

Tasks

A top-level tasks: map defines named, user-invoked job groups, run with daft run. Tasks are the serve on demand half of the workflow: provisioning stays finite and unattended in worktree-post-create, while starting dev servers, docker compose stacks, and watchers becomes an explicit, attended daft run.

yaml
tasks:
  run: # reserved default — bare `daft run`
    parallel: true
    jobs:
      - name: backend
        run: docker compose up
        env:
          COMPOSE_PROJECT_NAME: "api-{worktree_slug}"
      - name: web
        run: pnpm dev
        root: frontend
  seed-db: # `daft run seed-db`
    jobs:
      - name: seed
        run: ./scripts/seed.sh

A task body is a hook entry: it takes the same parallel/piped/follow, jobs, and skip/only fields, and each job takes the same job entry fields. Keeping tasks in their own section (rather than as custom hook names) keeps hook-name validation strict.

Task-specific rules:

  • Names must start with a letter or digit and contain only letters, digits, ., _, or -. The reserved name run is what bare daft run executes.
  • Arguments forward. Words after the task name are shell-escaped and appended to the task's command (daft run seed-db --reset runs ./scripts/seed.sh --reset); a first word naming no task forwards every word to the reserved run task, and a leading -- forces forwarding past the name match. Forwarding requires the task to resolve to a single foreground job with a single-line command — narrow multi-job tasks with --job.
  • Jobs only — the deprecated commands: form is rejected in tasks.
  • No execution timeout — a task job runs until it exits or is cancelled (lifecycle-hook jobs keep the 300-second default). This makes tasks the right home for long-running processes.
  • Foreground — a task that resolves to a single job passes the terminal straight through: the job inherits daft's stdio and its raw output is the whole interface, exactly as if you ran the command yourself. A multi-job task renders one live row per job with the logs threaded beneath. Either way, Ctrl+C cancels (twice to force-kill); there is no detached mode.
  • Trust — an explicit daft run runs even in an untrusted repo (it counts as consent), unlike lifecycle hooks which are skipped until the repo is trusted.

The daft-local.yml overlay layers machine-local tasks on top of the committed daft.yml, merged by name exactly like hooks.

Hook entries

Each hook is defined under the hooks key:

yaml
hooks:
  worktree-post-create:
    parallel: true
    jobs:
      - name: install
        run: npm install
      - name: build
        run: npm run build
FieldTypeDefaultDescription
parallelbooltrueRun jobs in parallel
pipedboolRun jobs sequentially, stop on first failure
followboolRun jobs sequentially, continue on failure
backgroundboolDefault background execution for all jobs in this hook
exclude_tagslistTags to exclude at hook level
excludelistGlob patterns to exclude
skipbool / string / listSkip condition (see Skip and only conditions)
onlybool / string / listOnly condition (see Skip and only conditions)
jobslistJobs to execute

Only one of parallel, piped, or follow can be set at a time.

Job entries

Each job in the jobs list supports:

FieldTypeDescription
namestringJob name (used for display, merging, and dependency references)
descriptionstringHuman-readable description (shown in dry-run and completions)
runstringInline shell command to execute
scriptstringScript file to run (relative to source_dir)
runnerstringInterpreter for script files (e.g., "bash", "python")
argsstringArguments to pass to the script
rootstringWorking directory / cwd, relative to worktree root (see Working directory)
tagslistTags for filtering with exclude_tags
skipbool / string / listSkip condition
onlybool / string / listOnly condition
osstring / listTarget OS (macos, linux, windows); skips if no match
archstring / listTarget architecture (x86_64, aarch64); skips if no match
envmapExtra environment variables
fail_textstringCustom failure message
interactiveboolJob needs TTY/stdin (forces sequential execution)
priorityintExecution ordering (lower runs first)
needslistNames of jobs that must complete before this job runs
trackslistWorktree attributes this job depends on: path, branch
groupobjectNested group of jobs (see Groups)
backgroundboolRun this job in the background (see Background jobs)
background_outputlog / silentOutput behavior for background jobs (default: log)
logobjectLog configuration (retention, max_log_size) for this job

A job must have exactly one of run, script, or group.

Working directory (root)

By default each job runs in the worktree root. Set root to run the job in a subdirectory instead — useful in a monorepo where a job targets a single package. The path is relative to the worktree root and sets the job's working directory (cwd).

yaml
hooks:
  worktree-post-create:
    jobs:
      - name: install-web
        run: pnpm install
        root: apps/web

Template variables

Job run/script commands and job env: values support template variables that are replaced with values from the execution context (this applies to both lifecycle hooks and daft run tasks):

VariableDescription
{branch}Target branch name (alias for {worktree_branch})
{worktree_path}Path to the target worktree
{worktree_root}Project root directory
{worktree_slug}Sanitized worktree name — [a-z0-9-], capped at 63
{worktree_branch}Target branch name
{source_worktree}Path to the source worktree (where command was invoked)
{git_dir}Path to the .git directory
{remote}Remote name (usually "origin")
{job_name}Name of the current job
{base_branch}Base branch name (for checkout -b commands)
{repository_url}Repository URL (for post-clone)
{default_branch}Default branch name (for post-clone)

{worktree_slug} is the worktree's path relative to the project root (falling back to the directory name), lowercased with every run of non-[a-z0-9] characters collapsed to a single - and the result capped at the 63-character DNS-label limit. Because it is keyed off the worktree rather than the branch, it is unique per worktree and stable even when the worktree is not on a branch — use it to keep per-worktree names collision-free, e.g. COMPOSE_PROJECT_NAME: "api-{worktree_slug}".

Move hooks only (available when DAFT_IS_MOVE is true):

VariableDescription
{old_worktree_path}Previous worktree path (before the move)
{old_branch}Previous branch name (before the move, rename only)

Skip and only conditions

skip and only control whether a hook or job runs. They can be set at either the hook level or the job level.

  • skip: If any condition matches, the hook/job is skipped
  • only: All conditions must match for the hook/job to run

Boolean — always skip or always run:

yaml
skip: true # Always skip this job
only: false # Never run this job

Environment variable — skip/run based on an env var being set and truthy:

yaml
skip: CI # Skip when $CI is set
only: DEPLOY_ENABLED # Only run when $DEPLOY_ENABLED is set

An env var is "truthy" if it is set, non-empty, not "0", and not "false".

Structured rules — a list of conditions:

yaml
skip:
  - merge # Named: skip during merge
  - rebase # Named: skip during rebase
  - ref: "release/*" # Ref: skip if branch matches glob
  - env: SKIP_HOOKS # Env: skip if env var is truthy
  - run: "test -f .skip-hooks" # Run: skip if command exits 0

Named conditions:

NameTriggers when
mergeGit is in a merge state (MERGE_HEAD exists)
rebaseGit is in a rebase state (rebase-merge or rebase-apply exists)

Structured condition fields:

FieldDescription
refGlob pattern matched against the current branch name
envEnvironment variable name; truthy = condition met
runShell command; exit code 0 = condition met
descHuman-readable reason shown when the condition triggers a skip

Groups

A job can contain a nested group of sub-jobs instead of a run or script. The group runs as a unit with its own execution mode.

yaml
hooks:
  worktree-post-create:
    piped: true
    jobs:
      - name: checks
        group:
          parallel: true
          jobs:
            - name: lint
              run: cargo clippy
            - name: format
              run: cargo fmt --check
      - name: build
        run: cargo build
Group fieldTypeDescription
parallelboolRun group jobs in parallel
pipedboolRun group jobs sequentially, stop on first failure
jobslistJobs within the group

Background jobs

Jobs marked background: true run in the background after the command returns.

The background_output field controls notification behavior:

ValueLog fileTerminal notification on failure
logAlways writtenYes
silentWritten only on failureNo

Default is log. Set DAFT_NO_BACKGROUND_JOBS=1 to promote all background jobs to foreground.

Log configuration

The log field at the top level sets defaults for background-job log storage and cleanup. Individual jobs can override retention and max_log_size.

yaml
# Top-level default
log:
  retention: 14d # how long to keep logs
  max_log_size: 10MB # per-log file cap
  max_total_size: 500MB # per-repo total budget (repo-only)
  keep_last: 3 # sanity floor — keep at least this many invocations per worktree
  stale_running_after: 24h # how long before a stuck Running job is treated as cancelled

hooks:
  worktree-post-create:
    jobs:
      - name: build
        run: cargo build
        background: true
        log:
          retention: 1d # per-job override
          max_log_size: 50MB # per-job override
FieldTypeDefaultScopeDescription
retentionstring7dper-jobHow long to keep logs (e.g., 7d, 24h, 30m).
max_log_sizestring10MBper-jobTruncate output.log to this size with a footer marker.
max_total_sizestring500MBrepo-onlyTotal disk budget for all logs under this repo. LRU eviction when exceeded.
keep_lastint3repo-onlyAlways retain at least this many invocations per worktree, regardless of retention or budget.
stale_running_afterstring24hrepo-onlyA Running job older than this with no live coordinator socket is treated as cancelled by cleanup.

retention and max_log_size are resolved at hook-fire time and captured into the job's meta.json. Cleanup reads these directly — editing daft.yml after a hook fires will not retroactively change retention for already-completed jobs.

max_total_size, keep_last, and stale_running_after are persisted to <state>/jobs/<repo-uuid>/repo-policy.json on every hook fire (most-recent- write wins). Cleanup reads this file at run time; if it's missing (orphaned state dir whose repo no longer fires hooks), built-in defaults apply.

Config merging

When multiple config sources exist, they are merged in this order (lowest to highest precedence):

  1. Main config (daft.yml)
  2. Extends files (listed in extends)
  3. Per-hook files (worktree-post-create.yml, etc.)
  4. Local override (daft-local.yml)

Merging rules:

  • Scalar fields (e.g., min_version, colors): higher-precedence value wins
  • Named jobs: jobs with the same name are replaced by the higher-precedence version
  • Unnamed jobs: appended from the overlay

Use git daft hooks dump to inspect the fully merged configuration:

bash
git daft hooks dump

Examples

Minimal quick-start

yaml
hooks:
  worktree-post-create:
    jobs:
      - name: install-deps
        run: npm install
      - name: setup-env
        run: cp .env.example .env

Platform constraint with skip condition

yaml
- name: install-brew
  description: Install Homebrew package manager
  os: macos
  run:
    /bin/bash -c "$(curl -fsSL
    https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
  skip:
    - run: "command -v brew"
      desc: Brew is already installed

Inline command, script with runner, and env vars

yaml
- name: lint
  run: cargo clippy -- -D warnings

- name: setup
  script: setup.sh
  runner: bash
  args: --verbose

- name: test
  run: npm test
  env:
    NODE_ENV: test
    CI: "true"
  fail_text: "Tests failed! Fix before continuing."

Job dependencies

yaml
hooks:
  worktree-post-create:
    jobs:
      - name: install-npm
        run: npm install
      - name: install-pip
        run: pip install -r requirements.txt
      - name: build
        run: npm run build
        needs: [install-npm]
      - name: deploy
        run: ./deploy.sh
        needs: [build, install-pip]

Background jobs with hook-level default

yaml
hooks:
  worktree-post-create:
    background: true
    jobs:
      - name: install deps
        run: pnpm install
        background: false # override: run in foreground
      - name: warm build cache
        run: cargo build # inherits background: true
      - name: precompile assets
        run: pnpm build:assets # inherits background: true

Move-tracked jobs

yaml
hooks:
  worktree-post-create:
    jobs:
      - name: link-build-output
        description: Symlink build artifacts to a shared directory
        run: ln -sf {worktree_path}/dist /opt/project/builds/current
        tracks: [path]

      - name: set-branch-env
        description: Write branch name to local env file
        run: echo "CURRENT_BRANCH={branch}" > .env.branch
        tracks: [branch]

      - name: install-deps
        description: Install project dependencies
        run: npm install
        # Not tracked -- only runs on initial worktree creation

  worktree-pre-remove:
    jobs:
      - name: unlink-build-output
        run: rm -f /opt/project/builds/current
        tracks: [path]

      - name: clear-branch-env
        run: rm -f .env.branch
        tracks: [branch]

Running these in CI

The same daft.yml runs locally and in CI — that's the parity story. See Recipes → CI parity for invoking daft hooks run worktree-post-create from GitHub Actions, GitLab CI, or a generic shell-based runner, plus how to skip local-only steps in CI via skip: { env: { CI: "true" } }.

Released under MIT or Apache-2.0.