A global DDEV add-on that clones an existing DDEV project (files + database) into a new directory. If the project is a git repo, it uses git worktree to create a linked worktree; otherwise it copies files with rsync.
The new directory gets its own DDEV project name via config.worktree.local.yaml, so both instances can run simultaneously without collision.
ddev add-on install elabx/ddev-worktreesOr install from a local clone:
ddev add-on install /path/to/ddev-worktreeOnce installed, the ddev worktree and ddev worktree-remove commands are available in every DDEV project.
cd ~/projects/my-site
# Create a worktree for a new branch
ddev worktree feature-login
# Both projects now run side-by-side
ddev list
# my-site running
# my-site-feature-login running
# Done with the worktree? Clean up
ddev worktree-remove feature-loginddev worktree <branch-or-name> [--profile=NAME] [--no-db] [--no-start] [--force]
Creates a clone of the current DDEV project in a sibling directory.
What it does:
- Exports the database from the source project
- Creates the target directory via
git worktree add(orrsyncif not a git repo) - Copies
.ddev/if it's gitignored - Copies any files specified in the profile's
copylist - Creates
.ddev/config.worktree.local.yamlwith a unique project name - Starts DDEV and imports the database
- Runs any
post_createcommands from the profile
Flags:
| Flag | Description |
|---|---|
--profile=NAME |
Which profile from .ddev/worktree-hooks.yaml to use |
--no-db |
Skip database export/import |
--no-start |
Create files only — don't start DDEV or run post_create |
--force, -f |
Overwrite an existing target directory/project |
Target directory: ../<source-dirname>-<sanitized-name>
Target project name: <source-project>-<sanitized-name>
Branch names are sanitized for use as directory names (e.g., feature/foo becomes feature-foo).
ddev worktree-remove <name> [--list]
List clones:
ddev worktree-remove --listRemove a clone:
ddev worktree-remove feature-loginThis stops the DDEV project, removes the git worktree (or deletes the directory), and cleans up. It refuses to remove directories that don't have a config.worktree.local.yaml as a safety measure (the older config.worktree.yaml is still accepted, so worktrees created before v1.1.0 remain removable).
Create this file in your project's .ddev/ directory to configure what gets copied and what commands run after clone creation. Commit it to git so your team shares the same setup.
# .ddev/worktree-hooks.yaml
default_profile: full
profiles:
full:
copy:
- site/assets
- .env
post_create:
- composer install
- npm install
minimal:
copy:
- .env
post_create:
- composer install
frontend:
copy:
- .env
post_create:
- npm installFields:
| Field | Description |
|---|---|
default_profile |
Which profile to use when --profile is not specified |
profiles.<name>.copy |
Files/directories to copy from source to target (relative to project root). Use this for things not in git: uploads, .env, etc. |
profiles.<name>.post_create |
Shell commands to run in the target directory after DDEV is started |
If no .ddev/worktree-hooks.yaml exists, the command works fine — it just skips the copy and post_create steps.
Note: Only the ProcessWire profile has been tested. The other profiles are untested examples — adapt them to your project's needs.
ProcessWire:
default_profile: full
profiles:
full:
copy:
- site/assets
- .env
post_create:
- composer install
minimal:
copy:
- .env
post_create:
- composer install
frontend:
copy:
- .env
- site/assets
post_create:
- npm installDrupal:
default_profile: full
profiles:
full:
copy:
- sites/default/files
- .env
post_create:
- composer install
- ddev drush crLaravel:
default_profile: full
profiles:
full:
copy:
- storage/app
- .env
post_create:
- composer install
- npm install
- ddev artisan key:generateWordPress:
default_profile: full
profiles:
full:
copy:
- wp-content/uploads
- .env
post_create:
- composer installThe clone gets a config.worktree.local.yaml with override_config: true and a unique name field. This overrides the project name from config.yaml without modifying any git-tracked files. Both projects can run simultaneously with their own containers, databases, and URLs.
When the source project is a git repo:
- Existing local branch —
git worktree add <path> <branch> - Remote branch only — creates a local tracking branch
- No such branch — creates a new branch from HEAD
If there's no git repo, the command uses rsync to copy all files, excluding DDEV runtime artifacts.
The generated config is named config.worktree.local.yaml, and the .local. is deliberate: DDEV's own generated .ddev/.gitignore already ignores /config.*.local.y*ml. The file stays out of git status without this add-on writing to .gitignore, .git/info/exclude, or any other git-owned file.
One wrinkle: .ddev/.gitignore is itself untracked, so a fresh worktree starts without it and the config is briefly visible to git status. The first ddev start regenerates the ignore file and it disappears. With --no-start, it stays visible until you start the project.
With a database export (the default), the dump is written to a private temp directory created with mktemp -d under $TMPDIR (falling back to /tmp), and removed by an EXIT/INT/TERM trap — so an interrupted or failed run leaves nothing behind, and concurrent runs never collide.
Affects v1.1.0 and earlier on macOS. Those versions built the dump path with the X placeholders mid-string (ddev-worktree-db-XXXXXX.sql.gz). BSD mktemp only substitutes a trailing run of Xs, so the name was used literally — the same fixed path on every run. There was also no cleanup trap, so any failure between export and import left the file behind, and every later run then failed on it.
Unblock an affected machine:
rm -f /tmp/ddev-worktree-db-*.sql.gzThen upgrade to v1.1.1 or later, which uses a temp directory plus a cleanup trap. Note that reinstalling or updating the add-on overwrites ~/.ddev/commands/host/worktree, so patch the repo rather than the installed copy.
- DDEV >= v1.24.0
- yq (recommended) — for full YAML parsing of
.ddev/worktree-hooks.yaml. Install viabrew install yqor see yq docs. Without yq, the command falls back to basic grep/awk parsing with a warning. - rsync — for file copying (pre-installed on macOS and most Linux distributions)
ddev add-on remove ddev-worktree