Appearance
Configuration
daft's settings live in several places — Git's config system, the repository's daft.yml, and a global config file — resolved through different precedence chains. daft config hides that split: one place lists everything, says what each setting currently is, and says which layer decided it.
The settings browser
bash
daft configOpens a full-screen browser over every setting daft has, grouped by what it does rather than where it is stored. The panel below the list shows the selected setting's whole chain — every layer's value, with a dot on the one that won — so "why is this on?" has an answer that does not require knowing which file to look in.
enter edits, space flips a boolean, u clears a value, and s chooses whether writes go to this repository or to every repository. / filters across names, keys, and descriptions.
Setting values from the command line
bash
daft config list # everything, with values and origins
daft config list --modified # only what something actually sets
daft config get daft.autocd # one value, for scripts
daft config get daft.autocd --origin # the value plus its full chain
daft config set daft.autocd false # change it here
daft config set --global daft.autocd false
daft config unset daft.autocd # reveal whatever it was maskingget prints the value and nothing else, exiting non-zero when there is none — the same contract git config --get has, so it drops into scripts.
Values are checked before anything is written, so a bad enum or an unknown column is refused where you typed it rather than at the next command that reads it. Keys are matched the way Git matches them, which means the section and the trailing name are case-insensitive but the part between them is not — daft.checkoutbranch.carry is a different key from daft.checkoutBranch.carry and does nothing. daft config reports keys like that rather than crediting them.
Git's own commands still work for the Git-backed settings:
bash
git config daft.autocd false
git config --global daft.autocd falseChoosing a layer
--local and --global take Git's spelling, and every verb accepts both. The flag names one layer, and the same layer whether you are reading or writing — get --local prints exactly what set --local would replace:
bash
daft config get daft.remote --global # what the shared layer says
daft config get daft.remote --local # what this worktree's own layer says
daft config set --local daft.remote up # the default; spelling it changes nothing
daft config list --global # the contents of the shared layerThat matters most for the settings Git does not store. For a daft.yml setting, --local is the daft.local.yml overlay and --global is the repository's committed daft.yml; for the worktree layout, --local is the repository's own entry and --global is the default in the global config. Every write says which store it landed in, so "global" never quietly means "the team's file".
A read narrowed to a layer exits 1 when that layer says nothing, which is how a script asks whether something is set here rather than only what it resolves to. It reports what the layer stores, which is not always what daft reads: a value outranked from above still prints, and list --global marks such a row outranked by local. --origin remains the way to see every layer at once, and so cannot be combined with a layer flag.
Two layers Git has are readable but never writable — system needs privileges daft should not ask for, and worktree needs an extension most repositories have off. They appear in get --origin and have no flag.
Machine-readable output
Every verb takes --format json|yaml|toon|markdown (and list, being rows, also takes ndjson|tsv|csv) plus --template:
bash
daft config list --format json # every setting as rows
daft config get daft.remote --format json # one setting, with its whole chain
daft config set remote-sync on --format jsonTwo properties hold so a consumer never has to know which flags were passed:
- A read always carries the ladder.
--originchanges how much a person is shown and nothing else, soget --format jsonincludes every layer, the diagnostics, and which scopes accept a write — without asking for it. --formatnever changes the exit code. A narrowed read that finds nothing still exits 1 and emits its document, so the status and thevaluefield always agree.
A write's record names every key it touched, which is the only place the individual writes behind a behavior are reported — including, when a sequence fails part-way, which ones landed and the state it left the behavior in.
Behaviors
Some settings only make sense together. Turning remote sync on means three booleans; making daft merge squash and clean up means three enums, two of which daft refuses to let you set into a contradictory pair. A behavior is a name for such a group and for the states it can be in, so you can say what you want once instead of getting three keys right in the right order.
bash
daft config get remote-sync # on, off, or custom
daft config set remote-sync on # writes all three
daft config unset remote-sync # clears all three at this scopeBehaviors appear at the top of daft config list and of the browser, above the categories. In the browser, space steps to the next state and → drills into the member settings, with the header naming the behavior you are inside.
enter opens a preset selector rather than a value editor: the states are listed by name, the highlighted one is explained in full, and under it every member appears with what it reads now, where that came from, and what the state would write. A row whose write a higher scope would outrank says so before you commit to it. unset sits below the states, since it is not a state — it is how you stop having one.
Three things worth knowing:
- A behavior is a view, not a store. It has no key and no value of its own; its state is worked out from what its settings currently resolve to. Setting one of them directly is always allowed — the behavior then reads
customand names which one is out of step. - It reads what daft actually does. The state comes from the effective values across every scope, not from one of them. A behavior can read
onfrom inside a repository that sets nothing, because the values are global. - A write goes to one scope; the state may not follow.
set --globalwhen a local value disagrees is written, but the local value still wins — so the command says where the behavior actually landed, not just what it wrote.
custom is a state you can read, never one you can ask for. To leave it, pick a real state or change the individual settings until they agree.
A behavior can also be read at one layer, because its write surface is all-or-nothing — set remote-sync on --local writes every member there, and unset remote-sync --local clears every one:
bash
daft config get remote-sync --local # the state this layer's own values nameA layer that sets only some members names no state, and the command exits 1 rather than reporting the nearest one. That refusal is the point: guessing from a partial layer is how the retired remote-sync --status came to report "Local only" while daft was fetching, having read one scope and inferred the members it could not see. get remote-sync --origin shows each member with its own chain, which is the answer to what a partial layer actually holds.
General Settings
| Key | Default | Description |
|---|---|---|
daft.autocd | true | CD into new worktrees when using shell wrappers |
daft.remote | "origin" | Default remote name for all operations |
daft.updateCheck | true | Show notifications when a new daft version is available |
daft.go.autoStart | false | Auto-create worktree when branch not found in daft go |
daft.start.forkNaming | derived | How daft start --fork names its sandboxes: derived (<source>-fork, -fork-2, …) or memorable (adjective-noun handles) |
Layout Settings
Layout configuration uses ~/.config/daft/config.toml (TOML format), not git config. This is different from the other settings on this page.
See the Layouts guide for detailed explanations of each layout.
| Key | File | Description |
|---|---|---|
defaults.layout | config.toml | Global default layout name or template |
layout | daft.yml (in repo) | Team-recommended layout for this repository |
[layouts.<name>] | config.toml | Custom layout definition |
Global Default
toml
# ~/.config/daft/config.toml
[defaults]
layout = "contained"Or use the command: daft layout default contained
Custom Layouts
toml
# ~/.config/daft/config.toml
[layouts.my-team]
template = "../.worktrees/{{ repo }}/{{ branch | sanitize }}"
[layouts.isolated]
template = "~/worktrees/{{ repo }}/{{ branch | sanitize }}"Team Convention (daft.yml)
yaml
# daft.yml (committed to repository)
layout: containedRemote Sync Settings
By default, daft does not contact the remote during worktree management. All remote operations are opt-in. The three keys below travel together as the remote-sync behavior, so you can move all of them with one command instead of remembering which three.
| Key | Default | Description |
|---|---|---|
daft.checkout.fetch | false | Fetch from remote before creating a worktree for an existing branch |
daft.checkout.push | false | Push new branches to remote after creation |
daft.pushVerify | auto | Base setting: when daft's suppressible pushes (remote-branch deletes, the upstream push) run the pre-push hook — see Git Hooks |
daft.checkout.pushVerify | daft.pushVerify | Checkout-scoped override of daft.pushVerify for the automatic upstream push (auto, always, never) |
daft.branchDelete.remote | false | Delete the remote branch when removing a local branch |
Enabling Remote Sync
remote-sync moves all three at once:
bash
daft config set remote-sync on # Full sync: fetch, push, delete
daft config set remote-sync off # Local only, daft's default
daft config get remote-sync # on, off, or custom
daft config get remote-sync --origin # each setting, and where its value came fromOr set them one at a time, from the browser or the command line:
bash
daft config set daft.checkout.fetch trueSetting a member on its own is always allowed; the behavior then reads custom and names which setting is out of step. daft config unset remote-sync clears all three at the chosen scope, and says what that revealed — clearing local values can uncover global ones.
Per-command overrides are available via --local (skip remote operations) and --remote (delete remote branch only, without removing local worktree or branch).
Checkout Settings
| Key | Default | Description |
|---|---|---|
daft.checkout.upstream | true | Set upstream tracking for branches |
daft.checkout.carry | false | Carry uncommitted changes when checking out existing branches |
daft.checkoutBranch.carry | true | Carry uncommitted changes when creating new branches |
daft.copy.enabled | true | Copy copy:-declared caches into newly created worktrees (details) |
Forge Settings
For checking out pull/merge requests (daft go pr:123, mr:45, or a PR/MR URL). daft shells out to gh/glab, which supply the authentication — daft stores no tokens. These are the only knobs daft itself owns.
| Key | Default | Description |
|---|---|---|
daft.forge.platform | Force the platform for an ambiguous remote: github or gitlab. Unset means detect by remote. | |
daft.forge.githubCli | gh | Override the GitHub CLI binary (for Enterprise wrappers) |
daft.forge.gitlabCli | glab | Override the GitLab CLI binary |
daft.forge.hostname | Forge hostname for self-hosted / Enterprise instances (passed to the CLI as --hostname) |
Run daft doctor to check whether gh/glab are installed and authenticated.
Update Settings
| Key | Default | Description |
|---|---|---|
daft.update.args | "--ff-only" | Default arguments passed to git pull in update operations (same-branch mode) |
List Settings
| Key | Default | Description |
|---|---|---|
daft.list.stat | "summary" | Default statistics mode for list command (summary or lines) |
daft.list.columns | Default column selection for list command (e.g., branch,path,age or +size,-annotation) | |
daft.list.sort | Default sort order for list command (e.g., +name, -activity, +owner,-size). Sortable: name, path, size, age, owner, activity | |
daft.list.sizeConcurrency | Max concurrent directory-size walks for --columns +size on both daft list and daft repo list. Default: CPU count. Lower on slow/network filesystems. The DAFT_SIZE_WALK_JOBS env var overrides this. |
Prune Settings
| Key | Default | Description |
|---|---|---|
daft.prune.cdTarget | "root" | Where to cd after pruning the current worktree. Values: root (project root) or default-branch (default branch worktree) |
daft.prune.stat | "summary" | Default statistics mode for prune command (summary or lines) |
daft.prune.columns | Default column selection for prune command | |
daft.prune.sort | Default sort order for prune command (e.g., +name, -activity) |
Sync Settings
| Key | Default | Description |
|---|---|---|
daft.sync.stat | "summary" | Default statistics mode for sync command (summary or lines) |
daft.sync.columns | Default column selection for sync command | |
daft.sync.sort | Default sort order for sync command (e.g., +name, -activity) | |
daft.sync.pushTimeout | "30m" | Wall-clock budget per push unit (git + pre-push hook). A hung hook is torn down and the push fails with a hint; off (or 0) disables |
daft.sync.pushHookStrategy | "per-branch" | Pre-push hook cadence for sync --push: per-branch runs the hook once per branch; batched pushes every branch in one git push so the hook fires once with all refs (one refusal fails the whole batch) |
Governor Settings
daft sync --push over many branches runs the repo's pre-push hook once per branch, concurrently. Heavy hooks (test suites, builds) are internally parallel, so the aggregate footprint multiplies — enough to exhaust machine memory. The resource governor keeps that fan-out inside the machine's memory budget: it caps concurrent hook-bearing pushes, admits new ones only while memory headroom allows, learns each hook's peak memory and duration across runs (so light hooks get full parallelism and heavy ones start capped), and under sustained pressure freezes — then kills and retries — the newest push rather than letting the machine swap.
The same governor also covers parallel hook and task job phases (a worktree-post-create fan-out, a pre-merge gate's parallel rings): admission caps the fan-out to the memory budget and the jobserver bounds each job's internal build parallelism. Foreground jobs the user is waiting on are subject to admission but are never frozen or killed. The governor only exists while a governed phase runs — a hook-less push or a single-job phase pays nothing.
Additionally, gated merges (repos with pre-merge hooks) serialize through a per-repository cross-process lane: a second daft merge started while one is mid-gate waits — announcing who holds the lane — instead of racing it.
| Key | Default | Description |
|---|---|---|
daft.governor.mode | "auto" | The governor (auto) or no governing at all (off). sync --no-throttle disables it for one run |
daft.governor.jobs | "auto" | Cap on concurrent hook-bearing pushes: auto = max(2, cores/4), or a number. sync --jobs N overrides per run |
daft.governor.memoryReserve | "auto" | Memory the governor keeps free: auto = max(10% RAM, 2G), a size (2G, 512M), or a percent (15%) |
daft.governor.jobserver | "auto" | Export a shared POSIX jobserver (MAKEFLAGS) so make/cargo/ninja inside concurrent hooks share one token pool. Note: a bare make in a hook picks up -jN and becomes (bounded) parallel — set off if a hook can't tolerate that |
Merge Settings
Defaults for daft merge flags. Each key can be set globally, locally, or system-wide; CLI flag arguments always override the configured default. The canonical reference for these keys is the daft merge CLI page.
| Key | Default | Description |
|---|---|---|
daft.merge.style | merge | Default merge style: merge, squash, rebase, or rebase-merge |
daft.merge.cleanup | keep | Default post-merge cleanup: keep or remove-branch |
daft.merge.edit | (git's default) | Default for the merge-message editor on a TTY (true/false) |
daft.merge.commit | true | Default commit-after-squash behavior (false is equivalent to --no-commit) |
daft.merge.signoff | false | Default for --signoff |
daft.merge.gpgSign | (unset) | Default for --gpg-sign (true, false, or <keyid>) |
daft.merge.verifySignatures | false | Default for --verify-signatures |
daft.merge.allowUnrelatedHistories | false | Default for --allow-unrelated-histories |
daft.merge.strategy | (unset) | Default merge strategy (-s / --strategy) |
daft.merge.strategyOption | (unset) | Default strategy option (-X / --strategy-option); repeatable via multi-value config |
daft.merge.adoptTargetOnDemand | prompt | How to handle merging into a branch with no worktree: prompt, yes, or no |
daft.merge.requireCleanTarget | true | Refuse to start the merge when the target worktree has uncommitted changes |
--set-default writes daft.merge.style and daft.merge.cleanup after a successful merge, so a failed or conflicted merge never silently changes your defaults.
These git-config keys are the personal layer — style preferences on your machine. Team merge policy (fast-forward requirement, clean-source requirement) is committed in daft.yml under a top-level merge: block and is deliberately not relaxable from git config — only explicit per-invocation flags (--no-ff-only, --source-worktree any) can override it. See Merge gate policy.
Default squash + cleanup recipe
To make daft merge <source> always squash and remove the source branch on success:
bash
git config daft.merge.style squash
git config daft.merge.cleanup remove-branchFor non-interactive or CI use, also suppress the editor so the auto-generated squash message is used verbatim:
bash
git config daft.merge.edit falsedaft merge feature/done then becomes a one-shot squash + commit + branch cleanup. The migration table in the daft merge reference covers the v1.9 → v1.10 key renames if you're migrating an older config.
See the daft merge reference for flag-level details and the Lifecycle hooks reference for pre-merge / post-merge hook configuration.
Ownership Settings
Controls how daft determines which branches are "yours" for the purposes of daft sync --rebase / --push, the Owner column in daft list / daft sync / daft prune, and the sync TUI divider between owned and unowned branches.
| Key | Default | Description |
|---|---|---|
daft.ownership.strategy | recency-plurality | Strategy for deducing branch ownership from the base..branch commit range. Values: tip, any, first, plurality, majority, recency-plurality. |
Each strategy decides the branch owner from the commits in the range base..branch (your branch minus everything already in the default branch):
tip— owner is the author of the newest commit. Fast and simple, but flips ownership whenever a teammate or bot pushes a single commit on top.any— owner is you if any commit in range is yours; otherwise the tip author. Most permissive.first— owner is the author of the oldest commit in range — "who started this branch."plurality— owner is the author with the most commits. Ties broken by most-recent-commit-of-tied-author.majority— owner is the author with strictly more than 50% of commits. No owner if no majority.recency-plurality(default) — owner is the author with the highest recency-weighted score. Each commit at rankkfrom the tip (k=0= tip) contributes weight1/(k+1). Ties broken by most-recent-commit. Matches the intuition "favor recent work" while staying robust to drive-by commits on top.
Multi-Remote Settings
| Key | Default | Description |
|---|---|---|
daft.multiRemote.enabled | false | Enable multi-remote directory organization |
daft.multiRemote.defaultRemote | "origin" | Default remote for new branches in multi-remote mode |
Hooks Settings
| Key | Default | Description |
|---|---|---|
daft.hooks.enabled | true | Master switch for all hooks |
daft.hooks.defaultTrust | "deny" | Default trust level for unknown repositories (deny, prompt, or allow) |
daft.hooks.userDirectory | ~/.config/daft/hooks/ | Path to user-global hooks directory |
daft.hooks.timeout | 300 | Hook execution timeout in seconds |
daft.hooks.trustPrune | true | Auto-prune stale entries from the trust database (background, once per 24h) |
Per-Hook Settings
Each hook type can be configured individually. The hook name uses camelCase.
| Key | Default | Description |
|---|---|---|
daft.hooks.<hookName>.enabled | true | Enable/disable a specific hook type |
daft.hooks.<hookName>.failMode | varies | Behavior on failure: abort or warn |
Hook names: postClone, worktreePreCreate, worktreePostCreate, worktreePreRemove, worktreePostRemove.
Default fail modes:
worktreePreCreate:abort(setup must succeed before creating worktree)worktreePostCreate:abort(a failed setup makes the creation command exit non-zero and skip its-x/--execcommands; the worktree is kept on disk)preMerge:abort(gates the merge)- All others:
warn(don't block operations)
A hook's fail mode can also be committed per-hook in daft.yml via fail_mode: (abort or warn), which ships a repo-wide default to every clone. The git config above takes precedence, so a developer can still override the committed value on their own machine. See the YAML reference.
Hook Output Settings
| Key | Default | Description |
|---|---|---|
daft.hooks.output.quiet | false | Suppress hook stdout/stderr |
daft.hooks.output.timerDelay | 5 | Seconds before a silent job shows an elapsed timer (verbose output only) |
daft.hooks.output.tailLines | 6 | Live rolling output lines per job in verbose output (0 = none); the persisted log is never windowed. Also sizes the live window of daft exec's per-worktree output threads |
daft.hooks.output.verbose | false | Thread each hook job's log through the progress timeline (-v per invocation does the same); in plain output, show each job's command line |
daft.hooks.output.parseManagers | true | Recognize hook-manager output (lefthook 2.x) on the pre-push stream and render the manager's jobs as first-class rows; false restores the single opaque pre-push job. Unrecognized streams fall back to it either way |
YAML Hooks Configuration
Hooks can also be configured through a daft.yml file for richer features including multiple jobs, execution modes, job dependencies, and conditional execution. The same file's top-level tasks: section defines named, user-invoked task groups run with daft run — the serve on demand counterpart to lifecycle hooks.
See the Hooks guide for the complete daft.yml reference, including the Tasks section.
Examples
bash
# Enable remote sync (fetch on checkout, push on start, delete remote on remove)
daft config set remote-sync on
# Or enable individual settings
git config daft.checkout.fetch true
git config daft.checkout.push true
git config daft.branchDelete.remote true
# Use a different remote
git config daft.remote upstream
# Disable auto-cd globally
git config --global daft.autocd false
# After pruning, cd to default branch worktree instead of root
git config daft.prune.cdTarget default-branch
# Auto-create worktree when branch not found in daft go
git config daft.go.autoStart true
# Use rebase-style update by default
git config daft.update.args "--rebase"
# Disable hooks globally
git config --global daft.hooks.enabled false
# Let creation commands continue past a failing post-create hook
git config daft.hooks.worktreePostCreate.failMode warnEnvironment Variables
| Variable | Description |
|---|---|
DAFT_CD_FILE | Temp file path for shell wrapper CD communication (set by shell wrappers) |
DAFT_NO_HINTS | Set to suppress contextual hint messages |
DAFT_NO_TRUST_PRUNE | Set to disable automatic trust database pruning |
DAFT_NO_UPDATE_CHECK | Set to disable version update notifications |
DAFT_NO_BACKGROUND_JOBS | Set to promote all background hook jobs to foreground (any value, including 0); --hooks foreground does this per run |
DAFT_SIZE_WALK_JOBS | Override directory-size-walk concurrency (takes precedence over daft.list.sizeConcurrency) |
NO_COLOR | Standard variable to disable colored output |
PAGER | Override the pager for daft release-notes |
Git Hooks
Every daft-initiated git push honors the repository's pre-push hook -- native .git/hooks, or hook managers registered through core.hooksPath (lefthook, husky, pre-commit). A pre-push secret scanner or test gate that fires on git push fires on daft's pushes too. The hook run is reported as a pre-push phase with the hook's output, using the same surface as daft's lifecycle hooks.
Pushes that provably carry no content are the conditional sites: the hook runs only when there is something for a content gate to validate. Two kinds of push qualify as ref-only:
- The automatic upstream push on
daft start/git worktree-checkout -b, when branching off a fully-pushed base -- a new branch name pointing at commits the remote already has. - Remote-branch deletes --
daft remove/git worktree-branch -dwith remote deletion enabled,daft rename's old-name cleanup, anddaft multi-remote move --delete-old. A delete pushes a null ref update and zero objects, so it is ref-only by construction.
Control this with daft.pushVerify (the base setting every such site reads; daft.checkout.pushVerify overrides it for the upstream push alone):
| Value | Behavior |
|---|---|
auto | (default) Run the hook only when the push carries new commits |
always | Run the hook on every such push, including ref-only ones |
never | Never run the hook on these pushes, even when the upstream push carries commits |
Note the asymmetry in never: auto and always decide per push from what it transmits, but never is unconditional. Setting daft.pushVerify to never to quiet the hook on deletes therefore also disarms it on the upstream push when that push does carry new commits. Scope the base key to deletes by re-arming the other site with daft.checkout.pushVerify = auto.
always is for repositories whose pre-push hooks act on the ref rather than the content (branch-naming policies, protected-branch guards that reject deleting release/* and friends): daft cannot classify an opaque hook, so the automatic skip is based purely on pushed content. Content-carrying pushes -- daft push, daft sync --push, and daft rename's new-name push -- always verify and do not consult these settings.
Pass --no-verify to skip the hook for one invocation: daft push, daft sync --push, daft start / daft go -b / git worktree-checkout -b, daft rename, daft remove / git worktree-branch -d/-D, and daft multi-remote move all accept it. daft merge is the exception -- its post-merge cleanup delete is governed by daft.pushVerify alone.
Remote operations are disabled by default. When enabled (via daft config set remote-sync on or by setting the individual keys), this affects:
daft start/git worktree-checkout -b-- pushes the new branch to set upstream tracking (controlled bydaft.checkout.push; pre-push hook gating perdaft.checkout.pushVerifyabove)daft remove/git worktree-branch -d-- pushes--deleteto remove the remote branch (controlled bydaft.branchDelete.remote; pre-push hook gating perdaft.pushVerifyabove)daft merge-- post-merge cleanup deletes the merged branch's remote ref on the samedaft.branchDelete.remotesetting, gated bydaft.pushVerify. Unlike the other sites,daft mergehas no--no-verify, so the config key is the only control there.daft multi-remote move --push-- pushes an existing branch to a new remote
Push failures are graded by the gate. When a pre-push hook is installed and the push fails, the command exits non-zero -- a gate saying no is a failure, not a warning (any worktree the command created or moved is still completed and usable, and the error names the manual recovery command). Without a hook, push failures (network issues, auth errors, remote rejection rules) stay non-fatal: the local worktree and branch remain usable and a warning is shown.
Use --local on any worktree command to skip all remote operations for that invocation, regardless of config.
daft's lifecycle hooks (configured in daft.yml or
.daft/hooks/) are separate from git's own hooks and are always executed normally. :::