A CLI tool that builds dynamic monorepo workspaces from independent repositories using git worktree.
Monorepo is a popular approach for managing related codebases, but it's not always the right answer:
- Too much context hurts AI performance. When you ask an AI to optimize the "cancel order" UX, it may confuse the customer-facing mobile interaction with a same-named feature in the merchant back-office — simply because both live in the same repository. A narrower, task-specific context leads to better results.
- Some situations objectively call for separate repositories. Migrating the remaining 5% of useful code from a legacy codebase into a modern repo doesn't mean you should dump 100% of the legacy code in first and let AI clean it up. That's messy. Keeping repos separate and pulling only what you need is the cleaner path.
- Monorepo boundaries are never universal. For a small company, "everything in one repo" makes sense. In a large organization with hundreds of teams and services, no single monorepo can realistically encompass all the code you might need to touch for a given task.
wtp takes a different approach: the dynamic monorepo.
Your repositories stay independent — they have their own history, their own CI, their own ownership. But when a task requires working across multiple repos simultaneously, wtp assembles them into a unified workspace on the fly. When the task is done, the workspace is dissolved. No permanent coupling, no structural compromise.
This gives you:
- Task-scoped context — only the repos relevant to your current work, nothing more
- Zero migration cost — no need to restructure existing repositories
- Flexible boundaries — workspaces can span teams, orgs, or even Git hosting platforms
- Full git compatibility — built on git worktree, every repo stays a normal git repo
- Rust 1.90+ (for Rust 2024 edition support)
- Git 2.30+
git clone https://github.com/eddix/wtp
cd wtp
cargo install --path wtp-cli# Create a new workspace for your feature
wtp create feature-x
# Switch your current repository to the workspace
# This creates a new worktree and branch named "feature-x"
cd ~/projects/my-repo
wtp switch feature-x
# Add another repository to the same workspace
cd ~/projects/another-repo
wtp switch feature-x
# Jump to workspace directory (requires shell integration, see below)
wtp cd feature-x
# Or import a repo while inside a workspace directory
cd ~/.wtp/workspaces/feature-x
wtp import company/project
# See all worktrees in the workspace
wtp statusTo enable wtp cd, add the following to your .zshrc or .bashrc:
eval "$(wtp shell-init)"How it works:
- The shell wrapper creates a temporary file and sets
WTP_DIRECTIVE_FILE wtp cdwrites acd '/path/to/workspace'command to this file- After wtp exits, the wrapper sources this file, changing the parent shell's directory
Generate tab-completion scripts with wtp completions:
# Zsh (add to .zshrc)
eval "$(wtp completions zsh)"
# Bash (add to .bashrc)
eval "$(wtp completions bash)"
# Fish (add to config.fish)
wtp completions fish | sourceCompletions include subcommands, flags, workspace names (dynamic), host aliases (dynamic), and file paths where applicable.
wtp searches for configuration in the following order (first existing file wins):
~/.wtp.toml~/.wtp/config.toml~/.config/wtp/config.toml
If multiple config files exist, a warning is displayed showing which file is being used.
# Workspace settings
workspace_root = "~/.wtp/workspaces" # Default location for all workspaces
# Host aliases - map short names to code roots
[hosts.gh]
root = "~/codes/github.com"
[hosts.gl]
root = "~/codes/gitlab.company.internal"
[hosts.bb]
root = "~/codes/bitbucket.org"
# Default host to use when none specified
default_host = "gh"
# Display preferences
[display]
# Colorize repo names in `wtp ls --long`, giving each repo a stable color
# derived from its name (so the same repo is always the same color).
# "auto" - color when the output is a terminal (default)
# "always" - color even when piped/redirected
# "never" - disable repo colors
repo_colors = "auto"In wtp ls --long, each repo name is tinted with a stable color hashed from its
name. The same repo always gets the same color across workspaces, so the few
repos you work with most become easy to pick out at a glance. Control it with
[display].repo_colors (auto / always / never); colors are stripped
automatically when output isn't a terminal or when NO_COLOR is set.
wtp supports running custom scripts on workspace lifecycle events. This is useful for initializing workspaces with standard configurations, tools, or documentation.
The on_create hook runs after a new workspace is created. Configure it in your config file:
[hooks]
on_create = "~/.wtp/hooks/on-create.sh"The hook script receives these environment variables:
| Variable | Description | Example |
|---|---|---|
WTP_WORKSPACE_NAME |
Name of the created workspace | my-feature |
WTP_WORKSPACE_PATH |
Full path to the workspace directory | /home/user/.wtp/workspaces/my-feature |
Example hook script (~/.wtp/hooks/on-create.sh):
#!/bin/bash
echo "Initializing workspace: $WTP_WORKSPACE_NAME"
# Drop a CLAUDE.md / AGENTS.md so coding agents recognize the workspace
# as the root of work instead of drifting into a single sub-worktree.
cat > "$WTP_WORKSPACE_PATH/CLAUDE.md" <<EOF
# wtp workspace: $WTP_WORKSPACE_NAME
This directory is a **wtp workspace** — each subdirectory is an
independent \`git worktree\` checked out for the same task. Treat this
directory as the root of work; \`cd\` into a subdirectory only when
running repo-specific commands.
EOF
# Mirror the same context for Codex / other agents.
cp "$WTP_WORKSPACE_PATH/CLAUDE.md" "$WTP_WORKSPACE_PATH/AGENTS.md"
# Copy spec coding config (example)
# cp ~/.templates/spec-coding.toml "$WTP_WORKSPACE_PATH/.spec.toml"
echo "✅ Workspace initialized!"Make the script executable:
chmod +x ~/.wtp/hooks/on-create.shNotes:
- Use
wtp create <name> --no-hookto skip the hook - Hook failures don't block workspace creation (a warning is shown)
- Hook stdout is displayed to the terminal
- On Unix, the script must have execute permissions
# List all workspaces
wtp ls
# Detailed listing (shows repo branches and status)
wtp ls --long
# Names only, one per line (useful for scripts and shell completion)
wtp ls --short
# Only workspaces that contain a repo whose name matches a pattern
# (case-insensitive substring). Handy for finding every workspace
# touching a given repo namespace, e.g. all i18n_* repos.
wtp ls --grep i18n
# Combine with any output format
wtp ls --long --grep i18nOutput:
main
feature-x
hotfix-123
All workspaces are stored under workspace_root (default: ~/.wtp/workspaces).
wtp create my-feature
# Skip the on_create hook (if configured)
wtp create my-feature --no-hookCreates a new workspace directory at <workspace_root>/<NAME> and registers it in the global config. If an on_create hook is configured, it will be executed after the workspace is created.
Note: The command outputs the path but cannot change your shell's directory. You'll need to cd manually:
cd $(wtp create my-feature 2>&1 | grep "Created" | awk '{print $NF}')# Remove workspace (ejects all worktrees first)
wtp rm my-feature
# Force removal even if worktrees have uncommitted changes
wtp rm my-feature --forceThis command:
- Ejects all worktrees via
git worktree remove(one by one, with progress) - Checks for leftover files in the workspace directory
- Removes the workspace directory if clean, or requires
--forcefor extra files
If any worktree has uncommitted changes, the command will stop and list them (unless --force is used).
Import an external git repository into the workspace you're currently in. You must cd into a workspace directory first. Both normal and bare git repositories are supported.
# Import a repo using the default host (if configured)
cd ~/.wtp/workspaces/feature-x
wtp import company/project
# Import with explicit host alias
wtp import company/project -H gh
# Import with full repo path
wtp import --repo ~/projects/my-repo
# Specify branch name (defaults to workspace name)
wtp import company/project -b feature-xyz
# Specify base for new branch
wtp import company/project -B main
# Stack a new layer on the branch you're standing on (see Stacked
# Worktrees below). Run inside a worktree directory: PATH is inferred
wtp import -b feature-xyz-2 --parent feature-xyz
# Interactive mode: run with no arguments to fuzzy-select a repo
wtp importRepository Path Resolution:
- If
--repois provided: uses the absolute path directly - If
-H/--hostis provided:<host_root>/<path> - If
default_hostis configured: uses that host - Otherwise: treats as absolute/relative filesystem path
Workspace Resolution:
The command detects the workspace from your current directory — it walks up the directory tree looking for a .wtp directory. If you're not inside a workspace, you'll get an error.
Remove a repository's worktree from the current workspace via git worktree remove.
# Eject a specific repository (must be inside a workspace directory)
wtp eject my-repo
# Force eject even if worktree has uncommitted changes
wtp eject my-repo --force
# Interactive mode: run with no arguments to select from workspace repos
wtp ejectThe command detects the workspace from your current directory. After ejecting, the worktree record is removed from .wtp/worktree.toml.
Cascade-rebase stacked worktrees (see Stacked Worktrees) after a lower layer moved.
# Inside a worktree directory: restack that worktree's whole chain
cd ~/.wtp/workspaces/feature-x/project@feature-xyz-2
wtp restack
# From the workspace root: restack every chain in the workspace
cd ~/.wtp/workspaces/feature-x
wtp restackBehavior:
- Fail-fast preflight: refuses to start if any layer has uncommitted changes, a rebase in progress, a missing directory, or an unresolvable parent — all problems are listed at once
- Each layer runs
git rebase --onto <parent> <fork_point>in its own worktree, replaying only that layer's own commits — a squash-merged bottom layer transplants cleanly - Stateless and idempotent: layers already based on their parent are
skipped; on conflict the run stops with the worktree path, conflicted
files, and instructions — resolve,
git rebase --continue, then re-runwtp restackand it continues where it left off - wtp never pushes: rewritten branches are listed at the end with
git push --force-with-leasetemplates
Metadata-only: rewires the stack edge and leaves git history untouched.
Typical use: the bottom PR of a stack merged, so its child now belongs on
main.
# Inside a worktree directory: retarget the layer you're standing in
wtp retarget main
# From anywhere in the workspace: name the worktree explicitly
wtp retarget project@feature-xyz-2 main
# Then apply the change
wtp restackSelf-parenting, unknown refs, and cycles are rejected. The recorded fork point is preserved, which is exactly what makes the post-squash-merge restack clean.
Add the current git repository to a workspace by creating a worktree.
# Switch current repo to an existing workspace
wtp switch my-feature
# Switch and create workspace if it doesn't exist
wtp switch --create my-feature
# With specific branch name
wtp switch my-feature --branch custom-branch
# With specific base
wtp switch my-feature --base developThis command:
- Detects the current git repository
- Creates/uses the specified branch
- Creates a worktree in the target workspace
- Records the worktree in the workspace's metadata
Workspace Handling:
- Without
--create: workspace must already exist - With
--create: creates workspace if it doesn't exist
Host Matching: When recording the worktree, wtp tries to match the repository path against configured host aliases to store a relative reference instead of an absolute path.
Shows the status of all worktrees in the current workspace. Must be run from within a workspace directory.
# Show status of current workspace
wtp status
# Detailed status (includes remote tracking, last commit info)
wtp status --long# Jump to a workspace directory
wtp cd my-featureNote: This command requires shell integration. Without it, you'll see:
Error: wtp cd requires shell integration
Host aliases map short names to code root directories, making it easier to reference repositories.
# Add a host alias
wtp host add gh ~/codes/github.com
# List configured hosts
wtp host ls
# Set default host
wtp host set-default gh
# Remove a host alias
wtp host rm gh
# Unset default host
wtp host set-default noneExample workflow:
# 1. Add host aliases
wtp host add gh ~/codes/github.com
wtp host add gl ~/codes/gitlab.company.com
# 2. Set default
wtp host set-default gh
# 3. Now you can use short paths
wtp import mycompany/project
# Resolves to: ~/codes/github.com/mycompany/projectConfiguration:
Hosts are stored in ~/.wtp/config.toml:
[hosts.gh]
root = "/home/user/codes/github.com"
[hosts.gl]
root = "/home/user/codes/gitlab.company.com"
default_host = "gh"wtp includes a fence mechanism that prevents accidental file operations outside the workspace_root. If you attempt to:
- Create a workspace outside
workspace_root - Import/switch to a workspace that's outside the boundary
You'll see a warning like:
⚠️ Warning: Workspace 'xxx' is outside workspace_root: /Users/you/.wtp/workspaces
Target path: /some/outside/path
Are you sure you want to proceed? [y/N]
This protects your system files from accidental modification by wtp commands.
A workspace is a logical collection of worktrees from different repositories. All workspaces are stored under workspace_root (default: ~/.wtp/workspaces).
Each workspace contains:
.wtp/worktree.toml- Metadata about all worktrees- Subdirectories for each repository's worktrees
Worktrees are organized as:
<workspace_root>/<workspace_name>/<repo_slug>/
For example:
~/.wtp/workspaces/feature-x/
├── my-project/ # worktree for "my-project" repo
└── another-project/ # worktree for "another-project" repo
Constraints:
- By default, each repository has one worktree per workspace, named after the repository slug (last component of the repo path)
- If you try to add a duplicate, you'll get an error like:
Error: Repository 'my-project' is already in this workspace with branch 'feature-x'. To add another branch of the same repository, re-run with -b <branch> and --with-branch-name.
To work on several branches of one repository side by side (e.g. applying the
same change to multiple release branches), pass --with-branch-name to
wtp import or wtp switch. The worktree directory is then named
<repo_slug>@<branch>:
wtp import company/project -b release-area-a-dev # project/
wtp import company/project -b release-area-b-dev --with-branch-name # project@release-area-b-dev/~/.wtp/workspaces/releases/
├── project/ # branch: release-area-a-dev
└── project@release-area-b-dev/ # branch: release-area-b-dev
Notes:
- Branch name separators like
/are sanitized in the directory name (feature/x→project@feature_x) wtp ejectaccepts the worktree directory name to pick an exact worktree (e.g.wtp eject project@release-area-b-dev); a bare repo slug that matches several worktrees is rejected as ambiguous- All worktrees of one repository share the same git refs, stash, and config —
this is standard
git worktreebehavior
Beyond working across repositories (horizontal), wtp supports stacked-PR style development within one repository (vertical): a chain of dependent branches, each in its own worktree directory.
cd ~/.wtp/workspaces/feature-x/project # standing on feat-1
wtp import -b feat-2 --parent feat-1 # stack feat-2 on top
cd ../project@feat-2
wtp import -b feat-3 --parent feat-2 # and feat-3 above that~/.wtp/workspaces/feature-x/
├── project/ # feat-1 (bottom of the stack)
├── project@feat-2/ # feat-2, parent: feat-1
└── project@feat-3/ # feat-3, parent: feat-2
wtp status renders the chain as a tree with each layer's divergence from
its parent (↑ commits of its own, ↓ commits it needs restacked in):
REPOSITORY BRANCH STATUS
project feat-1 ✓ clean
project └ feat-2 ↑2 ✓ clean
project └ feat-3 ↑1 ✓ clean
What each piece does:
--parentrecords the stack edge plus a fork point (the parent commit the layer was cut from) in.wtp/worktree.tomlwtp restackcascade-rebases layers onto their parents, replaying only each layer's own commitswtp retargetrewires an edge after the bottom of the stack lands- The parent can be any ref, not just another layer: point the bottom
layer at
origin/mainto track trunk, or retarget an orphaned layer atmainafter its parent branch merged and was deleted
Why one directory per layer beats switching branches in a single checkout
(git rebase --update-refs, Graphite): while the bottom layer waits for
review you keep working on upper layers, and when review feedback arrives
you just cd down — no stashing, no context switching. Each layer can
also host its own coding agent.
The full stacked workflow (design and rationale):
docs/design/stacked-worktree.md.
wtp deliberately never touches the forge — no PR creation, no PR-base
retargeting, no pushes. Pair it with gh/glab or your forge's CLI.
Host aliases let you use short names instead of full paths:
[hosts.gh]
root = "~/codes/github.com"Now instead of:
wtp import ~/codes/github.com/company/projectYou can use:
wtp import company/project -H gh
# or with default_host configured:
wtp import company/projectBy default, wtp import and wtp switch create branches named after the workspace:
wtp create feature-x
wtp switch feature-x # Creates branch "feature-x" from current HEADIf a branch already exists and is not checked out elsewhere:
wtp import company/project -b existing-branchThis will check out the existing branch instead of creating a new one.
Git worktree has a constraint: a branch can only be checked out in one worktree at a time.
If you try to add a branch that's already checked out:
Error: Branch 'feature-x' is already checked out in another worktree: my-project/feature-x
Workarounds:
- Use a different branch name:
wtp import ... -b feature-x-2 - Remove the existing worktree first
- Use a different workspace
Commands like wtp import, wtp eject, and wtp status detect the workspace from your current directory:
Error: Not in a workspace directory.
Run this command from within a workspace directory.
cd into a workspace directory first.
See Branch Conflicts above.
⚠️ Warning: Multiple config files found: ~/.wtp.toml, ~/.wtp/config.toml. Using ~/.wtp.toml
Consolidate your configuration into one file and remove the others.
Common git worktree issues:
- "already checked out" - Branch is in use by another worktree
- "is not a valid repository" - The repository path doesn't exist or isn't a git repo
- "is locked" - A previous git operation was interrupted; may need manual cleanup
cargo build --releasecargo testwtp-core/src/ # Core business logic (UI-independent)
├── lib.rs # Public API exports
├── config.rs # Configuration management
├── error.rs # Error types
├── fence.rs # Security fence
├── git.rs # Git command wrapper
├── workspace.rs # Workspace management
└── worktree.rs # Worktree data models
wtp-cli/src/ # CLI application
├── main.rs # Entry point
└── cli/ # CLI subcommands
├── mod.rs # CLI entry point and help system
├── cd.rs
├── completions.rs # Shell completion generation
├── create.rs
├── eject.rs # Eject a worktree from workspace
├── fuzzy.rs # Fuzzy finder integration
├── git_status_fmt.rs # Git status formatting extension trait
├── host.rs # Host alias management
├── import.rs
├── ls.rs
├── remove.rs
├── shell_init.rs
├── status.rs
├── switch.rs
└── theme.rs # Unified styling for help output
wtp-gui/ # GUI application (scaffold, GPUI-based)
MIT License - see LICENSE for details.
Contributions are welcome! Please feel free to submit a Pull Request.
If you want coding agents to use wtp more consistently, this repository includes agent-facing skill drafts under skills/.
skills/wtp-workspace-operator/SKILL.mdskills/wtp-repo-attach/SKILL.mdskills/wtp-safe-cleanup/SKILL.mdskills/README.md
These files are aimed at tools such as Codex, Claude Code, and Cursor. They describe when agents should use wtp, how to distinguish switch from import, and how to avoid risky cleanup behavior.
Integration notes for each tool live under docs/agent-integration/.