@linchpinagency/worktree-utils
v1.0.19
Published
A library of CLI commands to help manage jumping between worktrees when developing for WordPress using Studio, LocalWP or wp-env and Agentic workers
Maintainers
Readme
What is this CLI?
linchpin wt is a git worktree helper tuned for WordPress plugin development alongside Agent support to help easily swap Symlinks between your local environment and worktrees created by you or agents.
It is designed for this setup:
- Plugin repository in
~/Documents/GitHub/<plugin-name>. - Multiple git worktrees created by Codex or other agents.
- A shared local WordPress environment (Studio,
wp-env, or LocalWP). - A plugin directory in that environment that should point to a specific worktree via symlink.
Why symlinks? Why isn’t the repo checked out directly in my environment?
Your plugin repo is not checked out directly into Studio, LocalWP, or wp-env on purpose. The workflow relies on symlinks so you can swap which worktree (branch) the environment sees:
- The repo lives in its own directory (e.g.
~/Documents/GitHub/my-plugin) with multiple git worktrees (e.g.main,conductor/a,feature/b). - The WordPress environment has one plugin (or theme) slot (e.g.
~/Studio/mysite/wp-content/plugins/my-plugin). That slot is a symlink pointing at one of the worktree paths. - When you run
linchpin wt switch <branch>(or pick from the list), we repoint that symlink to the chosen worktree. This allows for our local environment to use an already checked out worktree with out any errors.
So you keep a single WordPress install and switch which worktree it uses by changing the symlink target.
Should you use this?
Use this CLI if you already like git worktree but need WordPress-specific environment switching.
This project does not replace git worktrees. It adds a WordPress workflow layer on top of them:
- Store plugin/theme target paths per local environment (
Studio,LocalWP,wp-env, custom). - Repoint one plugin/theme symlink to a different worktree with one command.
- Add safety checks around symlink replacement and worktree deletion.
- Keep one local WordPress install while reviewing many branches/worktrees.
You probably do not need this if:
- You only need
git worktree add/list/remove. - You do not use a shared local WordPress environment.
- You are fine managing symlink paths and switching manually.
linchpin wt vs plain git worktree
| Need | Plain git worktree | linchpin wt |
|---|---|---|
| Create/list/remove worktrees | Yes (git worktree ...) | Yes (wrapper commands: new, ls, del, get) |
| Switch which branch your WordPress site loads | Manual symlink edits | Built-in: linchpin wt switch [branch] --env <name> |
| Save WordPress environment paths for team use | No | Yes (linchpin wt config init + .linchpin.json) |
| Guardrails for WP plugin/theme symlink targets | No | Yes (blocks non-symlink target replacement unless --force) |
| Interactive worktree picker for switching | No | Yes (TTY picker + optional fzf for cd) |
If your pain is "I can create worktrees, but switching my WordPress site between them is manual and error-prone," this tool is the fit.
Install
npm install -g @linchpinagency/worktree-utilsFor local development in this repository:
npm linkTeam setup guide
1. Prerequisites
git2.37+ (worktree support).- Node.js
20+andnpm. - Optional:
fzffor interactivelinchpin wt cd. - A local WordPress environment (Studio,
wp-env, or LocalWP). - Your plugin repository cloned under
~/Documents/GitHub/<plugin-name>.
2. Install CLI
npm install -g @linchpinagency/worktree-utilsConfirm install:
linchpin --help
linchpin wt help3. Initialize project config
From the plugin or theme repo root (base worktree), run:
linchpin wt config initWhen run in an interactive terminal, you're guided through:
- Plugin, theme, or wp-content – Whether this repo is a WordPress plugin, theme, or a full wp-content project (the entire wp-content folder is the repo). You can pre-select this with
--type <plugin|theme|wp-content>. - Slug / symlink name – For plugins and themes, the WordPress directory name (defaults to the repo directory name). For wp-content projects, the symlink name (defaults to
wp-content) — useful when your repo has a client name instead ofwp-content. - Environment(s) – For each environment: Environment type (Studio, LocalWP, wp-env, or Other), which sets the base folder; then for Studio/LocalWP you pick a site from that base (list or
fzfif installed), or for wp-env you enter the WordPress root path; for Other you enter name and full path. - Choose the default environment for
linchpin wt switch.
This creates .linchpin.json. You can edit it later if paths or environments change.
- Create initial symlink(s) – If the target already exists as a real folder (not a symlink), you're prompted to back it up (rename with
.bkpsuffix), delete it (with confirmation), or skip that environment.
If .linchpin.json already exists, the flow offers Overwrite, Edit (keep existing and add more environments), or Cancel.
For scripts or CI (no TTY), use non-interactive mode so a default template is written without prompts:
linchpin wt config init --type <plugin|theme|wp-content> [--plugin-slug <slug>] [--force] [--no-interactive]Use --type wp-content when your repo represents an entire wp-content folder (common for client projects where the repo is named after the client, not wp-content). Use --force to overwrite an existing .linchpin.json without prompting. Use --no-interactive to skip prompts even when running in a terminal.
4. Paths built by config init
For Studio and LocalWP, paths are built from the environment type and the site you pick:
- Studio:
~/Studio/<site>/wp-content/plugins|themes/<slug>(or~/Studio/<site>/wp-contentfor wp-content projects) - LocalWP:
~/Local Sites/<site>/app/public/wp-content/plugins|themes/<slug>(or…/wp-contentfor wp-content projects) - wp-env: You provide the WordPress root; the CLI appends
wp-content/plugins|themes/<slug>(orwp-contentfor wp-content projects).
Use absolute paths in .linchpin.json if you edit by hand. ~ is supported.
5. Create and switch worktrees
Create a worktree for a new branch:
linchpin wt new feature/my-changeOr attach an existing remote branch:
linchpin wt get feature/existing-branchPoint your WordPress environment to that worktree:
linchpin wt switch feature/my-change --env studio6. Verify active target
Check current worktree metadata:
linchpin wt current --link --env studioList all worktrees:
linchpin wt ls7. Daily review workflow
- Open or create a worktree for the branch under review.
- Run
linchpin wt switch --env <environment>to repoint the plugin symlink. - Test the branch in the shared WordPress install.
- Repeat for the next worktree/branch.
- Clean up with
linchpin wt delwhen the branch is merged.
8. Switch and cd in one step
After wt switch repoints a symlink your shell is still in the old worktree. Wrap the command in cd to land in the new target automatically:
cd "$(linchpin wt switch feature/my-change)"
cd "$(linchpin wt switch)" # interactive picker
cd "$(linchpin wt switch --env localwp)" # specific environmentWhen piped (wrapped in $()), informational output goes to stderr so you still see it, while stdout carries the symlink path for cd.
Optional: fully automatic with shell-init
If you prefer linchpin wt switch to handle the cd for you every time, add this to your shell profile (~/.zshrc, ~/.bashrc, or ~/.config/fish/config.fish):
eval "$(linchpin shell-init)"This defines a thin wrapper that re-enters your current directory after a successful switch, so the shell picks up the repointed symlink. The shell is auto-detected from $SHELL. To force a specific shell: eval "$(linchpin shell-init --shell zsh)".
9. Path helpers
Use command substitution for other path-returning commands:
cd "$(linchpin wt cd)"
cd "$(linchpin wt home)"10. Troubleshooting
Missing .linchpin.json: Runlinchpin wt config initin the base worktree (interactive prompts) orlinchpin wt config init --type plugin --plugin-slug <slug> --no-interactivefor a default file.Environment '<name>' is not configured: Add the environment key in.linchpin.json.Target exists and is not a symlink: Uselinchpin wt switch ... --forceonly if replacing the directory is intended.Worktree has uncommitted changeson delete: Commit/stash first, or force withlinchpin wt del --force.fzf is not installed: Installfzfor pass a branch/path directly tolinchpin wt cd <ref>.
Command surface
linchpin shell-init [--shell bash|zsh|fish]
linchpin wt ls [--json]
linchpin wt current [--link] [--env <name>]
linchpin wt switch [worktree|branch] [--env <name>] [--force] [--dry-run]
# No argument in a TTY: interactive picker from available worktrees. Non-interactive: use current worktree.
# When piped, outputs the symlink target path for cd: cd "$(linchpin wt switch ...)"
linchpin wt new [name]
linchpin wt get <branch>
linchpin wt extract
linchpin wt mv <new-branch-name>
linchpin wt del [-f|--force]
linchpin wt cd [branch|path]
linchpin wt home
linchpin wt use
linchpin wt gone
linchpin wt copy <path>
linchpin wt link <path>
linchpin wt invoke <hook>
linchpin wt config init [--type <plugin|theme|wp-content>] [--plugin-slug <slug>] [--force] [--no-interactive]
linchpin wt config showShell usage notes:
linchpin wt cdandlinchpin wt homereturn paths for command substitution.- Use
cd "$(linchpin wt cd)"andcd "$(linchpin wt home)". linchpin wt cdusesfzfwhen no argument is provided.
Configuration
Create .linchpin.json in the base repository root. The easiest way is to run linchpin wt config init in a terminal and follow the prompts. You can also create or edit the file manually:
Plugin/theme project:
{
"agent": "conductor",
"agentBasePath": "/Users/you/conductor",
"wordpress": {
"contentType": "plugin",
"pluginSlug": "my-plugin",
"defaultEnvironment": "studio",
"environments": {
"studio": "/Users/you/Sites/studio/wp-content/plugins/my-plugin",
"wp-env": "/Users/you/Documents/projects/site/.wp-env/.../plugins/my-plugin",
"localwp": "/Users/you/Local Sites/site/app/public/wp-content/plugins/my-plugin"
}
}
}WP-content project (repo is the entire wp-content folder):
{
"agent": "conductor",
"wordpress": {
"contentType": "wp-content",
"defaultEnvironment": "localwp",
"environments": {
"localwp": "/Users/you/Local Sites/site/app/public/wp-content"
}
}
}If the repo uses a custom symlink name (e.g. the repo is named after the client), add "symlinkName":
{
"wordpress": {
"contentType": "wp-content",
"symlinkName": "client-wp-content",
"defaultEnvironment": "localwp",
"environments": {
"localwp": "/Users/you/Local Sites/site/app/public/client-wp-content"
}
}
}Behavior notes:
- Agent / base path:
agent(Conductor, Claude Code, Codex, or Custom Path) and optionalagentBasePathrecord where your worktree repos live. Default base paths: Conductor~/conductor, Claude Code~/Documents, Codex~/Documents/GitHub. For Custom Path you’re prompted for a base path duringconfig init. - If
defaultEnvironmentis omitted, the first environment key is used. ~is supported in configured paths.linchpin wt switchwithout a worktree argument: in an interactive terminal you get a picker of available worktrees; in non-interactive use it uses the current worktree.
Hooks
Hook files are sourced in a subshell when present:
.linchpin/hooks/<hook-name>
Supported lifecycle hooks:
pre-switch,post-switchpre-new,post-newpre-get,post-getpre-extract,post-extractpre-mv,post-mvpre-del,post-del
Manual invocation:
linchpin wt invoke pre-new
linchpin wt invoke post-switchHook environment variables include LINCHPIN_BRANCH, LINCHPIN_WORKTREE, and for switch hooks LINCHPIN_ENVIRONMENT.
To run commands after switching worktrees (e.g. composer install, npm run build), create .linchpin/hooks/post-switch. The hook runs with the worktree as the current directory:
#!/bin/bash
composer install
npm install && npm run buildTypical WordPress review flow
- Open a plugin worktree.
- Run
cd "$(linchpin wt switch --env studio)"to repoint the symlink and land in the new target. - Use your existing WordPress environment to review that branch.
- Move to another worktree and switch again.
Safety behavior
- Existing symlink targets are repointed safely.
- Existing non-symlink targets are blocked unless
--forceis used. linchpin wt delblocks dirty or unmerged branches unless forced.
Development
npm install
npm testHusky enforces Conventional Commits on commit-msg:
npm run prepareExample commit format:
feat(LINCHPIN-4850): add release automationReleases
Releases are managed by release-please in GitHub Actions:
- Pushes to
mainrun.github/workflows/release-please.yml. release-pleaseopens/updates a release PR from conventional commits.- When the release PR is merged, a GitHub release/tag is created.
- If a release is created, the workflow publishes
@linchpinagency/worktree-utilsto npm.

