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.
- Forge Integration: Fetch and display associated Pull Request (GitHub) or Merge Request (GitLab) status, including CI check results (via
ghorglabCLI). - Create from PR/MR: Create worktrees directly from open pull/merge requests via command palette.
- Status at a Glance: View dirty state, ahead/behind counts, and divergence from main.
- Tmux Integration: Create and manage tmux sessions per worktree with multi-window support.
- Diff Viewer: View diff with optional delta support.
- Repo Automation:
.wtinit/terminate commands with TOFU security. - LazyGit Integration: Launch lazygit directly for the selected worktree.
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. (highly recommended)
- lazygit: For full TUI git control.
- tmux: For TMUX integration support.
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. If you set show_output, lazyworktree pipes the command output through the configured pager.
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: Editor
show_help: true
s:
command: zsh
description: Shell
show_help: true
T: # Run tests and wait for keypress
command: make test
description: Run tests
show_help: false
wait: true
o: # Show output in the pager
command: git status -sb
description: Status
show_help: true
show_output: 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) |
show_output |
bool | false |
Run non-interactively and show stdout/stderr in the pager (ignores wait) |
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
pager: "less --use-color --wordwrap -qcR -P 'Press q to exit..'"
delta_args:
- --syntax-theme
- Dracula
trust_mode: "tofu" # Options: "tofu" (default), "never", "always"
merge_method: "rebase" # Options: "rebase" (default), "merge"
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. - Use
lazyworktree --theme <name>to pick a UI theme directly; the supported names match the ones listed above. 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.pagersets the pager forshow_outputcommands (default:$PAGER, fallbackless --use-color --wordwrap -qcR -P 'Press q to exit..', thenmore, thencat). When the pager isless, lazyworktree setsLESS=andLESSHISTFILE=-to ignore user defaults.merge_methodcontrols how the "Absorb worktree" action integrates changes into main:rebase(default) rebases the feature branch onto main then fast-forwards,mergecreates a merge commit.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. |