An opinionated git worktree workflow for managing multiple branches simultaneously.
git-workon clones repositories as bare repos with a worktrees-first layout, then provides commands for creating, finding, and cleaning up worktrees — so switching between branches is just cd, not git stash && git checkout.
brew install lettertwo/tap/git-workoncargo install git-workon
# Optional: install man page
git workon generate-man | install -m 644 /dev/stdin /usr/local/share/man/man1/git-workon.1git clone https://github.com/lettertwo/git-workon
cd git-workon
make install # installs git hooks + man page (PREFIX=/usr/local by default)
cargo install --path ./git-workonOverride the man page install location with PREFIX:
make install-man PREFIX=~/.localgit workon clone https://github.com/owner/repo
cd repo/main # jumps into the default branch worktree
git workon clone https://github.com/owner/repo --no-hooks # skip post-create hooksgit workon init # bare init in current directory with initial worktree
git workon init my-project # bare init in my-project/ with initial worktree
git workon init --no-hooks # skip post-create hooksgit workon new # interactive: prompts for name, then base branch
git workon new my-feature # creates branch + worktree
git workon new my-feature --base main # branch from main
git workon new my-feature -B existing # attach to existing branch, name dir 'my-feature'
git workon new my-feature --orphan # create branch with no parent commits
git workon new my-feature --detach # detach HEAD in the new worktree
git workon new my-feature --copy # copy local files from base worktree (overrides config)
git workon new my-feature --no-copy # skip file copy even if workon.autoCopy=true
git workon new my-feature --lock # lock worktree after creation
git workon #123 # create worktree from PR #123 (auto-fetches)When in a stack worktree, new defaults the base branch to the current HEAD branch (so the new branch stacks on top). Pass --base to override, or --no-stack to disable stack-aware behavior.
When a branch name exists on multiple remotes, new prompts you to choose one (suppressed to first-remote by --no-interactive).
git workon find # interactive: fuzzy picker across all worktrees
git workon find main # prints path to the 'main' worktree
git workon find feat --dirty # find a worktree matching 'feat' that has uncommitted changes
git workon find --clean # interactive: only clean worktrees
git workon find --ahead # interactive: only worktrees with unpushed commits
git workon find --behind # interactive: only worktrees behind upstream
git workon find --gone # interactive: only worktrees with deleted upstreams
git workon find --new my-feature # bypass resolution and force a new worktree
git workon my-feature # smart-route: find worktree, or create one if branch existsThe bare workon <name> routing: PR reference → new (auto-fetch); existing worktree → find (navigate); local or remote tracking branch with no worktree → new (auto-attach); stack-home + branch in current stack → checkout (in-place HEAD switch); no match → find (error).
find match priority: exact name > fuzzy dir/branch name > interactive picker. Multiple fuzzy matches trigger the picker. When stack-active, the picker shows a tree with ◉ (current), ◎ (exists), ◯ (metadata-only, no worktree). Pressing Tab on any item forces creation of a new worktree instead of navigating to an existing one. Selecting a ◯ item automatically routes to new to create/attach it.
Status filters suppress the stack tree and use a flat list (metadata-only diffs cannot satisfy worktree-status filters).
git workon list # all worktrees
git workon list --dirty # only worktrees with uncommitted changes
git workon list --clean # only worktrees without uncommitted changes
git workon list --ahead # worktrees with unpushed commits
git workon list --behind # worktrees behind their upstream
git workon list --gone # worktrees whose upstream branch was deleted
git workon list --dirty --ahead # filters combine with AND logic
# Note: filters produce a flat list in stack-enabled repos (the tree is suppressed).By default, pruning deletes the local branch ref along with the worktree. Use --keep-branch to preserve it.
git workon prune # interactive: shows candidates, prompts for confirmation
git workon prune --yes # skip confirmation prompt (for scripting)
git workon prune --dry-run # preview without deleting
git workon prune my-feature # prune a specific worktree by name
git workon prune --gone # also prune worktrees whose upstream branch is gone
git workon prune --gone --fetch # fetch --prune from remotes first so gone status is fresh
git workon prune --merged # also prune worktrees merged into the default branch
git workon prune --merged=release/v2 # merge target other than the default branch
git workon prune --keep-branch # prune worktrees but keep local branch refs
git workon prune --allow-dirty # prune even with uncommitted changes
git workon prune --allow-unmerged # prune even with unmerged commits
git workon prune --include-locked # include locked worktrees
git workon prune --force # override all safety checks (protection, dirty, unmerged, locked)A bare git workon prune (no --gone) shows a hint if any worktrees have gone upstreams.
Safety checks (skipped with --force): protected branches (workon.pruneProtectedBranches), default branch, uncommitted changes (tracked files only for --gone candidates), unmerged commits, locked worktrees.
git workon move new-name # rename the current worktree
git workon move old-name new-name # rename a specific worktree
git workon move new-name --dry-run # preview without renaming
git workon move new-name --force # override safety checks (dirty, unpushed, protected)git workon doctor # check for broken worktrees and missing dependencies
git workon doctor --fix # automatically repair fixable issues
git workon doctor --dry-run # preview fixes without applyingChecks performed:
- Worktrees: missing directories, broken git links, gone upstreams
- Dependencies:
ghCLI (PR features),ghauth status, git remote,gtCLI (stack features), hook commands in PATH - Configuration: renamed config keys (auto-fixable), invalid
stackModel/stackWorktreeGranularity, invalidprFormat,defaultBranchnot found in repo,stackModel=graphitewithoutgt init
Copies git-untracked files (ignored files and untracked-but-not-staged files). The to argument defaults to the current worktree when omitted.
git workon copy main # copy from 'main' into current worktree
git workon copy main my-feature # copy from 'main' into 'my-feature'
git workon copy main my-feature --pattern '.env*' # copy only files matching pattern
git workon copy main my-feature --exclude '.env.prod' # exclude specific files (additive with config)
git workon copy main my-feature --force # overwrite existing files in destination
git workon copy main my-feature --no-include-ignored # skip git-ignored filesgit workon find and git workon new print the worktree path to stdout — they don't cd on their own, since a subprocess can't change the parent shell's directory. Without the shell wrapper, you'd need to do this yourself:
# bash / zsh
cd "$(git workon find main)"
# fish
cd (git workon find main)The shell integration sets up a workon wrapper function that captures the output and cds automatically when the result is a directory:
# bash
eval "$(git workon shell-init bash)"
# zsh
eval "$(git workon shell-init zsh)"
# fish
git workon shell-init fish | sourceThe shell is auto-detected from $SHELL if not specified. Use --cmd to change the wrapper function name (default: workon):
eval "$(git workon shell-init --cmd wt)" # installs a 'wt' function insteadAfter setup, workon <name> changes your current directory to the worktree, and workon new <name> drops you directly into the newly created worktree.
Scripting: pass
--no-interactivetofindandnew, and--yestoprune, to suppress all prompts. Pass--jsonglobally to get machine-readable output on success and a{"error": {...}}envelope on failure.
These flags are accepted before any subcommand:
| Flag | Effect |
|---|---|
--json |
Output results as JSON; errors are {"error":{"code":…,"message":…}} |
--no-color |
Disable ANSI color output |
--no-stack |
Disable stack-aware behavior for this invocation |
-v / -q |
Increase/decrease log verbosity (can repeat, e.g. -vv) |
--json also sets --no-interactive for commands that prompt.
man git-workongit-workon uses git config keys under the workon.* namespace:
[workon]
# New worktrees
defaultBranch = main # base branch when none is specified
prFormat = pr-{number} # worktree name pattern for PR checkouts
# File copying
autoCopy = false # copy local files automatically on 'new'
copyPattern = .env.local # glob patterns to copy (multi-value)
copyExclude = .env.prod # patterns to exclude (multi-value)
copyIncludeIgnored = true # include git-ignored files when copying
# Hooks
postCreateHook = npm install # commands run after worktree creation (multi-value)
hookTimeout = 300 # hook timeout in seconds (0 = no timeout)
# Pruning
pruneProtectedBranches = main # branches protected from pruning (multi-value)
pruneProtectedBranches = release/*
pruneGone = false # prune gone-upstream worktrees by default
pruneFetch = false # fetch from remotes before evaluating gone status
# Stacked diffs (Graphite)
stackModel = auto # "auto", "graphite", or "none"
stackWorktreeGranularity = stack # "stack" (one worktree per stack)
gtAutoTrack = true # auto-run 'gt track' after 'workon new'See man git-workon or git workon --help for full documentation.
The core logic is published as the workon crate.
API docs are on docs.rs.