Stop juggling
git worktree add ../repo-123 -b 123/impl && cd && npm install && cp .env.gw start 123does all of it — andgw end 123cleans up safely.
$ gw start 123
✓ Created worktree at ../myrepo-123
Detected npm, running setup...
✓ npm setup completed
✨ Worktree ready at:
../myrepo-123
💡 Shell integration will change to this directory after the command completes.
$ gw end 123
✓ Successfully removed worktree for issue #123
| plain git worktree | gw |
|---|---|
git worktree add ../repo-123 -b 123/impl && cd ../repo-123 && npm install && cp ../.env . |
gw start 123 |
git worktree remove refuses on a dirty tree — or discards it with --force |
gw end checks uncommitted changes, unpushed commits, and merge status, then asks before removing |
| Worktrees pile up across old issues | gw clean bulk-removes every merged, clean worktree in one command |
-
Install
go install github.com/sotarok/gw@latest
-
Set up shell integration (required for auto-cd)
# Add to ~/.zshrc eval "$(gw shell-integration --show-script --shell=zsh)"
Bash and Fish variants are in the Shell Integration section.
-
Create a worktree
gw start 123
-
Remove it when you're done
gw end 123
That's it. Everything below is reference.
Core
- One command to create a worktree, check out a branch, install dependencies, and optionally copy
.envfiles (gw start/gw checkout) - Automatic package-manager detection and setup: npm, yarn, pnpm, cargo, go, pip, bundler, composer
- Auto-cd into the new worktree directory via shell integration
- Interactive branch/worktree selection when no argument is given
Safety
- Three pre-removal checks run in parallel before
gw endorgw clean: uncommitted changes, unpushed commits, and merge status against the base branch gw clean --dry-runpreviews what would be removed before touching anything- direnv-style trust model for project-local hook files (
.gwrc)
Integrations
- Lifecycle hooks:
post_start_hook,post_checkout_hook,pre_end_hook - Project-local
.gwrcat the repository root overrides hook keys per-repo (new in v1.1) - iTerm2 tab name updated automatically when creating, switching, or removing worktrees
- Zsh completion via shell integration (
gw endcompletes worktree branch names)
go install github.com/sotarok/gw@latestDownload the latest binary for your platform from the Releases page.
Linux
# AMD64
curl -L https://github.com/sotarok/gw/releases/latest/download/gw_Linux_x86_64.tar.gz | tar xz
sudo mv gw /usr/local/bin/
# ARM64
curl -L https://github.com/sotarok/gw/releases/latest/download/gw_Linux_arm64.tar.gz | tar xz
sudo mv gw /usr/local/bin/macOS
# Intel Mac
curl -L https://github.com/sotarok/gw/releases/latest/download/gw_Darwin_x86_64.tar.gz | tar xz
sudo mv gw /usr/local/bin/
# Apple Silicon
curl -L https://github.com/sotarok/gw/releases/latest/download/gw_Darwin_arm64.tar.gz | tar xz
sudo mv gw /usr/local/bin/git clone https://github.com/sotarok/gw.git
cd gw
make installCreate a new worktree for an issue number or branch name.
# Create worktree for issue #123 — creates branch "123/impl"
gw start 123
# Branch name with "/" — used exactly as given
gw start 476/impl-migration-script
# Named feature branch
gw start feature/new-feature
# Different base branch
gw start 456 develop
# Also copy .env files from the main worktree
gw start 789 --copy-envsThis will:
- Create a new worktree at
../{repository-name}-{identifier} - Create a new branch (
{issue-number}/implfor plain numbers, or the exact name provided) - Optionally copy untracked
.envfiles from the original repository - Run package-manager setup if a package manager is detected
- Change to the new worktree directory (requires shell integration)
| Flag | Description |
|---|---|
--copy-envs |
Copy untracked .env files to the new worktree |
--no-fetch |
Skip git fetch before running the command |
--no-project-hooks |
Skip project-local .gwrc hook overrides for this run |
Checkout an existing branch as a new worktree. If no branch is given, an interactive selector is shown.
# Checkout a specific branch
gw checkout feature/auth
# Checkout a remote branch
gw checkout origin/feature/api
# Interactive mode — select from list
gw checkout
# Checkout and copy .env files
gw checkout feature/auth --copy-envsThis will:
- Create a new worktree at
../{repository-name}-{branch-name} - Checkout the specified branch (or create a local tracking branch for a remote)
- Optionally copy untracked
.envfiles from the original repository - Run package-manager setup if a package manager is detected
- Change to the new worktree directory (requires shell integration)
| Flag | Description |
|---|---|
--copy-envs |
Copy untracked .env files to the new worktree |
--no-fetch |
Skip git fetch before running the command |
--no-project-hooks |
Skip project-local .gwrc hook overrides for this run |
Remove a worktree. If no issue number is given, an interactive selector is shown.
# Remove the worktree for issue #123
gw end 123
# Interactive mode — select from list
gw end
# Skip safety checks and remove immediately
gw end 123 --forceBefore removing, gw end runs three safety checks in parallel:
- Uncommitted changes in the worktree
- Unpushed commits on the branch
- Whether the branch is merged into the base branch
If any check trips, gw end prints the warnings and prompts for confirmation. Use --force to skip all checks.
| Flag | Short | Description |
|---|---|---|
--force |
-f |
Force removal without safety checks |
--no-fetch |
Skip git fetch before running the command |
|
--no-project-hooks |
Skip project-local .gwrc hook overrides for this run |
Bulk-remove all worktrees that are safe to delete. Useful for clearing out merged work after a sprint.
# Preview what would be removed
gw clean --dry-run
# Confirm and remove all merged/clean worktrees
gw clean
# Remove without the confirmation prompt
gw clean --forcegw clean evaluates each worktree against the same three safety checks as gw end, then displays a table showing which worktrees are removable and which are not (with per-worktree reasons). It asks for confirmation before removing anything, unless --force is given.
--dry-run shows the table but skips the confirmation and removal entirely.
The pre_end_hook runs for each worktree that is about to be removed, with cwd set to that worktree.
| Flag | Short | Description |
|---|---|---|
--force |
-f |
Remove without confirmation prompt |
--dry-run |
Show what would be removed without removing | |
--no-fetch |
Skip git fetch before running the command |
|
--no-project-hooks |
Skip project-local .gwrc hook overrides for this run |
View and edit configuration interactively, or list current values.
# Open the interactive TUI editor
gw config
# Print all configuration values (non-interactive)
gw config --listThe interactive editor supports:
- Arrow keys or
j/kto navigate EnterorSpaceto toggle boolean valuessto saveqto quit?to view help
Run an interactive setup to create ~/.gwrc with your preferences.
gw initPrint the shell integration script. Normally consumed via eval in your shell config — see Shell Integration.
gw shell-integration --show-script --shell=zshAll configuration lives in ~/.gwrc. Use gw init for first-time setup or gw config to edit at any time.
| Key | Default | Description |
|---|---|---|
auto_cd |
true |
Automatically change directory to the new worktree after creation (requires shell integration) |
update_iterm2_tab |
false |
Update iTerm2 tab title with worktree information (macOS only) |
auto_remove_branch |
false |
Automatically delete the local branch after successful worktree removal |
copy_envs |
(unset) | Copy .env files to new worktrees. When unset (nil), gw prompts each time; set to true or false to fix the behavior |
fetch_before_command |
true |
Run git fetch --all --prune before commands to sync remote branch info |
post_start_hook |
(empty) | Shell command to execute after a successful gw start |
post_checkout_hook |
(empty) | Shell command to execute after a successful gw checkout |
pre_end_hook |
(empty) | Shell command to execute before a worktree is removed by gw end or gw clean, with cwd set to the worktree |
# gw configuration file
auto_cd = true
update_iterm2_tab = false
auto_remove_branch = false
# copy_envs = false # Uncomment to set default behavior
fetch_before_command = true
# Hook commands executed after successful worktree operations
# Available env vars: GW_WORKTREE_PATH, GW_BRANCH_NAME, GW_REPO_NAME, GW_COMMAND
# post_start_hook =
# post_checkout_hook =
# Hook commands executed before a worktree is removed (from end/clean)
# Runs with cwd set to the worktree. Same env vars as above; GW_COMMAND is "end" or "clean"
# pre_end_hook =
Hook commands are executed via sh -c with the following environment variables:
| Variable | Description |
|---|---|
GW_WORKTREE_PATH |
Absolute path to the worktree |
GW_BRANCH_NAME |
Branch name of the worktree |
GW_REPO_NAME |
Repository name |
GW_COMMAND |
The command that triggered the hook (start, checkout, end, or clean) |
Hook failures are treated as warnings and do not block the overall command.
| Hook | Fires |
|---|---|
post_start_hook |
After gw start successfully creates a worktree |
post_checkout_hook |
After gw checkout successfully creates a worktree |
pre_end_hook |
Before gw end removes a worktree, and before each worktree gw clean removes. Runs with cwd set to the worktree so it can operate on files that are about to disappear |
Open a new tmux window for the worktree instead of changing directory in the current shell:
auto_cd = false
post_start_hook = tmux new-window -c "$GW_WORKTREE_PATH" -n "$GW_BRANCH_NAME"
post_checkout_hook = tmux new-window -c "$GW_WORKTREE_PATH" -n "$GW_BRANCH_NAME"
Open a new iTerm2 tab for the worktree on macOS. First, create a hook script:
mkdir -p ~/.gw/hooks
cat > ~/.gw/hooks/iterm2-new-tab.sh << 'EOF'
#!/bin/bash
osascript <<APPLESCRIPT
tell application "iTerm"
tell current window
create tab with default profile
tell current session of current tab
write text "cd '${GW_WORKTREE_PATH}' && clear"
set name to "${GW_BRANCH_NAME}"
end tell
end tell
end tell
APPLESCRIPT
EOF
chmod +x ~/.gw/hooks/iterm2-new-tab.shThen configure .gwrc:
auto_cd = false
post_start_hook = ~/.gw/hooks/iterm2-new-tab.sh
post_checkout_hook = ~/.gw/hooks/iterm2-new-tab.sh
Send a desktop notification after worktree creation:
post_start_hook = osascript -e 'display notification "Worktree ready at '"$GW_WORKTREE_PATH"'" with title "gw"'
If each worktree runs its own docker compose stack, tear it down before the worktree is deleted. pre_end_hook runs with the worktree as its working directory, so relative paths work naturally:
pre_end_hook = docker compose down -v --remove-orphans
Or use the bundled example script (examples/hooks/docker-compose-down.sh) which skips worktrees with no compose file:
pre_end_hook = ~/.gw/hooks/docker-compose-down.sh
When update_iterm2_tab is enabled and you're using iTerm2:
- Tab name is updated to
{repository-name} {issue-number/branch-name}when creating or switching worktrees - Tab name is reset when removing worktrees
- Only activates when running inside iTerm2 (automatically detected)
For repository-specific hook commands (e.g. a post_start_hook that only makes sense for one project), place a .gwrc at the main worktree root — the same directory as the repository's .git — using the same key = value format as ~/.gwrc.
# <repo>/.gwrc
post_start_hook = pnpm dev
Scope (v1.1): hooks only. Only the three hook keys — post_start_hook, post_checkout_hook, pre_end_hook — can be overridden per project. Any other key (auto_cd, update_iterm2_tab, auto_remove_branch, copy_envs, fetch_before_command) is parsed but never applied from a project .gwrc; gw prints a one-line note to stderr (note: project .gwrc key 'auto_cd' is ignored in v1.1 (hooks-only)) and keeps using the global value.
Merge rule. The global ~/.gwrc is the base. Only the hook keys the project file actually writes are overridden — a hook key the project file doesn't mention keeps its global value. Writing a hook key with an empty value (post_start_hook =) disables that global hook for the project, without needing trust approval (an empty value can't execute code).
Where the project file is found. For a linked worktree (created by gw start/gw checkout), gw resolves git rev-parse --git-common-dir's parent directory as the main worktree root and reads .gwrc there, so every linked worktree reads the same project .gwrc as the main repository; a .gwrc accidentally checked out inside a linked worktree itself is never read. For every other layout — including a --separate-git-dir checkout or a git submodule, where --git-common-dir's parent is not the worktree root — gw instead resolves the root via git rev-parse --show-toplevel.
Because a project .gwrc ships with the repository, its hook values could run arbitrary code as soon as someone runs a gw command in a clone they don't fully trust. gw uses a direnv-style trust model:
- The first time a project
.gwrcdeclares a non-empty hook value,gwprompts (default: No) before running it, showing the file path and the hook value(s) awaiting approval. - Approval is keyed by a hash of the file's absolute path and content. Editing the file — even by one character — invalidates the old approval and re-prompts. A different clone (different absolute path) of the same content also re-prompts.
- Approval is stored in
~/.gw/trust/<hash>and applies repo-wide: once one worktree approves a project.gwrc, every other worktree of that same repository (which all read the same main-root file) uses it without re-prompting, as long as the content hasn't changed. - The prompt appears on stderr / your terminal, never on stdout —
gw start/gw checkout's stdout is consumed by shell integration and must stay machine-parseable. - If stdin isn't a terminal (e.g. running in CI or a script), or the prompt is declined, or the trust store can't be written,
gwfails closed: the untrusted project value is not used, the command falls back to the global value for that key, and a warning is printed to stderr. The command itself still completes. gw shell-integrationandgw config --listnever trigger this prompt: the former's stdout must stay clean, and the latter only reads the existing trust state to display it.
Use --no-project-hooks to skip project hook overrides entirely for one invocation (no prompt, global values used):
gw start 123 --no-project-hooksgw end --force and gw clean --force/--dry-run also skip project hooks automatically (no prompt) — these are typically scripted/non-interactive invocations where blocking on a prompt would be unwelcome. Note that under any of these skip paths, even an empty-value override (which normally needs no trust) does not apply — global values are used unconditionally.
Check what's currently in effect, including origin and trust state, with:
gw config --listpost_start_hook : pnpm dev [project] [trusted]
post_checkout_hook : (not set) [default]
pre_end_hook : (not set) [global]
An untrusted project override is shown with the effective (global) value plus a note, e.g. [project, untrusted — global value used], rather than being hidden.
- Trust covers the
.gwrcfile only, not any script it references.post_start_hook = ./scripts/setup.shis approved by the content of.gwrc, not the content ofsetup.sh— ifsetup.shchanges later, no re-approval is triggered (the same boundary direnv draws around.envrcand the files it sources). - Relative-path hook values are resolved against the worktree's cwd at hook-execution time, not the main root where
.gwrcwas approved. Sincegwchanges into the new/target worktree before runningpost_start_hook/post_checkout_hook(and into the worktree being removed forpre_end_hook), a relative path actually runs whatever file exists at that path on the checked-out branch — which can differ from what you approved if you check out a different branch afterward. Prefer absolute paths for hook scripts (post_start_hook = /abs/path/to/scripts/setup.sh) to avoid this entirely. - Hook values must fit on one physical line — the config parser does not support line continuations. Wrap multi-command hooks in a script file and reference it by path.
See examples/hooks/README.md for a worked example.
Shell integration is what makes auto_cd work: after gw start or gw checkout prints the worktree path to stdout, the shell wrapper reads it and runs cd for you in the current shell process.
Add one of these lines to your shell configuration file:
# For Bash (~/.bashrc)
eval "$(gw shell-integration --show-script --shell=bash)"
# For Zsh (~/.zshrc)
eval "$(gw shell-integration --show-script --shell=zsh)"
# For Fish (~/.config/fish/config.fish)
gw shell-integration --show-script --shell=fish | sourceThis method ensures you always have the latest shell integration code. See SHELL_INTEGRATION.md for full details.
gw start doesn't change my directory
Shell integration is not set up. Add the eval line for your shell to your shell config file and reload it — see Shell Integration above.
gw asks me to trust a .gwrc
The repository you cloned ships a project-local hook file. gw shows the hook values before running them. Approve to run the hooks, decline to fall back to your global config, or pass --no-project-hooks to skip project hooks for that invocation without being prompted.
gw end refuses to remove my worktree
The safety checks found uncommitted changes, unpushed commits, or a branch not yet merged into the base branch. gw end prints the specific reason(s). Resolve them first, or use gw end --force to override all checks.
How do I skip the automatic fetch?
Pass --no-fetch to any command for a one-off skip, or set fetch_before_command = false in ~/.gwrc to disable it permanently.
gw/
├── cmd/ # Command implementations (start, checkout, end, clean, config, …)
├── examples/hooks/ # Example hook scripts (tmux, iTerm2, docker-compose, etc.)
├── internal/
│ ├── config/ # Configuration loading, saving, and fieldSpecs table
│ ├── detect/ # Package-manager detection and setup
│ ├── git/ # Git operations via CLI subprocess (no go-git)
│ ├── hook/ # Lifecycle hook execution
│ ├── iterm2/ # iTerm2 tab-name integration
│ ├── spinner/ # Terminal spinner for long-running operations
│ ├── trust/ # Trust store for project-local hook approval
│ └── ui/ # Interactive TUI components (worktree/branch selector)
├── main.go
└── go.mod
go build -o gw# Run all tests
go test ./...
# Run tests with coverage
make test
# Generate coverage report
make coverage
# View coverage in terminal
make coverage-reportRun make fmt && make check as the pre-commit gate — format first, then lint + tests.
MIT