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
logobjectLog configuration (see Log configuration)

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 (relative to worktree root)
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.

Template variables

Commands (run) support template variables that are replaced with values from the execution context:

VariableDescription
{branch}Target branch name (alias for {worktree_branch})
{worktree_path}Path to the target worktree
{worktree_root}Project root directory
{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)

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.