LazyWorktree is a TUI for Git worktrees. It provides a keyboard-driven workflow for creating, inspecting, and navigating worktrees within a repository.
Built with BubbleTea, it focuses on fast iteration, clear state visibility, and tight Git tooling integration.
See other Screenshots below
- Worktree management: create, rename, remove, absorb, and prune merged worktrees.
- Powerful creation options:
- From current branch, optionally with uncommitted changes.
- Checkout existing branch or create a new branch from it.
- From GitHub/GitLab issue with automatic branch naming.
- From open GitHub/GitLab PR or MR.
- View CI logs from GitHub Actions.
- Display linked PR/MR, CI status, and checks.
- Stage, unstage, commit, edit, and diff files.
- View diffs in a pager with optional delta integration.
- Manage per-worktree tmux or zellij sessions.
- Cherry-pick commits between worktrees.
- Command palette with MRU-based navigation.
- Custom commands: define keybindings, tmux/zellij layouts, and per-repo workflows.
- Init/terminate hooks via
.wtfiles with TOFU security. - Shell integration: jump into worktrees and return to the last-used one.
- Automatic branch naming via scripts (e.g., LLM tools).
- Install lazyworktree (see below).
- Run
lazyworktreein a Git repository. - Press
?for help.
Common overrides:
lazyworktree --worktree-dir ~/worktrees
# Override config values via command line
lazyworktree --config lw.theme=nord --config lw.sort_mode=active- Git: 2.31+
- Forge CLI:
ghorglabfor PR/MR status
Optional:
- delta: Syntax-highlighted diffs (recommended)
- lazygit: Full TUI git control
- tmux / zellij: Session management
Build-time only:
- Go 1.25+
Pre-built binaries are available in the Releases.
brew tap chmouel/lazyworktree https://github.com/chmouel/lazyworktree
brew install lazyworktree --caskIf macOS shows "Apple could not verify lazyworktree":
Option 1: System Settings > Privacy & Security > "Open Anyway"
Option 2: Remove quarantine attribute:
xattr -d com.apple.quarantine /opt/homebrew/bin/lazyworktreeyay -S lazyworktree-binDirect installation:
go install github.com/chmouel/lazyworktree/cmd/lazyworktree@latestVia cloning and building:
git clone https://github.com/chmouel/lazyworktree.git
cd lazyworktree
go build -o lazyworktree ./cmd/lazyworktreeShell helpers change directory to the selected worktree on exit. Optional but recommended.
Zsh helpers are in shell/functions.zsh. See ./shell/README.md for details.
Create and delete worktrees from the command line. Legacy aliases wt-create and wt-delete still work.
Create from current branch:
# Auto-generated name from current branch
lazyworktree create
# Explicit name
lazyworktree create my-feature
# With uncommitted changes
lazyworktree create --with-change
# Explicit name + changes
lazyworktree create my-feature --with-changeCreate from a specific branch:
# Explicit name
lazyworktree create --from-branch main my-feature [--with-change] [--silent] [--output-selection /tmp/selection]
# Auto-generated name (sanitised from source branch)
lazyworktree create --from-branch feature/new-feature [--with-change] [--silent] [--output-selection /tmp/selection]Name can be explicit or auto-generated:
lw create my-feature- explicit name from current branchlw create --from-branch main my-feature- explicit name from specific branchlw create- auto-generated from current branchlw create --from-branch feature/cool-thing- creates "feature-cool-thing"lw create --generate- force auto-generation even with positional argument- Names are sanitised to lowercase alphanumeric with hyphens
Create from a PR:
lazyworktree create --from-pr 123 [--silent] [--output-selection /tmp/selection]lazyworktree delete [--no-branch] [--silent]Deletes worktree and branch (if names match). Use --no-branch to skip branch deletion.
| Key | Action |
|---|---|
Enter |
Jump to worktree (exit and cd) |
c |
Create new worktree (from branch, commit, PR/MR, or issue) |
m |
Rename selected worktree |
D |
Delete selected worktree |
d |
View diff in pager (respects pager config) |
A |
Absorb worktree into main |
X |
Prune merged worktrees (refreshes PR data, checks merge status) |
! |
Run arbitrary command in selected worktree (with command history) |
v |
View CI checks (Enter opens in browser, Ctrl+v views logs in pager) |
o |
Open PR/MR in browser (or root repo in editor if main branch with merged/closed/no PR) |
ctrl+p, : |
Command palette |
g |
Open LazyGit |
r |
Refresh list (also refreshes PR/MR/CI for current worktree on GitHub/GitLab) |
R |
Fetch all remotes |
S |
Sync with upstream (pull + push, requires clean worktree) |
P |
Push to upstream (prompts to set upstream if missing) |
f |
Filter focused pane (worktrees, files, commits) |
/ |
Search focused pane (incremental) |
alt+n, alt+p |
Move selection and fill filter input |
β, β |
Move selection (filter active, no fill) |
s |
Cycle sort mode (Path / Last Active / Last Switched) |
Home |
Go to first item in focused pane |
End |
Go to last item in focused pane |
? |
Show help |
1 |
Focus Worktree pane (toggle zoom if focused) |
2 |
Focus Status pane (toggle zoom if focused) |
3 |
Focus Log pane (toggle zoom if focused) |
Tab, ] |
Cycle to next pane |
[ |
Cycle to previous pane |
= |
Toggle zoom for focused pane (full screen) |
Log Pane (when focused on commit log):
| Key | Action |
|---|---|
Enter |
Open commit file tree (browse files changed in commit) |
d |
Show full commit diff in pager |
C |
Cherry-pick commit to another worktree |
j/k |
Navigate commits |
ctrl+j |
Next commit and open file tree |
/ |
Search commit titles (incremental) |
Commit File Tree (when viewing files in a commit):
| Key | Action |
|---|---|
j/k |
Navigate files and directories |
Enter |
Toggle directory collapse/expand, or show file diff |
d |
Show full commit diff in pager |
f |
Filter files by name |
/ |
Search files (incremental) |
n/N |
Next/previous search match |
q, Esc |
Return to commit log |
Status Pane (when focused on status):
Displays changed files in a collapsible tree view, grouped by directory (similar to lazygit).
| Key | Action |
|---|---|
j/k |
Navigate between files and directories |
Enter |
Toggle directory expand/collapse, or show diff for files |
e |
Open selected file in editor |
d |
Show full diff of all files in pager |
s |
Stage/unstage selected file or directory |
D |
Delete selected file or directory (with confirmation) |
c |
Commit staged changes |
C |
Stage all changes and commit |
g |
Open LazyGit |
ctrl+β, ctrl+β |
Jump to previous/next folder |
/ |
Search file/directory names (incremental) |
CI Status Pane (when viewing CI checks):
| Key | Action |
|---|---|
Enter |
Open CI job in browser |
Ctrl+v |
View CI logs in pager |
Ctrl+r |
Restart CI job (GitHub Actions only) |
Filter Mode:
Applies to focused pane (worktrees, files, commits). Active filter shows [Esc] Clear hint.
alt+n,alt+p: Navigate and update filter inputβ,β,ctrl+j,ctrl+k: Navigate without changing inputEnter: Exit filter mode (filter remains)Esc,Ctrl+C: Clear filter
Search Mode:
- Type to jump to the first matching item
n,N: Next / previous matchEnter: Close searchEsc,Ctrl+C: Clear search
Command History (!):
Saved per repository (100 max). Use β/β to navigate.
Command Palette Actions:
- Select theme: Change theme with live preview (see Themes).
- Create from current branch: Copy current branch to a new worktree. Tick "Include current file changes" to carry over uncommitted changes. Uses
branch_name_scriptif configured.
- Click: Select and focus panes or items
- Scroll: Navigate lists in any pane
Default worktree location: ~/.local/share/worktrees/<organization>-<repo_name>.
Reads ~/.config/lazyworktree/config.yaml. Example (also in config.example.yaml):
worktree_dir: ~/.local/share/worktrees
sort_mode: switched # Options: "path", "active" (commit date), "switched" (last accessed)
auto_refresh: true
refresh_interval: 10 # Seconds
disable_pr: false # Disable all PR/MR fetching and display (default: false)
icon_set: nerd-font-v3
search_auto_select: false
fuzzy_finder_input: false
palette_mru: true # Enable MRU (Most Recently Used) sorting for command palette
palette_mru_limit: 5 # Number of recent commands to show (default: 5)
max_untracked_diffs: 10
max_diff_chars: 200000
max_name_length: 95 # Maximum length for worktree names in table display (0 disables truncation)
theme: "" # Leave empty to auto-detect based on terminal background colour
# (defaults to "rose-pine" for dark, "dracula-light" for light).
# Options: see the Themes section below.
git_pager: delta
pager: "less --use-color --wordwrap -qcR -P 'Press q to exit..'"
editor: nvim
git_pager_args:
- --syntax-theme
- Dracula
trust_mode: "tofu" # Options: "tofu" (default), "never", "always"
merge_method: "rebase" # Options: "rebase" (default), "merge"
session_prefix: "wt-" # Prefix for tmux/zellij session names (default: "wt-")
# Branch name generation for issues and PRs
issue_branch_name_template: "issue-{number}-{title}" # Placeholders: {number}, {title}, {generated}
pr_branch_name_template: "pr-{number}-{title}" # Placeholders: {number}, {title}, {generated}
# Automatic branch name generation (see "Automatically Generated Branch Names")
branch_name_script: "" # Script to generate names from diff/issue/PR content
init_commands:
- link_topsymlinks
terminate_commands:
- echo "Cleaning up $WORKTREE_NAME"
custom_commands:
t:
command: make test
description: Run tests
show_help: true
wait: true
# Custom worktree creation menu items
custom_create_menus:
- label: "From JIRA ticket"
description: "Create from JIRA issue"
command: "jayrah browse 'SRVKP' --choose"
interactive: true # TUI-based commands need this to suspend lazyworktree
post_command: "git commit --allow-empty -m 'Initial commit for ${WORKTREE_BRANCH}'"
post_interactive: false # Run post-command in background
- label: "From clipboard"
description: "Use clipboard as branch name"
command: "pbpaste"Highest to lowest priority:
- CLI overrides (
--configflag) - Git local configuration (
git config --local) - Git global configuration (
git config --global) - YAML configuration file (
~/.config/lazyworktree/config.yaml) - Built-in defaults
Use the lw. prefix:
# Set globally
git config --global lw.theme nord
git config --global lw.worktree_dir ~/.local/share/worktrees
# Set per-repository
git config --local lw.theme dracula
git config --local lw.init_commands "link_topsymlinks"
git config --local lw.init_commands "npm install" # Multi-values supportedTo view configured values:
git config --global --get-regexp "^lw\."
git config --local --get-regexp "^lw\."Themes
theme: colour theme (auto-detected:draculadark,dracula-lightlight). See Themes.lazyworktree --show-syntax-themes: show delta syntax-theme defaults.lazyworktree --theme <name>: select UI theme.
Worktree list and refresh
sort_mode:"switched"(last accessed, default),"active"(commit date), or"path"(alphabetical).auto_refresh: background refresh of git metadata (default: true).ci_auto_refresh: periodically refresh CI status for GitHub repositories (default: false).refresh_interval: refresh frequency in seconds (default: 10).icon_set: choose icon set ("nerd-font-v3", "emoji", "text", "none"). Usenoneor an empty value to suppress icons.max_untracked_diffs,max_diff_chars: limits for diff display (0 disables).max_name_length: maximum display length for worktree names (default: 95, 0 disables truncation).
Search and palette
search_auto_select: start with filter focused (or use--search-auto-select).fuzzy_finder_input: show fuzzy suggestions in input dialogs.palette_mru: enable MRU sorting in command palette (default: true). Control count withpalette_mru_limit(default: 5).
Diff, pager, and editor
git_pager: diff formatter (default:delta). Empty string disables formatting.git_pager_args: arguments for git_pager. Auto-selects syntax theme for delta.git_pager_interactive: settruefor interactive viewers likediffnavortig.pager: pager for output display (default:$PAGER, fallback toless).ci_script_pager: pager for CI logs with direct terminal control. Falls back topager. Example to strip GitHub Actions timestamps:
ci_script_pager: |
sed -E '
s/.*[0-9]{4}-[0-9]{2}-[0-9]{2}T([0-9]{2}:[0-9]{2}:[0-9]{2})\.[0-9]+Z[[:space:]]*/\1 /;
t;
s/.*UNKNOWN STEP[[:space:]]+//' | \
tee /tmp/.ci.${LW_CI_JOB_NAME_CLEAN}-${LW_CI_STARTED_AT}.md |
less --use-color -q --wordwrap -qcR -P 'Press q to exit..'CI environment variables: LW_CI_JOB_NAME, LW_CI_JOB_NAME_CLEAN, LW_CI_RUN_ID, LW_CI_STARTED_AT.
editor: editor for Status paneekey (default:$EDITOR, fallback tonvim).
Worktree lifecycle
init_commands,terminate_commands: run before repository.wtcommands.
Sync and multiplexers
merge_method:"rebase"(default) or"merge". Controls Absorb and Sync (S) behaviour.session_prefix: prefix for tmux/zellij sessions (default:wt-). Palette filters by this prefix.
Branch naming
branch_name_script: script for automatic branch suggestions. See Automatically generated branch names.issue_branch_name_template,pr_branch_name_template: templates with placeholders{number},{title},{generated}.
Custom create menu
custom_create_menus: add custom items to the creation menu (ckey). Supportsinteractiveandpost_command.
Built-in themes:
| Theme | Notes | Best For |
|---|---|---|
| dracula | Dark (#282A36) | Dark terminals, vibrant colours, default fallback |
| dracula-light | White (#FFFFFF) | Light terminals, Dracula colours, default light theme |
| narna | Charcoal (#0D1117) | Dark terminals, blue highlights |
| clean-light | White (#FFFFFF) | Light terminals, cyan accent |
| catppuccin-latte | Soft white (#EFF1F5) | Catppuccin Latte light palette |
| rose-pine-dawn | Warm white (#FAF4ED) | RosΓ© Pine Dawn warm palette |
| one-light | Light grey (#FAFAFA) | Atom One Light |
| everforest-light | Beige (#F3EFDA) | Everforest nature light |
| solarized-dark | Deep teal (#002B36) | Classic Solarized dark palette |
| solarized-light | Cream (#FDF6E3) | Classic Solarized light palette |
| gruvbox-dark | Dark grey (#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 |
| modern | Zinc (#18181B) | Sleek modern dark theme with violet accents |
| tokyo-night | Storm (#24283B) | Tokyo Night Storm with blue highlights |
| one-dark | Dark (#282C34) | Atom One Dark classic palette |
| rose-pine | Midnight (#191724) | RosΓ© Pine dark and moody |
| ayu-mirage | Mirage (#212733) | Ayu Mirage modern look |
| everforest-dark | Dark (#2D353B) | Everforest nature dark |
| kanagawa | Wave (#1F1F28) | Kanagawa Wave inspired by Japanese art |
Set in config: theme: dracula
Define custom themes that inherit from built-in themes or define new colour schemes.
Inherit from built-in:
custom_themes:
my-dark:
base: dracula
accent: "#FF6B9D"
text_fg: "#E8E8E8"
my-light:
base: dracula-light
accent: "#0066CC"Defining a complete theme (all 11 colour fields required):
custom_themes:
completely-custom:
accent: "#00FF00"
accent_fg: "#000000"
accent_dim: "#2A2A2A"
border: "#3A3A3A"
border_dim: "#2A2A2A"
muted_fg: "#888888"
text_fg: "#FFFFFF"
success_fg: "#00FF00"
warn_fg: "#FFFF00"
error_fg: "#FF0000"
cyan: "#00FFFF"Inherit from other custom themes:
custom_themes:
base-custom:
base: dracula
accent: "#FF0000"
derived:
base: base-custom
accent: "#00FF00"Colour fields: accent, accent_fg, accent_dim, border, border_dim, muted_fg, text_fg, success_fg, warn_fg, error_fg, cyan.
Values must be hex (#RRGGBB or #RGB). With base, only override what you need. Without base, all 11 fields are required. Custom themes appear alongside built-in themes.
Shows CI check statuses for worktrees with associated PR/MR:
βGreen - Passed |βRed - Failed |βYellow - Pending |βGrey - Skipped |βGrey - Cancelled
Status is fetched lazily and cached for 30 seconds. Press p to refresh.
Define keybindings in config. Commands run interactively (TUI suspends) and appear in the palette. Use show_output to pipe through pager.
Defaults: t = tmux, Z = zellij. Override via custom_commands.t or custom_commands.Z. Palette lists sessions matching session_prefix (default: wt-).
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
c: # Open Claude CLI in a new terminal tab (Kitty)
command: claude
description: Claude Code
new_tab: true
show_help: true
t: # Open a tmux session with multiple windows
description: Tmux
show_help: true
tmux: # If you specify zellij instead of tmux this would manage zellij sessions
session_name: "wt:$WORKTREE_NAME"
attach: true
on_exists: switch
windows:
- name: claude
command: claude
- name: shell
command: zsh
- name: lazygit
command: lazygit| Field | Type | Default | Description |
|---|---|---|---|
command |
string | required | Command to execute |
description |
string | "" |
Shown in help and palette |
show_help |
bool | false |
Show in help screen (?) and footer |
wait |
bool | false |
Wait for keypress after completion |
show_output |
bool | false |
Show stdout/stderr in pager (ignores wait) |
new_tab |
bool | false |
Launch in new terminal tab (Kitty) |
tmux |
object | null |
Configure tmux session |
zellij |
object | null |
Configure zellij session |
| Field | Type | Default | Description |
|---|---|---|---|
session_name |
string | wt:$WORKTREE_NAME |
Session name (env vars supported, special chars replaced) |
attach |
bool | true |
Attach immediately; if false, show modal with instructions |
on_exists |
string | switch |
Behaviour if session exists: switch, attach, kill, new |
windows |
list | [ { name: "shell" } ] |
Window definitions for the session |
If windows is empty, creates a single shell window.
| 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) |
| Field | Type | Default | Description |
|---|---|---|---|
session_name |
string | wt:$WORKTREE_NAME |
Session name (env vars supported, special chars replaced) |
attach |
bool | true |
Attach immediately; if false, show modal with instructions |
on_exists |
string | switch |
Behaviour if session exists: switch, attach, kill, new |
windows |
list | [ { name: "shell" } ] |
Tab definitions for the session |
If windows is empty, creates a single shell tab. Session names with /, \, : are replaced with -.
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | window-N |
Tab name (supports env vars) |
command |
string | "" |
Command to run in the tab (empty uses your default shell) |
cwd |
string | $WORKTREE_PATH |
Working directory for the tab (supports env vars) |
Available: WORKTREE_BRANCH, MAIN_WORKTREE_PATH, WORKTREE_PATH, WORKTREE_NAME, REPO_NAME.
Single keys (e, s), modifiers (ctrl+e, alt+t), special keys (enter, esc, tab, space).
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: trueCustom commands override built-in keys.
Create a .wt file in your repository to run commands when creating/removing worktrees. Format inspired by wt.
init_commands:
- link_topsymlinks
- cp $MAIN_WORKTREE_PATH/.env $WORKTREE_PATH/.env
- npm install
- code .
terminate_commands:
- echo "Cleaning up $WORKTREE_NAME"Environment variables: WORKTREE_BRANCH, MAIN_WORKTREE_PATH, WORKTREE_PATH, WORKTREE_NAME.
Since .wt files execute arbitrary commands, lazyworktree uses TOFU. On first encounter or modification, select Trust, Block, or Cancel. Hashes stored in ~/.local/share/lazyworktree/trusted.json.
Configure trust_mode: tofu (default, prompt), never (skip all), always (no prompts).
link_topsymlinks: Built-in command that symlinks untracked/ignored root files, editor configs (.vscode,.idea,.cursor,.claude/settings.local.json), createstmp/, and runsdirenv allowif.envrcexists.
Special characters are converted to hyphens for Git compatibility. Leading/trailing hyphens are removed, consecutive hyphens collapsed. Length capped at 50 (manual) or 100 (auto) characters.
| Input | Converted |
|---|---|
feature.new |
feature-new |
bug fix here |
bug-fix-here |
feature:test |
feature-test |
Configure branch_name_script to generate names via tools like aichat or claude code. PRs/issues output to {generated} placeholder; diffs output complete names.
Note
Smaller, faster models suffice for branch names.
# For PRs/issues: generate a title (available via {generated} placeholder)
branch_name_script: "aichat -m gemini:gemini-2.5-flash-lite 'Generate a short title for this PR or issue. Output only the title (like feat-ai-session-manager), nothing else.'"
# Choose which template to use:
pr_branch_name_template: "pr-{number}-{generated}" # Use generated title
# pr_branch_name_template: "pr-{number}-{title}" # Use original PR title
# pr_branch_name_template: "pr-{number}-{generated}-{title}" # Use both!
# For diffs: generate a complete branch name
# 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.'"{number}- PR/issue number{title}- Original sanitised title{generated}- Generated title (falls back to{title}){pr_author}- PR author username
Examples:
| Template | Result (PR #2 by @alice: "Add AI session management") | Generated: feat-ai-session-manager |
|---|---|---|
pr-{number}-{title} |
pr-2-add-ai-session-management |
Not used |
pr-{number}-{generated} |
pr-2-feat-ai-session-manager |
Used |
pr-{number}-{pr_author}-{title} |
pr-2-alice-add-ai-session-management |
Not used |
pr-{number}-{pr_author}-{generated} |
pr-2-alice-feat-ai-session-manager |
Used |
If script fails, {generated} falls back to {title}.
Receives content on stdin, outputs branch name on stdout (first line). Timeout: 30s.
LAZYWORKTREE_TYPE (pr/issue/diff), LAZYWORKTREE_NUMBER, LAZYWORKTREE_TEMPLATE, LAZYWORKTREE_SUGGESTED_NAME.
Example:
# Different prompts for different types
branch_name_script: |
if [ "$LAZYWORKTREE_TYPE" = "diff" ]; then
aichat -m gemini:gemini-2.5-flash-lite 'Generate a complete branch name for this diff'
else
aichat -m gemini:gemini-2.5-flash-lite 'Generate a short title (no pr- prefix) for this PR/issue'
fi
# Use PR/issue number in the prompt
branch_name_script: |
aichat -m gemini:gemini-2.5-flash-lite "Generate a title for PR #$LAZYWORKTREE_NUMBER. Output only the title."
create-from-issue-demo.mp4
See COMPARISON.md for a detailed comparison with other worktree managers.
Originally a Python Textual app, migrated to Go (BubbleTea) for faster startup. Python version: https://github.com/chmouel/lazyworktree/tree/python
- π Fediverse - <@chmouel@chmouel.com>
- π¦ Twitter - <@chmouel>
- π Blog - <https://blog.chmouel.com>

