| name | daft-worktree-workflow |
| description | Guides the daft worktree workflow for compartmentalized Git development. Use when working in daft-managed repositories (repos with a .git/ bare directory and branch worktrees as sibling directories), when setting up worktree environment isolation, or when users ask about worktree-based workflows. Covers daft commands, hooks automation via daft.yml, and environment tooling like mise, direnv, nvm, and pyenv. |
| daft_version | 1.27.1 |
daft Worktree Workflow
Core Philosophy
daft treats each Git worktree as a compartmentalized workspace, not just a
branch checked out to disk. Each worktree is a fully isolated environment with
its own:
- Working files and Git index
- Build artifacts (
node_modules/, target/, venv/, .build/)
- IDE state and configuration (
.vscode/, .idea/)
- Environment files (
.envrc, .env)
- Running processes (dev servers, watchers, test runners)
- Installed dependencies (potentially different versions per branch)
Creating a worktree is spinning up a new development environment, not just
"checking out a branch". Split that work along two lifecycles:
- Provision on create.
daft.yml lifecycle hooks do finite, idempotent,
unattended setup — install dependencies, copy env files, configure environment
tools — so the developer can start working immediately.
- Serve on demand. Long-running, attended processes — dev servers,
docker compose stacks, watchers — belong in tasks, started explicitly
with daft run. Booting a backend stack in every worktree you only ever read
wastes resources and invites port collisions.
The same job schema powers both; only the trigger differs (a lifecycle event vs.
an explicit daft run). See Tasks (daft run) below.
Never use git checkout or git switch to change branches in a daft-managed
repo. Navigate between worktree directories instead.
Detecting a daft-Managed Repository
daft supports multiple layouts. The most common is the contained layout:
my-project/
+-- .git/ # Bare repository (shared Git metadata)
+-- main/ # Worktree for the default branch
| +-- src/
| +-- package.json
+-- feature/auth/ # Worktree for a feature branch
| +-- src/
| +-- package.json
Key indicators of any daft-managed repository:
git rev-parse --git-common-dir from any worktree finds the shared Git
directory
daft layout show reports which layout the repo uses
- Contained layout:
.git/ at the project root is a bare repository
(directory, not a file) with branch worktrees as siblings
- Other layouts: the main checkout looks like a normal Git repo, but daft
manages worktrees elsewhere
If you see any of these patterns, the user is using daft. Apply worktree-aware
guidance throughout the session.
Four built-in layouts control where worktrees are placed:
| Layout | Template | Description |
|---|
contained | {{ repo_path }}/{{ branch }} | Worktrees inside the repo directory |
sibling | {{ repo }}.{{ branch | sanitize }} | Worktrees next to the repo (default) |
nested | {{ repo }}/.worktrees/{{ branch | sanitize }} | Worktrees in a hidden subdirectory |
centralized | {{ daft_data_dir }}/worktrees/{{ repo }}/{{ branch | sanitize }} | Worktrees in a central data directory |
Manage layouts with daft layout show / list / transform <layout> /
default [layout]. Layout is selected at clone time by --layout, then the
daft.yml layout: field, then the global default, then the built-in default
(sibling); custom template layouts live in ~/.config/daft/config.toml.
daft.yml can pin the team-recommended layout alongside hooks:
layout: contained
hooks:
worktree-post-create:
jobs:
- name: install-deps
run: npm install
Running daft
Execution rule
Run and recommend daft commands using the short verbs exactly as written in this
file: daft go, daft start, daft clone, daft init, daft carry,
daft exec, daft run, daft merge, daft list, daft update, daft prune,
daft remove, daft rename, daft sync, daft push, daft adopt,
daft eject, plus the noun groups daft hooks ..., daft repo ...,
daft layout ..., daft config ..., daft doctor, and daft skill ....
Invoke the daft binary directly.
Never run or emit the alternate spellings some users have configured:
git worktree-* subcommands, git-worktree-* binaries, long daft worktree-*
names, git daft ..., or shortcut aliases such as gwtco. They depend on
symlinks and shell wrappers that are usually absent from agent shells, and the
daft verbs are the canonical register. This applies to commands you execute and
to commands you write in explanations, docs, scripts, and daft.yml suggestions
alike.
Recognizing user vocabulary
Users may still type those alternate spellings in their own terminals. Translate
what they say into daft verbs; respond and act in daft verbs. It is fine to
acknowledge their form once ("gwtco runs daft go").
| User says or types | Means |
|---|
git worktree-checkout, gwtco, gwco, gcw | daft go |
git worktree-checkout -b, gwtcb, gwcob, gcbw | daft start |
gwtcm, gwtcbm, gwcobd, gcbdw | daft start off the default branch |
git worktree-clone, gwtclone, gclone | daft clone |
git worktree-init, gwtinit | daft init |
git worktree-carry, gwtcarry | daft carry |
git worktree-warm | daft warm |
git worktree-exec | daft exec |
git worktree-merge | daft merge |
git worktree-list, gwtls | daft list |
git worktree-fetch, gwtfetch | daft update |
git worktree-prune, gwtprune, gprune | daft prune |
git worktree-branch -d / -D, gwtbd | daft remove / daft remove -f |
git worktree-branch -m, gwtrn | daft rename |
git worktree-sync, |
The long daft worktree-<name> spellings map the same way. Shortcut aliases are
optional symlinks users manage with daft activate shortcuts; never execute
them yourself.
If a documented command is rejected
If daft rejects a command or flag documented here (unknown subcommand,
unexpected argument), do not fall back to raw git worktree plumbing.
- Re-discover the real surface:
daft --help, then daft <command> --help.
- The installed copy of this skill may be stale relative to the installed
binary. Refresh it with
daft skill install, which writes the
version-matched skill embedded in the daft binary (compare daft --version
with daft_version in this file's frontmatter).
- Proceed with the syntax
--help reports.
Operating across worktrees: -C <path>
Every daft command accepts a top-level -C <path> flag that changes the
effective working directory before any path-dependent state is resolved (repo
discovery, layout, hooks, daft.yml). Semantics match git -C.
daft -C /path/to/repo list
daft -C /path/to/repo go feature/x
This is the recommended pattern for agents working across multiple worktrees:
each command is self-contained ("do X in path Y") with no cd juggling. Rules:
repeated flags compose like git -C (-C /a -C b means /a/b — not "last
wins"); relative arguments resolve against the post--C cwd; -C is parsed
only at the front of the argv, so an inner -C in a daft exec shell command
is preserved.
daft does not change your shell's directory
The daft binary cannot cd the parent shell. After creating a worktree,
navigate to it explicitly — sibling worktrees live at ../<branch>/ relative to
any worktree. (Users with shell integration installed get automatic cd via
DAFT_CD_FILE wrappers; agent shells do not have those wrappers.)
When a user asks why their terminal did not follow a new worktree, point them at
shell integration: eval "$(daft shell-init bash)" in ~/.bashrc or
~/.zshrc, daft shell-init fish | source for fish. Opt out per command with
--no-cd or globally with git config daft.autocd false. Agents recommend
these lines; they never eval them.
daft repo remove invalidates your cwd
Running daft repo remove from inside the repo being deleted invalidates the
agent's cwd mid-operation. Either pass an explicit path
(daft repo remove /path/to/repo) and stay outside, or cd to a safe ancestor
first. The binary writes a redirect path to $DAFT_CD_FILE for shell wrappers,
but agent shells typically lack that wrapper, so follow-up commands fail with
chdir: no such file or directory until the cwd is fixed.
Command Reference
All commands run from any directory inside any worktree; daft finds the project
root via git rev-parse --git-common-dir.
Worktree Lifecycle
| Command | Description |
|---|
daft clone <url> [--layout <LAYOUT>] [--install [--git-exclude]] | Clone a remote repository into worktree layout. --install bootstraps a starter daft.yml after cloning (copied into every worktree of a multi-branch clone, implies --trust-hooks; skipped if the repo ships a tracked daft.yml; rejected with --no-checkout). |
daft init <name> [--layout <LAYOUT>] | Initialize a new local repository in worktree layout |
daft go <branch> | Create/enter a worktree for an existing local or remote branch; --local skips the remote fetch even when daft.checkout.fetch is enabled |
daft go pr:<number> | Check out a GitHub PR or GitLab MR (mr:<number>, or a pasted PR/MR URL) into a worktree on its source branch, configured to pull from the PR head. Fork-aware; resolves via the gh/glab CLI, which must be installed and authenticated (daft doctor reports). The platform is detected from the remote (pr:/mr: are aliases); daft.forge.platform overrides for ambiguous remotes. Works cross-repo from anywhere: daft go <repo> pr:<number> checks the PR out in that cataloged repo. |
daft go - | Switch to the previous worktree (cd - style toggle) |
daft go -s <branch> | Same, but auto-creates the branch if not found (also daft.go.autoStart) |
daft go <commit-ish> | Open the canonical detached sandbox for a point in history — a tag, SHA, HEAD~2, origin/master — when the name is no branch and no repo. Idempotent (revisits land in the same worktree), hooks run, no branch exists. See Anonymous Worktrees below. |
daft start <branch> [base] | Create a new branch and worktree from the current or specified base; does not push by default (daft.checkout.push); --local skips remote even when push is enabled. A leading cataloged-repo name creates the branch in that repo instead — see the Repo Catalog table. From a detached HEAD (inside a sandbox), the new branch bases on that commit — the promotion gesture. |
Management
| Command | Description |
|---|
daft list [--format <FMT>] [-b|-r|-a] [--columns COLS] [--sort COLS] | List worktrees: branch (✦ = default), path, base ahead/behind, file status, remote status, age, owner, commit. In forge repos the default listing also includes a row per open PR (see Machine-Readable Output; --columns -pr for worktrees only). -b/-r/-a include local/remote branches without worktrees. Output contract and JSON fields: see Machine-Readable Output. |
daft exec [TARGETS]... [--all] [-x CMD]... [-- CMD ARGS]... | Run command(s) across worktrees: positional/glob targets or --all; -x repeatable shell pipelines; trailing -- for direct argv. Parallel by default (--sequential/--keep-going for serial); failed worktrees' captured output is dumped after the run, -v dumps successful ones too. On an interactive terminal, runs render as a live plan-then-execute rail. |
daft run [<task>] [<args>...] [--list] [--job <name>] [--tag <tag>] | Run a named task from daft.yml's top-level tasks: section in the current worktree; bare daft run runs the reserved run task, and words after the task name forward to it as arguments (a first word naming no task forwards everything to run). Output streams live, there is no execution timeout, and Ctrl+C cancels (twice force-kills). Executes even in an untrusted repo — explicit invocation counts as consent. See Tasks (daft run). |
daft env [[repo:]VAR[@worktree]] [--export] [--write [PATH]] [--ad-hoc] | Print deterministic per-worktree env values (ports, names) derived from the worktree's name — no allocation, no registry; the same inputs give the same answer on every machine, even for a worktree not created yet. Bare daft env lists the declared set; VAR prints one raw value; --export emits eval-able exports; --write materializes a dotenv file. Declared in daft.yml env:; hooks, tasks, and daft exec receive the values automatically. Unknown names error under a declared schema (--ad-hoc escapes). Daft's own job variables answer too — prints the live value of any of the seven that exist at rest, and the rest (, , , …) error explaining when they exist; serializing them is refused, so // stay the declared set. |
For ad-hoc commands across worktrees use daft exec; for named tasks committed
in daft.yml (dev servers, compose stacks) use daft run; for recurring
lifecycle automation use daft.yml hooks.
Repo Catalog and the Graph
daft keeps a machine-local repo catalog — every repo it touches registers
automatically (clone, init, adopt, or any daft command run inside it). Names
derive from the remote URL; collisions auto-suffix (api, api-2).
| Command | Description |
|---|
daft repo add [<path>] [--name <name>] | Explicitly register a repo (only needed for repos daft never touched) or rename the current entry. Explicit --name collisions error; automatic registration auto-suffixes. |
daft repo list [--all] [--worktrees] | List cataloged repos (name, worktree count, path, remote). --all includes removed entries; --worktrees expands each repo into a tree; --columns +size/+layout/+branch adds columns; --format json adds default branch. |
daft repo info [<repo>] | One entry in full: status, path, remote, default branch, layout, worktrees, resolved relations. Accepts a name, uuid, or path — ., a subdirectory, or any worktree resolves to its enclosing repo; --format json adds identity plumbing. |
daft repo link <target> [--name <label>] [--kind <kind>] | Declare a relation from the current repo to <target> (catalog name, repo path, or remote URL — uncloned URLs allowed): writes a deduped entry to the worktree's daft.yml. Re-linking is a no-op; --name/--kind update in place; self-links are refused. |
daft repo unlink <target> | Remove a relation from the current worktree's daft.yml, matched by label first, then resolved URL. A missing edge is a no-op (exit 0). |
daft go <repo> | Jump to another cataloged repo's default-branch worktree. Local resolution wins: a branch named like a repo shadows it (use --repo). A catalog match beats daft.go.autoStart. Works outside any git repo. |
daft go <repo> <branch> | Open a branch's worktree in another repo (created on demand); daft go --repo <name> [-b <branch> [base]] is the explicit form. After a hop, daft go - returns to the source worktree. |
daft start <repo> <branch> [base] | Create a NEW branch in another cataloged repo, based on its default branch unless [base] is given. Local-first, first match wins: an existing local branch named keeps the local reading; a slot that already resolves to a ref here is read as a base (so creates local branch ); naming your own repo stays local. is the explicit form that always crosses. The destination is announced before any work; the target's trust gates its hooks; runs there, is refused. |
Cross-repo edges are committed in daft.yml under a top-level relations: key
— url: required (matched against the catalog by normalized URL, so the
manifest is portable), name:/kind: optional, edges directed. Manage them
with daft repo link/daft repo unlink rather than hand-editing:
relations:
- url: git@github.com:acme/api-client.git
name: client
kind: consumer
Post-setup command execution (-x/--exec)
daft clone, daft init, daft go, and daft start accept repeatable
-x/--exec commands that run sequentially in the new worktree after hooks
complete, stopping on first failure. Interactive programs work — stdio is fully
inherited.
daft clone https://github.com/org/repo -x 'mise install'
daft start my-feature -x claude
Use -x for finite setup steps. To start a long-running process (a dev server,
a compose stack, a watcher), define a task and run it on demand with daft run
— see Tasks (daft run).
Anonymous Worktrees (sandboxes and forks)
Worktrees decoupled from branches: detached-HEAD checkouts with hooks run and
environment set up, living exactly as long as their directory. Two commands
create them, and choosing between them is a decision rule, not a preference:
- Visit —
daft go <commit-ish> when you need to look at a point in
history (build an old release, inspect a tag, reproduce a PR's "before" state)
and sharing is fine. Idempotent: the first visit materializes the canonical
sandbox for that commit, every later visit — by any spelling — lands in the
same worktree, environment warm.
- Mint —
daft start --fork [<base>] when you need a private worktree to
run work in without colliding with anyone (or anything) else, or when you need
several. Always fresh: run it twice, get two. Never matched by go's
resolution — a fork is reachable only by its printed name.
The fork contract is built for agents: stdout is the created path (one per
line under -n), narration is stderr, so capture is the whole integration:
wt=$(daft start --fork)
wt=$(daft start origin/master --fork)
daft start --fork -n 3 -x './rebuild.sh'
Parallel agents each run their own --fork — names are claimed atomically, so
concurrent invocations never collide and need no coordination. Do NOT share one
worktree between parallel agents, and do NOT fall back to raw
git worktree add --detach: it skips hooks and produces a half-configured
checkout that cannot build.
Aftercare contract:
- These worktrees have no branch and no upstream — never push from one.
- Remove with
daft remove <name> (the printed path's basename) when done.
Wildcards sweep a fleet: daft remove 'main-fork*' (quote the pattern — daft
expands it against sandbox names; it never matches branches, and a pattern
matching nothing errors).
- Commits made inside are safe while the worktree exists, but die with it:
removal refuses when HEAD moved off the pinned commit. Two routes keep the
work. Promote when it deserves a branch:
daft start <new-branch> from
inside the sandbox bases the new branch on the detached HEAD. Merge back
when adopting it into the branch you forked from: a fork's HEAD is a legal
merge source, spelled worktrees/<dirname>/HEAD — so
daft merge worktrees/<dirname>/HEAD --into <branch> adopts one fork, several
sources octopus-merge a fleet at once, and git cherry-pick <sha> adopts
single commits. After either route the commits are reachable elsewhere, so
daft remove <dirname> -f is safe — the -f acknowledges the moved pin,
which removal cannot verify on its own.
- Sandboxes show in
daft list under their directory name with a dim ○;
prune and sync skip them.
Naming: forks follow daft.start.forkNaming — derived (default:
<source>-fork, -fork-2, …) or memorable (brave-otter). Visit sandboxes
name themselves after stable spellings (v1.18.0, origin-master) and after a
commit-hex prefix for positional spellings like HEAD~2.
Merging Across Worktrees (daft merge)
daft merge performs git merge without forcing you to git switch into the
target branch: land a feature into main while staying in your worktree,
octopus-merge several sources, or script merges (-y auto-accepts prompts).
daft merge feature/api --no-edit
daft merge feature/api --into main --no-edit
daft merge feat/a feat/b --into main --no-edit
daft merge --squash feature/api --no-edit
daft merge --rebase feature/api --into main
daft merge --rebase-merge feature/api --into main --no-edit
daft merge -s ours --into release feature/old --no-edit
daft merge feature/done --into main -r --no-edit
daft merge feature/done --into main -r --squash --set-default --no-edit
daft merge feature/hotfix --into release/1.2 --adopt-target --no-edit
daft merge --continue|--abort|--quit [<worktree>]
Pitfalls to communicate to the user:
- Default style is always-merge-commit (never fast-forward, unlike plain
git merge). Use --rebase for linear history. Always pass --no-edit in CI
or non-TTY contexts to avoid an editor prompt.
--squash commits by default, opening an editor pre-populated with the
squash message. --no-edit uses it verbatim, -m supplies your own,
--no-commit stages without committing (incompatible with -r). Without a
TTY and without --no-edit/-m, daft refuses before merging.
- The target must be clean (
daft.merge.requireCleanTarget, default true).
Commit, stash, or daft carry <target> the changes first.
- Conflicts do not hijack the shell: daft reports the conflicted files and
the exact command to finish. Resolve in the target worktree,
git add, then
daft merge --continue [<target>]; bail with daft merge --abort [<target>].
- Squash-staged state: closing the squash editor without saving leaves the
changes staged.
daft merge --continue re-opens the editor;
daft merge --abort resets the index.
- Octopus aborts on conflict — multi-source merges are all-or-nothing.
-r removes both worktree and branch. Regular merges use safe
git branch -d semantics; squash uses force-delete backed by daft's
content-equivalence proof. If the source branch moved during the editor
session, cleanup is refused with a hint.
- Ephemeral targets: when the target branch has no worktree, daft prompts;
--adopt-target accepts, --no-adopt-target refuses, and
daft.merge.adoptTargetOnDemand (prompt/yes/no) sets the default.
pre-merge and post-merge hooks fire around the merge with DAFT_MERGE_* env
vars (see Hook Types below).
Merge Gate Policy
A repo can commit a merge quality boundary in daft.yml — enforced natively by
daft merge, before pre-merge hooks fire and re-verified when the ref moves (so
the tree the hooks tested is the tree that lands):
merge:
ff: only
source_worktree: clean
The gated workflow is rebase-first: rebase the track onto the target, let the
pre-merge rings run, then daft merge (now fast-forward-equivalent). With
pre-merge hooks configured, octopus merges are refused — one track at a time.
Flags mirror the config: --ff-only / --source-worktree clean supply the
policy on unconfigured repos; --no-ff-only / --source-worktree any relax a
committed policy for one invocation (announced). If a merge is refused with
"advanced while the merge gate ran", the track moved mid-gate — re-run the
merge. These refusals are policy, not errors to work around; do not retry with
relax flags unless the user explicitly decides to override team policy.
Hooks System (daft.yml)
Hooks automate worktree lifecycle events, configured in a daft.yml file at the
repository root.
Hook Types
| Hook | Trigger | Runs From |
|---|
post-clone | After daft clone | New default branch worktree |
worktree-pre-create | Before new worktree is added | Source worktree |
worktree-post-create | After new worktree is created | New worktree |
worktree-pre-remove | Before worktree is removed | Worktree being removed |
worktree-post-remove | After worktree is removed | Current worktree |
pre-merge | After pre-flight checks, before the merge runs | Target worktree |
post-merge | After the merge completes (success or conflict) | Target worktree |
worktree-pre-remove/worktree-post-remove also fire when daft merge -r
cleans up a merged source worktree; there DAFT_COMMAND=merge (not
branch-delete), so scripts can tell merge cleanup from a standalone
daft remove.
During daft clone, post-clone fires first (one-time repo bootstrap), then
worktree-post-create (per-worktree setup) — so post-clone can install
foundational tools the per-worktree hooks depend on.
A failing worktree-post-create aborts the creation command by default: the
command exits non-zero and skips its -x/--exec commands, but the new
worktree stays on disk. Fix the cause, then
daft hooks run worktree-post-create from inside that worktree to finish setup
— do not treat the worktree as missing.
git config daft.hooks.worktreePostCreate.failMode warn opts back into
continue-on-failure; a repo can also commit fail_mode: warn (or abort) on a
hook in daft.yml to ship that default to every clone, with the git config
taking precedence over the committed value.
pre-merge aborts the merge on failure; post-merge warns but never rolls
back. Both expose DAFT_MERGE_* env vars: SOURCES, TARGET_BRANCH,
TARGET_PATH, MODE (merge/ff/squash/octopus), STRATEGY,
EPHEMERAL, CROSS_WORKTREE, SOURCE_SHAS (source tips captured before the
merge). post-merge adds RESULT
(success/conflict/already-up-to-date/aborted), COMMIT_SHA,
CONFLICTED_FILES, PROMOTED_FROM_EPHEMERAL. RESULT=aborted fires when a
squash commit is abandoned (editor closed without saving, pre-commit hook fail,
GPG-sign fail) and COMMIT_SHA is then empty. Neither hook fires on a no-op
merge (already up to date).
daft.yml Format
min_version: "1.5.0"
env:
salt: myapp
ports:
- WEBAPP_PORT
- API_PORT: 8
values:
COMPOSE_PROJECT_NAME: "myapp-{worktree_slug}"
hooks:
worktree-post-create:
parallel: true
jobs:
- name: install-deps
run: npm install
- name: setup-env
run: cp .env.example .env
Keep env.ports append-only (inserting mid-list renumbers later offsets).
Top-level env: declares derived values; a job-level env: is a literal map;
skip:/only: env: is a truthiness predicate — nesting depth disambiguates.
Config File Locations (first match wins)
daft.yml, daft.yaml, .daft.yml, .daft.yaml, .config/daft.yml,
.config/daft.yaml. Additionally: daft.local.yml for machine-specific
overrides (not committed) and per-hook files like worktree-post-create.yml.
The deprecated name daft-local.yml still works for one release cycle but warns
(and doctor flags it); prefer daft.local.yml.
Execution Modes
Set one per hook (default is parallel):
| Mode | Field | Behavior |
|---|
| Parallel | parallel: true | All jobs run concurrently |
| Piped | piped: true | Sequential; stop on first failure |
| Follow | follow: true | Sequential; continue on failure |
Job Fields
- name: job-name
description: "Install npm dependencies"
run: "npm install"
runner: "bash"
root: "frontend"
env:
NODE_ENV: development
tags: ["build"]
skip: CI
only: DEPLOY_ENABLED
arch: x86_64
needs: [install-npm]
tracks: [path, branch]
interactive: true
priority: 1
fail_text:
Job Dependencies
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]
Independent jobs run in parallel; dependent jobs wait for their dependencies.
Background Jobs
Jobs with background: true run asynchronously after the command returns, so
the user can start working while long-running tasks complete. A coordinator
process manages them and writes output to log files.
- Background jobs participate in the DAG: a foreground job depending on a
background job promotes it to foreground automatically.
needs: between background jobs is honored — the coordinator schedules them
in topological wave order.
- If a dependency fails or is cancelled, the dependent job is recorded as
Skipped in daft hooks jobs listings.
background: true at the hook level sets the default for all its jobs.
--hooks <auto|foreground|background|off> picks how a run's hook phase
executes, on every command that fires hooks: go, start, clone, adopt,
merge. foreground runs background jobs inline and waits (CI, debugging); a
promoted job's failure then fails the hook, which for worktree-post-create
aborts the command. background detaches the whole phase, but only where that
changes nothing but timing: it declines for a phase daft still acts on (every
pre-* gate, post-clone, and post-merge when the merge used an ephemeral
worktree or --remove-branch) and for a phase declaring an execution order
background jobs cannot preserve (parallel: false, piped:, follow: —
express it with needs: instead). A declined phase runs inline and daft says
so. off is exactly --skip-hooks all, and is the only mode that also
affects legacy .daft/hooks/* scripts. All of it is orthogonal to
--skip-hooks, which picks which jobs run, so the two compose.
DAFT_NO_BACKGROUND_JOBS=1 promotes for commands without the flag. Promoted
jobs keep the standard job timeout.
daft hooks jobs lists, cancels, retries, and prunes records; removing a
worktree cancels its running background jobs.
When generating daft.yml, mark jobs background: true when they warm caches,
pre-build, or do other work whose results are not needed immediately.
Groups
A job can contain a nested group with its own execution mode:
- name: checks
group:
parallel: true
jobs:
- name: lint
run: cargo clippy
- name: format
run: cargo fmt --check
Template Variables
Available in job run/script commands and in job env: values, for lifecycle
hooks and daft run tasks alike:
| Variable | Description |
|---|
{branch} | Target branch name |
{worktree_path} | Path to the target worktree |
{worktree_root} | Project root directory |
{worktree_slug} | Sanitized worktree name ([a-z0-9-]) |
{source_worktree} | Path to the source worktree |
{git_dir} | Path to the .git directory |
{remote} | Remote name (usually origin) |
{job_name} | Name of the current job |
{base_branch} | Base branch (branch-creating commands) |
{repository_url} | Repository URL (post-clone) |
{default_branch} | Default branch name (post-clone) |
{old_worktree_path} | Previous worktree path (move hooks only) |