README
ΒΆ
lazyworktree - Lazy Git Worktree Manager
A Bubble Tea-based TUI for managing Git worktrees efficiently. Visualize status, manage branches, and jump between worktrees with ease.
Features
- Worktree Management: Create, rename, delete, absorb, and prune merged worktrees.
- Base Selection: Pick a base branch or commit from a list (or enter a ref) when creating a worktree.
- Create from PR/MR: Create worktrees directly from open pull/merge requests via command palette.
- Command Palette: Fuzzy search and run actions quickly.
- Status at a Glance: View dirty state, ahead/behind counts, and divergence from main.
- Forge Integration: Fetch and display associated Pull Request (GitHub) or Merge Request (GitLab) status, including CI check results (via
ghorglabCLI). - Diff Viewer: Three-part diff with optional delta support and a full-screen viewer.
- Commit Details: Open commit metadata and diffs directly from the log pane.
- Repo Automation:
.wtinit/terminate commands with TOFU security. - LazyGit Integration: Launch
lazygitdirectly for the selected worktree. - Shell Integration: Jump (cd) directly to selected worktrees upon exit.
- Mouse Support: Full mouse support for scrolling and clicking to select items and focus panes.
Screenshots
Prerequisites
- Go: 1.25+ (for building from source)
- Git: 2.31+ (recommended)
- Forge CLI: GitHub CLI (
gh) or GitLab CLI (glab) for repo resolution and PR/MR status.
Optional:
- delta: For syntax-highlighted diffs.
- lazygit: For full TUI git control.
Installation
From Source
Clone the repository and build:
git clone https://github.com/chmouel/lazyworktree.git
cd lazyworktree
go build -o lazyworktree ./cmd/lazyworktree
Install to your PATH:
go install ./cmd/lazyworktree
Or build and run directly:
go run ./cmd/lazyworktree/main.go
You can override the default worktree root:
lazyworktree --worktree-dir ~/worktrees
Pre-built Binaries
Pre-built binaries for various platforms are available in the Releases section.
πΊ Homebrew
brew tap chmouel/lazyworktree https://github.com/chmouel/lazyworktree
brew install lazyworktree
Arch
yay -S lazyworktree-bin
Shell Integration (Zsh)
To enable the "jump" functionality (changing your shell's current directory on exit), add the helper functions from shell/functions.shell to your .zshrc. The helper uses --output-selection to write the selected path to a temp file.
Example configuration:
# Add to .zshrc
source /path/to/lazyworktree/shell/functions.shell
# Create an alias for a specific repository
# worktree storage key is derived from the origin remote (e.g. github.com:owner/repo)
# and falls back to the directory basename when no remote is set.
pm() { worktree_jump ~/path/to/your/main/repo "$@"; }
Now you can run pm to open the TUI, select a worktree, and upon pressing Enter, your shell will cd into that directory.
You can also jump directly to a worktree by name and enable completion:
pm() { worktree_jump ~/path/to/your/main/repo "$@"; }
_pm() { _worktree_jump ~/path/to/your/main/repo; }
compdef _pm pm
If you want a shortcut to the last-selected worktree, use the built-in
worktree_go_last helper (reads the .last-selected file):
alias pl='worktree_go_last ~/path/to/your/main/repo'
Custom Initialization and Termination
You can create a .wt file in your main repository to define custom commands to run when creating or removing a worktree. This format is inspired by wt.
Example .wt configuration
init_commands:
- link_topsymlinks
- cp $MAIN_WORKTREE_PATH/.env $WORKTREE_PATH/.env
- npm install
- code .
terminate_commands:
- echo "Cleaning up $WORKTREE_NAME"
The following environment variables are available to your commands:
WORKTREE_BRANCH: Name of the git branch.MAIN_WORKTREE_PATH: Path to the main repository.WORKTREE_PATH: Path to the new worktree being created or removed.WORKTREE_NAME: Name of the worktree (directory name).
Security: Trust on First Use (TOFU)
Since .wt files allow executing arbitrary commands found in a repository, lazyworktree implements a Trust on First Use security model to prevent malicious repositories from running code on your machine automatically.
- First Run: When
lazyworktreeencounters a new or modified.wtfile, it will pause and display the commands it intends to run. You can Trust (run and save), Block (skip for now), or Cancel the operation. - Trusted: Once trusted, commands run silently in the background until the
.wtfile changes again. - Persistence: Trusted file hashes are stored in
~/.local/share/lazyworktree/trusted.json.
You can configure this behavior in config.yaml via the trust_mode setting:
tofu(Default): Prompts for confirmation on new or changed files. Secure and usable.never: Never runs commands from.wtfiles. Safest for untrusted environments.always: Always runs commands without prompting. Useful for personal/internal environments but risky.
Special Commands
link_topsymlinks: This is a built-in automation command (not a shell command) that runs without TOFU prompts once the.wtfile is trusted. It performs:- Symlinks all untracked and ignored files from the root of the main worktree to the new worktree (excluding subdirectories).
- Symlinks common editor configurations (
.vscode,.idea,.cursor,.claude). - Ensures a
tmp/directory exists in the new worktree. - Automatically runs
direnv allowif a.envrcfile is present.
Custom Commands
You can define custom keybindings in your ~/.config/lazyworktree/config.yaml to execute commands in the selected worktree. Custom commands are executed interactively (the TUI suspends, just like when launching lazygit) and show up in the command palette.
By default, t opens a tmux session with a single shell window. You can override it by defining custom_commands.t.
When attach is true, lazyworktree attaches to the session immediately. When attach is false, it shows an info modal with instructions to attach manually.
Configuration Format
Add a custom_commands section to your config:
custom_commands:
e:
command: nvim
description: Open editor
show_help: true
s:
command: zsh
description: Open shell
show_help: true
t: # Run tests and wait for keypress
command: make test
description: Run tests
show_help: false
wait: true
a: # Open CLaude CLI in the selected workspace in a new kitty tab
command: "kitten @ launch --type tab --cwd $WORKTREE_PATH -- claude"
description: Open Claude
show_help: true
t: # Open a tmux session with multiple windows
description: Open tmux
show_help: true
tmux:
session_name: "${REPO_NAME}_wt_$WORKTREE_NAME"
attach: true
on_exists: switch
windows:
- name: claude
command: claude
- name: shell
command: zsh
- name: lazygit
command: lazygit
Field Reference
| Field | Type | Default | Description |
|---|---|---|---|
command |
string | required | The command to execute |
description |
string | "" |
Description shown in the help screen and command palette |
show_help |
bool | false |
Whether to show this command in the help screen (?) and footer hints |
wait |
bool | false |
Wait for key press after command completes (useful for quick commands like ls or make test) |
tmux |
object | null |
Configure a tmux session instead of executing a single command |
tmux fields
| Field | Type | Default | Description |
|---|---|---|---|
session_name |
string | ${REPO_NAME}_wt_$WORKTREE_NAME |
tmux session name (supports env vars) |
attach |
bool | true |
If true, attach/switch immediately; if false, show info modal with attach instructions |
on_exists |
string | switch |
Behavior if session exists: switch, attach, kill, new |
windows |
list | [ { name: "shell" } ] |
Window definitions for the session |
If windows is omitted or empty, lazyworktree creates a single shell window.
tmux window fields
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | window-N |
Window name (supports env vars) |
command |
string | "" |
Command to run in the window (empty uses your default shell) |
cwd |
string | $WORKTREE_PATH |
Working directory for the window (supports env vars) |
Environment Variables
Custom commands have access to the same environment variables as init/terminate commands:
WORKTREE_BRANCH: Name of the git branchMAIN_WORKTREE_PATH: Path to the main repositoryWORKTREE_PATH: Path to the selected worktreeWORKTREE_NAME: Name of the worktree (directory name)REPO_NAME: Name of the repository (from GitHub/GitLab)
Supported Key Formats
Custom commands support the same key formats as built-in keybindings:
- Single keys:
e,s,t,l, etc. - Modifier combinations:
ctrl+e,ctrl+t,alt+s, etc. - Special keys:
enter,esc,tab,space, etc.
Examples:
custom_commands:
"ctrl+e":
command: nvim
description: Open editor with Ctrl+E
"alt+t":
command: make test
description: Run tests with Alt+T
wait: true
Key Precedence
Custom commands take precedence over built-in keys. If you define a custom command with key s, it will override the built-in sort toggle. This allows you to fully customize your workflow.
Key Bindings
| Key | Action |
|---|---|
Enter |
Jump to worktree (exit and cd) |
c |
Create new worktree |
m |
Rename selected worktree |
D |
Delete selected worktree |
d |
View diff (auto-refreshes) |
F |
Full-screen diff viewer |
A |
Absorb worktree into main |
X |
Prune merged worktrees |
p |
Fetch PR/MR status (also refreshes CI checks) |
o |
Open PR/MR in browser |
ctrl+p, P |
Command palette |
g |
Open LazyGit |
r |
Refresh list |
R |
Fetch all remotes |
f, / |
Filter worktrees |
s |
Toggle sort (Name/Last Active) |
? |
Show help |
Command Palette Actions:
- Create from PR/MR: Select an open PR/MR to create a worktree. Auto-generates a name (
pr{number}-{sanitized-title}) that you can edit. - Create from changes: Create a new worktree from the current uncommitted changes in the selected worktree. Stashes all changes (including untracked files), creates a new worktree, and applies the stashed changes to it. Requires a worktree to be selected and have uncommitted changes.
Mouse Controls
- Click: Select and focus panes or items
- Scroll Wheel: Scroll through lists and content
- Worktree table (left pane)
- Info/Diff viewer (right top pane)
- Log table (right bottom pane)
Configuration
Worktrees are expected to be organized under
~/.local/share/worktrees/<repo_name> by default, though the script attempts
to resolve locations via gh repo view or glab repo view. If the repo name
cannot be detected, lazyworktree falls back to a local local-<hash> key for
cache and last-selected storage.
Global Config (YAML)
lazyworktree reads ~/.config/lazyworktree/config.yaml (or .yml) for default
settings. Example (also in config.example.yaml):
worktree_dir: ~/.local/share/worktrees
sort_by_active: true
auto_fetch_prs: false
max_untracked_diffs: 10
max_diff_chars: 200000
theme: dracula # Options: "dracula" (default), "narna", "clean-light", "solarized-dark",
# "solarized-light", "gruvbox-dark", "gruvbox-light",
# "nord", "monokai", "catppuccin-mocha"
delta_path: delta
delta_args:
- --syntax-theme
- Dracula
trust_mode: "tofu" # Options: "tofu" (default), "never", "always"
init_commands:
- link_topsymlinks
terminate_commands:
- echo "Cleaning up $WORKTREE_NAME"
custom_commands:
e:
command: nvim
description: Open editor
show_help: true
wait: false
Notes:
--worktree-diroverridesworktree_dir.themeselects the color theme. Available themes:dracula,narna,clean-light,solarized-dark,solarized-light,gruvbox-dark,gruvbox-light,nord,monokai,catppuccin-mocha. Default:dracula.init_commandsandterminate_commandsrun before any repo-specific.wtcommands (if present).- Set
sort_by_activetofalseto sort by path. - Set
auto_fetch_prstotrueto fetch PR data on startup. - Use
max_untracked_diffs: 0to hide untracked diffs;max_diff_chars: 0disables truncation. - Run
lazyworktree --show-syntax-themesto print the default delta--syntax-themevalues for each UI theme. delta_argssets arguments passed todelta(defaults follow the UI theme: Dracula βDracula, Narna βOneHalfDark, Clean-Light βGitHub, Solarized Dark βSolarized (dark), Solarized Light βSolarized (light), Gruvbox Dark βGruvbox Dark, Gruvbox Light βGruvbox Light, Nord βNord, Monokai βMonokai Extended, Catppuccin Mocha βCatppuccin Mocha).delta_pathsets path to delta executable (default:delta). Set to empty string to disable delta and use plain git diff output.branch_name_scriptruns a script to generate branch name suggestions when creating worktrees from changes. The script receives the git diff on stdin and should output a branch name. See AI-powered branch names below.
Themes
lazyworktree includes built-in themes:
| Theme | Background | Best For |
|---|---|---|
| dracula | Dark (#282A36) | Dark terminals, vibrant colors, default |
| narna | Charcoal (#0D1117) | Dark terminals, blue highlights |
| clean-light | White (#FFFFFF) | Light terminals, soft colors |
| solarized-dark | Deep teal (#002B36) | Classic Solarized dark palette |
| solarized-light | Cream (#FDF6E3) | Classic Solarized light palette |
| gruvbox-dark | Dark gray (#282828) | Gruvbox dark, warm accents |
| gruvbox-light | Sand (#FBF1C7) | Gruvbox light, earthy tones |
| nord | Midnight blue (#2E3440) | Nord calm cyan accents |
| monokai | Olive black (#272822) | Monokai bright neon accents |
| catppuccin-mocha | Mocha (#1E1E2E) | Catppuccin Mocha pastels |
Select a theme in your config file:
theme: dracula # or any listed above
CI Status Display
When viewing a worktree with an associated PR/MR, lazyworktree automatically fetches and displays CI check statuses in the info pane:
βGreen - PassedβRed - FailedβYellow - Pending/RunningβGray - SkippedβGray - Cancelled
CI status is fetched lazily (only for the selected worktree) and cached for 30 seconds to keep the UI snappy. Press p to force a refresh of CI status.
AI-Powered Branch Names
When creating a worktree from changes (via the command palette), you can configure an external script to suggest branch names. The script receives the git diff on stdin and should output a single branch name.
This is useful for integrating AI tools like aichat, claude, or any other CLI tool that can generate meaningful branch names from code changes.
Configuration
Add branch_name_script to your ~/.config/lazyworktree/config.yaml:
# Using aichat with Gemini
branch_name_script: "aichat -m gemini:gemini-2.5-flash-lite 'Generate a short git branch name (no spaces, use hyphens) for this diff. Output only the branch name, nothing else.'"
How It Works
- When you select "Create from changes" in the command palette
- If
branch_name_scriptis configured, the current diff is piped to the script - The script's output (first line only) is used as the suggested branch name
- You can edit the suggestion before confirming
Script Requirements
- The script receives the git diff on stdin
- It should output just the branch name (first line is used)
- If the script fails or returns empty, the default name (
{current-branch}-changes) is used - The script has a 30-second timeout to prevent hanging
Speed performance
lazyworktree is designed to be super snappy:
- Caching: It caches worktree metadata in
.worktree-cache.jsonunder<worktree_dir>/<repo_key>/;repo_keyis the remoteowner/repowhen available, otherwise a locallocal-<hash>key derived from the repo path. - Background Updates: As soon as the UI is visible, a background task refreshes the data from Git and updates the cache automatically.
- Welcome Screen: If no worktrees are detected (e.g., during first-time use or in an unconfigured directory), a welcome screen guides you through the setup.
Trivia
This used to be a python textual application, but the startup-time was too slow and I have decided to move it to a go charmbracelet bubble based TUI. You can still see or try if you want the old python interface here https://github.com/chmouel/lazyworktree/tree/python
Copyright
Authors
Chmouel Boudjnah
- π Fediverse - <@chmouel@chmouel.com>
- π¦ Twitter - <@chmouel>
- π Blog - <https://blog.chmouel.com>
Directories
ΒΆ
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
lazyworktree
command
Package main is the entry point for the lazyworktree application.
|
Package main is the entry point for the lazyworktree application. |
|
internal
|
|
|
app
Package app provides the main application UI and logic using Bubble Tea.
|
Package app provides the main application UI and logic using Bubble Tea. |
|
commands
Package commands provides utility helpers for workspace-related shell commands.
|
Package commands provides utility helpers for workspace-related shell commands. |
|
config
Package config loads application and repository configuration from YAML.
|
Package config loads application and repository configuration from YAML. |
|
git
Package git wraps git commands and helpers used by lazyworktree.
|
Package git wraps git commands and helpers used by lazyworktree. |
|
models
Package models defines the data objects shared across lazyworktree packages.
|
Package models defines the data objects shared across lazyworktree packages. |
|
security
Package security manages trust decisions and persistence for repository config files.
|
Package security manages trust decisions and persistence for repository config files. |
|
theme
Package theme provides theme definitions and management for the TUI.
|
Package theme provides theme definitions and management for the TUI. |