A fast, intuitive CLI that makes Git worktrees as simple as switching branches.
Without Grove:
git clone --bare git@github.com:org/repo.git .bare echo "gitdir: ./.bare" > .git git worktree add main cd main cp ../other-worktree/.env . # Don't forget this npm install # Or this # Later: cd ../feat-auth to switch
With Grove:
grove clone git@github.com:org/repo grove add feat/auth --switch # .env copied, hooks run automatically grove switch main # Just like git checkout, but each branch keeps its own directory
demo.mp4
Note
Grove is under active development. Core functionality is stable, but APIs may change between major versions.
New to worktrees?
Git worktrees let you work on multiple branches simultaneously in separate directories. No stashing. No "wrong branch" mistakes. Your work stays exactly where you left it.
The catch: git worktree is clunky. Grove makes it feel like git checkout β but each branch gets its own persistent directory.
- Git 2.48+ β Grove uses
--relative-pathsfor portable worktrees
curl -fsSL https://raw.githubusercontent.com/sQVe/grove/main/install.sh | shgo install github.com/sqve/grove/cmd/grove@latest
git clone https://github.com/sQVe/grove && cd grove go build -o grove ./cmd/grove sudo mv grove /usr/local/bin/
Download .deb or .rpm packages from GitHub Releases.
Grove works without additional dependencies, but installing the GitHub CLI (gh) enables enhanced features:
- PR worktrees: Create worktrees from pull requests with
grove add --pr 123orgrove clone https://github.com/owner/repo/pull/123 - Squash-merge detection:
grove pruneaccurately detects branches merged via GitHub's squash-and-merge, even with multiple commits. Withoutgh, only single-commit squash merges are detected via git.
See GitHub CLI installation for setup instructions.
Without this, grove switch only prints a path β you'd have to cd manually. Add to your shell config:
# bash (~/.bashrc) or zsh (~/.zshrc) eval "$(grove switch shell-init)" # fish (~/.config/fish/config.fish) grove switch shell-init | source # powershell ($PROFILE) grove switch shell-init | Invoke-Expression
Tab completion for commands, flags, and worktree names.
# bash eval "$(grove completion bash)" # zsh eval "$(grove completion zsh)" # fish grove completion fish | source # powershell grove completion powershell | Invoke-Expression
# Clone a repository grove clone https://github.com/owner/repo cd repo # Start a feature grove add feat/auth --switch # Check your worktrees grove list # Switch back to main grove switch main # Clean up when done grove remove feat/auth --branch
grove clone <url> [directory]
Clone a repository into a Grove workspace.
Flags:
--branches <list>β Comma-separated branches to create worktrees for--shallowβ Shallow clone (depth=1)-v, --verboseβ Show git output
Examples:
grove clone https://github.com/owner/repo
grove clone https://github.com/owner/repo my-project
grove clone https://github.com/owner/repo --branches main,develop
grove clone https://github.com/owner/repo/pull/123 # Clone and checkout PRgrove init new [directory] / grove init convert
Initialize a Grove workspace.
Subcommands:
new [directory]β Create new workspaceconvertβ Convert existing repository
Flags (convert):
--branches <list>β Additional branches to create worktrees for-v, --verboseβ Show git output
Examples:
grove init new my-project grove init convert grove init convert --branches develop,staging
grove add [branch|PR-URL|ref]
Add a worktree for a branch, pull request, or ref.
Flags:
-s, --switchβ Switch to the worktree; prints the path of an existing one instead of erroring--base <branch>β Create new branch from this base instead of the default branch--no-fetchβ Skip fetching the base branch or the existing branch's upstream--name <name>β Custom directory name-d, --detachβ Detached HEAD state--pr <number>β Create worktree for a pull request--from <worktree>β Source worktree for file preservation (name or branch)--resetβ Reset diverged PR branch to match remote (use with--pr)
Examples:
grove add feat/auth grove add feat/auth --switch grove add --base main feat/auth grove add --pr 123 # PR by number grove add --pr 123 --reset # PR, discarding local commits grove add --detach v1.0.0 # Tag in detached HEAD grove add --from dev feat/auth # Copy .env from dev worktree
New branches start from the default branch on origin, fetched with a five-second timeout.
If fetching fails, Grove warns with the base ref and commit age, then uses the existing
origin ref, local default branch, or bare HEAD. Existing worktrees stay unchanged.
An explicit --base <branch> fetches the branch from origin before resolving it, even
when its remote-tracking ref is missing. It prefers origin/<branch>, falls back to the
local branch, and errors if neither exists. --base origin/<branch> requires the origin
ref. --base HEAD selects the bare repository's HEAD without fetching.
Use --no-fetch to skip fetching once, or
grove config set --global grove.fetchBase false to disable it by default. Both leave the
refs on disk untouched and pick from them in the same order.
Adding an existing local branch fetches its upstream (origin/<branch> when none is
configured) and fast-forwards the branch first when it is strictly behind; a branch that is
ahead or diverged is checked out as is. --no-fetch skips that fetch too. Re-running
grove add --pr N on a same-repo PR that already has a worktree refreshes it: a clean
worktree is fast-forwarded, a branch with local commits errors unless --reset discards them,
and a worktree with uncommitted tracked changes refuses to refresh. Fork PRs still error. With
-s, grove add prints the path of an existing worktree for a branch instead of erroring.
grove switch <worktree>
Switch to a worktree by directory or branch name.
Requires shell integration (see Setup section).
Examples:
grove switch main grove switch feat-auth grove switch feat/auth
grove list
List all worktrees with status.
Flags:
--fastβ Skip dirty and sync status checks--filter <status>β Filter bydirty,ahead,behind,gone,locked, orpr. Comma-separated filters use OR.lockedandprwork with--fast.--jsonβ JSON output. Includesprwhen a PR number is recorded andlast_commitwhen commit times are known (omitted with--fast).--sort name|recentβ Sort by name or latest commit (recentneeds commit times, so it cannot be combined with--fast)-v, --verboseβ Show paths, upstreams, and a PR column (#42) when any worktree has a recorded PR number, blank on the rows without one. PR numbers come from local config;liststays offline.
Examples:
grove list grove list --fast grove list --filter dirty grove list --filter ahead,behind grove list --filter pr grove list --json grove list --sort recent
grove fetch
Fetch all remotes and show branch changes.
Flags:
--jsonβ JSON output-v, --verboseβ Show commit hash details
Examples:
grove fetch grove fetch --verbose grove fetch --json
grove status
Show current worktree status.
Flags:
-v, --verboseβ Show all diagnostic sections--jsonβ JSON output
Examples:
grove status grove status --verbose
grove remove <worktree>...
Remove one or more worktrees. With --branch, unmerged branches are rejected before removal unless --force is set.
Flags:
-f, --forceβ Remove even if dirty or locked; with--branch, delete unmerged and unpushed commits--branchβ Also delete the branch--ignore-missingβ Skip unknown targets with a warning
Examples:
grove remove feat-auth grove remove feat-auth --branch grove remove --force wip grove remove feat-auth bugfix-123 # Remove multiple grove remove --ignore-missing feat-auth bugfix-123 # Skip missing worktrees
grove move <worktree> <new-branch>
Rename a branch and its worktree.
Examples:
grove move feat/old feat/new
grove lock <worktree>...
Lock one or more worktrees to prevent removal.
Flags:
--reason <text>β Reason for locking
Examples:
grove lock main grove lock release --reason "Production release" grove lock feat-auth bugfix-123 # Lock multiple
grove unlock <worktree>...
Unlock one or more worktrees.
Examples:
grove unlock feat-auth
grove unlock feat-auth bugfix-123 # Unlock multiplegrove prune
Remove worktrees with deleted upstream branches. Dry-run by default.
When removing worktrees whose upstream was deleted on remote, local branches are also deleted.
Flags:
--commitβ Actually remove (default is dry-run)-f, --forceβ Remove even if dirty, locked, or unpushed--stale <duration>β Include inactive worktrees (e.g.,30d,2w)--mergedβ Include branches merged into default branch--detachedβ Include detached worktrees
Examples:
grove prune # Dry-run grove prune --commit # Actually remove grove prune --stale 30d --commit grove prune --merged --commit grove prune --detached --commit
grove exec [worktrees...] -- <command>
Execute a command in worktrees.
Flags:
-a, --allβ Execute in all worktrees-j, --parallel Nβ Run at most N commands at once (default: 1)--fail-fastβ Stop starting commands after the first failure; running commands finish--jsonβ Write results as JSON to stdout and child output to stderr
With N > 1, each worktree's header, stdout, and stderr appear together on stderr
as each command finishes. Output is buffered in memory without a size cap.
Parallel commands receive no stdin. --json always lists worktrees in target
order, though with --fail-fast it can list more of them than a sequential run,
since commands already running when the first failure lands still finish and
still report. With N = 1 or no flag, output streams as before and commands
inherit stdin.
Examples:
grove exec --all -- npm install grove exec --all -j 4 -- npm test grove exec main feat-auth -- git pull grove exec --all --fail-fast -- go build grove exec --all -- bash -c "npm install && npm test"
grove config <subcommand>
Manage configuration.
Subcommands:
listβ Show all settingsget <key>β Get valueset <key> <value>β Set value (requires--sharedor--global)unset <key>β Remove setting (requires--sharedor--global)initβ Create.grove.tomltemplate
Flags:
--sharedβ Target.grove.toml--globalβ Target git config
Examples:
grove config list grove config get preserve.patterns grove config set --global grove.plain true grove config init
Edit .grove.toml to set array values:
[preserve] patterns = [".env", ".env.local"] [link] patterns = [".next", ".turbo"] [autolock] patterns = ["main", "release/*"]
grove doctor
Diagnose workspace issues.
Flags:
--fixβ Auto-fix safe issues--jsonβ JSON output--perfβ Disk space analysis
Detects:
- Dependency versions (Git 2.48+, optional gh CLI)
- Broken
.gitpointers - Stale worktree entries
- Unreachable remotes
- Invalid
.grove.tomlsyntax - Hook commands not found in PATH
- Stale lock files
Examples:
grove doctor grove doctor --fix grove doctor --perf
Grove uses a two-layer configuration system:
.grove.tomlβ Team settings (checked into repository)- Git config (
~/.gitconfig) β Personal settings
Run grove config init to create a .grove.toml template.
Default configuration
# Grove - Git worktree management # https://github.com/sqve/grove # Use Nerd Font icons in output (when not in plain mode). nerd_fonts = true # Threshold for marking worktrees as stale (no commits within this period). # Format: number followed by d (days), w (weeks), or m (months). stale_threshold = "30d" # Disable colors and symbols in output. plain = false # Enable debug logging. debug = false # Fetch the base branch from origin before creating a new branch. fetch_base = true [preserve] # Files to copy from the current worktree when creating a new one. # Useful for environment files and local configuration that shouldn't be in git. # Supports glob patterns. These patterns override git config grove.preserve. patterns = [ ".env", ".env.keys", ".env.local", ".env.*.local", ".envrc", ".grove.toml", "docker-compose.override.yml", ] # Path segments to exclude from preservation. # Files containing any of these path segments will be skipped. exclude = [ # Caches ".cache", "__pycache__", # Build output "build", "coverage", "dist", "out", "target", # Dependencies ".venv", "node_modules", "vendor", "venv", ] # Directories to recursively copy when creating a new worktree. # Unlike preserve.patterns, this copies the full directory tree. directories = [] [link] # Directories to symlink from the source worktree when creating a new one. # Useful for sharing tool state (e.g., .claude) across worktrees. # Patterns without / match directory names in the worktree root. # Patterns with / match paths relative to the worktree root. # Creates relative symlinks so the workspace remains portable. patterns = [] [hooks] # Shell commands to run after creating a worktree. # Runs sequentially, stops on first failure. # Examples: ["npm install"], ["go mod download", "make setup"] add = [] [autolock] # Branch patterns to automatically lock when creating worktrees. # Locked worktrees are protected from accidental deletion. # Supports exact matches and trailing /* wildcards (e.g., "release/*"). patterns = ["develop", "main", "master"]
Husky and lefthook set core.hooksPath to a relative path (.husky or .lefthook). In a bare worktree setup, this config is shared across all worktrees via .bare/config. The relative path resolves correctly from any worktree because the directory exists in every checkout. Re-run the installer after worktree creation to ensure the path is set:
Husky
[hooks] add = ["npm install", "npx husky"]
lefthook
[hooks] add = ["lefthook install"]
pre-commit writes hook scripts directly into the git hooks directory (.bare/hooks/), which is shared by all worktrees. Run pre-commit install once from any worktree β no Grove hook needed.
Next.js build output (.next) and Turborepo cache (.turbo) can be shared across worktrees to avoid redundant rebuilds. Avoid symlinking node_modules/ unless its contents are portable and dependency versions match across worktrees. It can contain absolute paths and branch-specific dependency versions.
[link] patterns = [".next", ".turbo"]
For a monorepo that meets those conditions, use a path pattern to link each app's dependencies:
[link] patterns = ["apps/*/node_modules"]
Rust target/ directories consume significant disk space. Sharing via symlink avoids duplicate compilation across worktrees.
[link] patterns = ["target"]
Parallel cargo build across worktrees sharing the same target directory will serialize on file locks.
AI coding tools store local configuration in git-ignored files that are lost in new worktrees. Preserve them to carry project context and settings across worktrees.
[preserve] patterns = [ "CLAUDE.local.md", ".claude/settings.local.json", ".aider.conf.yml", ".copilot/config.json", ]
These patterns replace the default preserve patterns.
Contributions welcome! See CONTRIBUTING.md for development setup and guidelines.