A CLI tool for managing git worktrees with a plugin system for extensibility.
# Install globally
npm install -g @jescalan/wt
# Or run directly with npx
npx @jescalan/wt <command>All commands have short aliases for faster typing:
| Command | Alias | Description |
|---|---|---|
create |
c |
Create a new worktree |
merge |
m |
Merge and clean up |
search |
s |
Interactive worktree selector |
list |
l |
List all worktrees |
remove |
rm |
Remove a worktree |
status |
st |
Show worktree overview |
Create a new worktree with a branch.
wt create feature-auth
wt c feature-auth # Short formThis will:
- Create a new worktree at the configured path (default:
../{repo}-{branch}) - Create a new branch (or use existing if it exists)
- Copy gitignored files from the current worktree (configurable)
- Run any configured
beforeCreate/afterCreatehooks
Merge the current worktree's branch into the default branch and clean up.
wt merge # Merge, remove worktree, delete branch
wt merge -k # Merge but keep worktree and branch
wt m # Short formThis will:
- Switch to the main worktree
- Merge the current branch
- Remove the worktree and delete the branch (unless
--keep) - Run any configured hooks
On merge failure (conflicts, uncommitted changes), you'll be returned to your original worktree with a clear error message.
Interactively select and switch between worktrees.
wt search
wt s # Short formUse arrow keys to navigate, Enter to select, q/Escape to cancel.
List all worktrees with status information.
wt list
wt l # Short formOutput example:
* main /path/to/repo (default)
feature-auth /path/to/repo-feature 2 ahead, 1 behind
bugfix-xyz /path/to/repo-bugfix 3 uncommitted
Shows:
- Current worktree marked with
* - Default branch marked with
(default) - Ahead/behind count compared to default branch
- Count of uncommitted changes
Remove a worktree with confirmation.
wt remove feature-auth # Remove specific worktree
wt remove # Interactive selection
wt rm -f feature-auth # Skip confirmation promptOptions:
-f, --force: Skip the confirmation prompt
The default branch worktree cannot be removed.
Show a detailed overview of all worktrees.
wt status
wt st # Short formOutput example:
Repository: my-project
Default branch: main (3 worktrees)
┌─────────────────┬──────────────────────────────┬─────────────┐
│ Branch │ Path │ Status │
├─────────────────┼──────────────────────────────┼─────────────┤
│ main │ ~/code/my-project │ clean │
│ feature-auth │ ~/code/my-project-feature │ 2 modified │
│ bugfix-xyz │ ~/code/my-project-bugfix │ 1 ahead │
└─────────────────┴──────────────────────────────┴─────────────┘
Add one line to your shell config to enable automatic directory changing:
Zsh (~/.zshrc):
eval "$(wt init zsh)"Bash (~/.bashrc):
eval "$(wt init bash)"Fish (~/.config/fish/config.fish):
wt init fish | sourceCreate a wt.config.ts file in your project root (or any parent directory):
import type { WtConfig } from "@jescalan/wt/types";
export default {
// Copy gitignored files when creating worktrees (default: true)
copyGitIgnoredFiles: true,
// Worktree path pattern (default: "../{repo}-{branch}")
// Variables: {repo}, {branch}, {parent}
worktreePath: "../{repo}-{branch}",
// Plugins to load
plugins: [],
// Hooks - can be commands, functions, or arrays of both
hooks: {
// Run npm install after creating a worktree
afterCreate: "npm install",
// Run tests before merging
beforeMerge: "npm test",
},
} satisfies WtConfig;Customize where worktrees are created using these variables:
{repo}- Repository name{branch}- Branch name{parent}- Parent directory of the repository root
Examples:
// Default: ../my-project-feature-auth
worktreePath: '../{repo}-{branch}',
// All worktrees in a dedicated folder: ../worktrees/feature-auth
worktreePath: '../worktrees/{branch}',
// In parent's parent directory: ../../my-project-feature-auth
worktreePath: '../../{repo}-{branch}',You can also use wt.config.js or wt.config.mjs.
Hooks support three formats:
// 1. Shell command string
afterCreate: 'npm install',
// 2. Function
afterCreate: async (ctx) => {
ctx.logger.info('Installing dependencies...');
await ctx.exec('npm install');
},
// 3. Array of commands and/or functions (run in sequence)
afterCreate: [
'npm install',
'npm run db:migrate',
async (ctx) => ctx.logger.success('Ready!'),
],Commands run in the worktree directory and log their output. If a command fails, subsequent items in the array are skipped, but other hooks continue.
Plugins can hook into the worktree lifecycle to perform custom actions.
| Hook | When it runs |
|---|---|
beforeCreate |
Before creating a new worktree |
afterCreate |
After worktree is created and files are copied |
beforeMerge |
Before merging into the default branch |
afterMerge |
After successful merge |
beforeRemove |
Before removing a worktree |
afterRemove |
After worktree and branch are removed |
Every hook receives a context object:
interface HookContext {
repoRoot: string; // Git repository root
defaultBranch: string; // e.g., 'main' or 'master'
branchName: string; // Branch being operated on
worktreePath: string; // Path to the worktree
sourceWorktree?: string; // Source worktree (for create)
targetWorktree?: string; // Target worktree (for merge)
logger: Logger; // Logging utilities
exec: (cmd: string) => Promise<ExecResult>; // Run shell commands
}import type { WtPlugin } from "@jescalan/wt/types";
export function myPlugin(options = {}): WtPlugin {
return {
name: "my-plugin",
hooks: {
afterCreate: async (ctx) => {
ctx.logger.info("Worktree created!");
// Run a command
const result = await ctx.exec("npm install", { cwd: ctx.worktreePath });
if (result.exitCode !== 0) {
ctx.logger.warn("npm install failed");
}
},
beforeRemove: async (ctx) => {
ctx.logger.info(`Cleaning up ${ctx.branchName}`);
},
},
};
}// wt.config.ts
import { myPlugin } from "./plugins/my-plugin";
export default {
plugins: [
myPlugin({
/* options */
}),
],
};Automatically create/delete Neon database branches with worktrees:
// wt.config.ts
import { neonPlugin } from "@jescalan/wt/plugins/neon";
export default {
plugins: [
neonPlugin({
projectIdEnvVar: "NEON_PROJECT_ID", // env var or .env key
envFile: ".env", // path to .env file
parentBranch: "current", // 'main', 'current', or branch name
}),
],
};The plugin will:
- Create a Neon branch when you create a worktree
- Update
DATABASE_URLin the new worktree's.env - Delete the Neon branch when you merge/remove the worktree
Parent branch options:
'main'(default): Branch from Neon's primary branch'current': Branch from the current git branch's Neon branch (inherits migrations/data)'branch-name': Branch from a specific named Neon branch
Requires the Neon CLI to be installed.
Automatically create/delete PlanetScale database branches with worktrees:
// wt.config.ts
import { planetscalePlugin } from "@jescalan/wt/plugins/planetscale";
export default {
plugins: [
planetscalePlugin({
databaseEnvVar: "PLANETSCALE_DATABASE", // env var or .env key for database name
orgEnvVar: "PLANETSCALE_ORG", // env var or .env key for org (optional)
envFile: ".env", // path to .env file
parentBranch: "current", // 'main', 'current', or branch name
}),
],
};The plugin will:
- Create a PlanetScale branch when you create a worktree
- Generate a password and update
DATABASE_URLin the new worktree's.env - Delete the PlanetScale branch when you merge/remove the worktree
Parent branch options:
'main'(default): Branch from PlanetScale's primary branch'current': Branch from the current git branch's PlanetScale branch (inherits schema/data)'branch-name': Branch from a specific named PlanetScale branch
Requires the PlanetScale CLI to be installed and authenticated.
Automatically update Codex session paths when worktrees are removed:
// wt.config.ts
import { codexPlugin } from "@jescalan/wt/plugins/codex";
export default {
plugins: [
codexPlugin({
codexHome: "~/.codex", // optional, defaults to $CODEX_HOME or ~/.codex
}),
],
};Automatically migrate Claude Code sessions when worktrees are removed:
// wt.config.ts
import { claudePlugin } from "@jescalan/wt/plugins/claude";
export default {
plugins: [
claudePlugin({
claudeHome: "~/.claude", // optional, defaults to ~/.claude
}),
],
};Claude Code stores conversation history in ~/.claude/projects/ using encoded paths as directory names (e.g., /Users/jeff/Sites/wt becomes -Users-jeff-Sites-wt). When you remove a worktree, those sessions would become orphaned.
This plugin moves session files from the worktree's project directory to the target worktree's directory, preserving your conversation history. If there are filename collisions, the migrated files are renamed with a branch suffix.
- Git 2.5+ (for worktree support)
- Node.js 18+
# Install dependencies
bun install
# Build
bun run build
# Run tests
bun test
# Type check
bun run typecheckMIT