Mount or unmount a git worktree as a fully functional local Laravel Herd WordPress site.
When developing on a feature branch using git worktree, the worktree directory is bare: no WordPress core, no wp-config, no plugins, no uploads, no built assets. wt-link mount wires all of that up in seconds so the branch is live at the same .test URL as your main site.
brew tap piqusy/tap
brew install wt-linkDownload wt-link-<version>.tar.gz from Releases, then:
tar -xzf wt-link-<version>.tar.gz
sudo cp bin/wt-link /usr/local/bin/
sudo cp -r lib/wt-link /usr/local/lib/| Tool | Purpose |
|---|---|
WP-CLI (wp) |
WordPress core download |
Laravel Herd (herd) |
Local domain management |
| Composer | PHP dependency install (fallback only) |
bun / npm / yarn / pnpm |
JS dependency install and asset build (auto-detected) |
jq |
Parse setup.json |
rsync |
File sync (macOS built-in) |
wt-link <command> [--cwd PATH] [--force] [--yes] [--hard-copy]
Commands:
mount Set up a git worktree as a fully working local Herd site
unmount Tear down and restore the canonical site
status Show current link status
list Show all registered sites and their active worktrees
rebuild-composer Re-run composer install for all Eightshift packages
rebuild-node Re-run <pm> install + build for all Eightshift packages
Options:
--cwd PATH Run against a specific worktree directory (default: current dir)
--force Switch domain to this worktree without prompting (shows a notice)
--yes / -y Proceed non-interactively with no output (for hook contexts)
--hard-copy Hard-copy untracked plugins instead of symlinking (parallel cp -Rl)
--no-indicator Skip injecting the worktree branch indicator into the site
# Inside a worktree directory
wt-link mount
# Target a specific directory
wt-link mount --cwd ~/Sites/myproject.feature-branch
# Override the canonical site location
CANONICAL_SITE=~/Sites/myproject wt-link mount
# Force re-mount (e.g. after canonical site deps changed)
wt-link mount --force
# Hard-copy plugins instead of symlinking (filesystem-isolated, faster plugin activation)
wt-link mount --hard-copy
# Tear down
wt-link unmount
# Check status
wt-link status
# List all registered sites (works from any directory)
wt-link list
# Rebuild PHP deps after a composer.json change
wt-link rebuild-composer
# Rebuild JS assets after a package.json change
wt-link rebuild-node- WP core — Downloads WordPress (version from
setup.json) or extracts from WP-CLI cache - wp-config.php — Copies from the canonical site
- Root runtime files — Copies
wt-link.json,wt-link.local.json,.env, and.env.*variants from the canonical site when they are missing in the worktree (example templates like.env.exampleare skipped) - Plugins — Symlinks git-untracked plugins from the canonical site; use
--hard-copyto hard-copy instead (parallelcp -Rl, useful when plugins need filesystem isolation between worktrees) - Uploads — Symlinks
wp-content/uploadsfrom the canonical site - Branch indicator — Injects
wp-content/mu-plugins/wt-link-indicator.phpwith the current branch name baked in; shows a fixed bottom-center toast (⎇ branch) on both frontend and admin, click to dismiss. Pass--no-indicatorto skip. - Eightshift packages — For each theme/plugin with
eightshift-libs:- Hardlink-copies
vendor/andvendor-prefixed/from the canonical site (cp -Rl; zero extra disk on APFS, falls back tocomposer installif canonical has none) - Runs
<pm> installsequentially per theme — package manager auto-detected from lockfile (bun,yarn,pnpm, ornpm) - Runs
<pm> run buildin parallel across themes; skips build for plugins
- Hardlink-copies
- Herd link — Runs
herd link <site-name>so the worktree is live athttps://<site-name>.test/. Ifurls.subdomainsis set, each subdomain is linked and secured as well (e.g.https://research.mysite.test/)
A state file at ~/.config/wt-link/<site>.<worktree-basename>.state tracks everything created so unmount can reverse it precisely. Existing .worktree-link-state files inside worktrees are migrated to this location automatically on next mount.
Reverses all of the above: removes symlinks, hard-copied plugin directories, copied root runtime files, WP core files, wp-config, restores the Herd link to the canonical site, and deletes the state file.
Re-runs composer install for every Eightshift package in the worktree. Useful after pulling changes that add or update PHP dependencies.
Re-runs <pm> install and <pm> run build for every Eightshift package in the worktree. Useful after pulling changes that add or update JS dependencies or after a failed build.
wt-link reads setup.json from the worktree root. Minimum required fields:
{
"urls": {
"local": "https://mysite.test/"
},
"core": "6.7.2"
}This is the standard Eightshift project format.
If your site uses WP multisite or WPML with subdomain sub-sites (e.g. research.mysite.test), declare them in a wt-link.json file at the project root:
{ "subdomains": ["research", "platform"] }On mount, each subdomain gets its own herd link and TLS certificate so it is reachable at https://research.mysite.test/, etc. On unmount, links and certs are restored to the canonical site automatically.
For per-developer overrides (e.g. you only need one of the sub-sites), create wt-link.local.json alongside it — its values replace the base:
{ "subdomains": ["research"] }Add wt-link.local.json to your project's .gitignore. Commit wt-link.json so every developer gets subdomains working out of the box.
If your canonical site has local runtime files like .env, .env.local, or similar .env.* variants, wt-link mount copies them into the worktree when missing so the site can boot without committing secrets. Example templates such as .env.example are intentionally not copied.
The tool is split into a thin entry point and nine library modules:
bin/
wt-link # Entry point: arg parsing, project resolution, dispatch
lib/wt-link/
ui.sh # log/success/warn/error/step output helpers
utils.sh # require_cmd, require_pm, wp_clean
project.sh # find_project_root, detect_package_manager, find_* helpers
state.sh # State file and registry read/write helpers
runtime.sh # run_pm_install, run_pm_build, run_with_spinner, wait_for_herd
mount.sh # cmd_mount + 8 private _mount_* sub-functions
unmount.sh # cmd_unmount
status.sh # cmd_status, cmd_starship
list.sh # cmd_list
rebuild.sh # cmd_rebuild_composer, cmd_rebuild_node
To run from source without installing:
git clone https://github.com/piqusy/wt-link
cd wt-link
./bin/wt-link --helpShow a ⛓ symbol in your prompt when inside a mounted worktree.
wt-link starship is a pure exit-code boolean — exit 0 when the current directory is a mounted worktree, exit 1 otherwise. The symbol is defined entirely in your config.
Add to ~/.config/starship.toml:
[custom.wt-link]
command = 'cd "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null && wt-link starship'
when = 'cd "$(git rev-parse --show-toplevel 2>/dev/null)" 2>/dev/null && wt-link starship'
format = "[⎇ $output]($style) "
style = "bold yellow"
shell = ["sh"]
ignore_timeout = true
git rev-parse --show-toplevelresolves the repo root first so the indicator works from any subdirectory, not just the worktree root.
Add to ~/.p10k.zsh:
function prompt_wt_link() {
wt-link starship || return
p10k segment -f yellow -t '⎇' # change symbol to taste
}Then add wt_link to your prompt elements in the same file:
POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=(... wt_link ...)
# or on the right:
# POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(... wt_link ...)Add to ~/.zshrc (after source $ZSH/oh-my-zsh.sh):
function _wt_link_prompt() {
wt-link starship && echo '⎇ ' # change symbol to taste
}
RPROMPT='$(_wt_link_prompt)'"$RPROMPT"Or add it to the left prompt by modifying your theme's PROMPT variable instead.
Add to ~/.bashrc or ~/.bash_profile:
_wt_link_prompt() {
wt-link starship && printf '⎇ ' # change symbol to taste
}
PS1='$(_wt_link_prompt)'"$PS1"MIT © Ivan Ramljak