Rails::Worktrees
rails-worktrees adds Rails-friendly bin/wt and bin/ob commands for creating Git worktrees and opening the right local browser URL for each worktree.
Requirements
- Ruby >= 3.2.0
- Rails >= 7.1, < 8.2
- Git
Installation
bundle add rails-worktrees --group development
bin/rails generate worktrees:install
# or, to also generate bin/ob without the yolo follow-ups:
bin/rails generate worktrees:install --browser
# or, to apply the common Procfile.dev + Puma + mise follow-ups automatically:
bin/rails generate worktrees:install --yolo
# optionally, to let the pi-worktree extension run bin/wt setup in new Pi worktrees:
bin/rails generate worktrees:install --piThe generated initializer writes app config under Rails.application.config.x.rails_worktrees, so the file stays safe to load even in environments like test where the gem is only bundled for :development. When rails-worktrees is loaded, the gem applies those settings for both the CLI and in-process Rails usage.
The installer adds:
-
bin/wt— a mise-aware bootstrap wrapper that executes the gem-owned CLI -
bin/ob— an optional browser helper generated by--browseror--yolo -
config/initializers/rails_worktrees.rb— optional configuration -
Procfile.dev.worktree.example— a copy-paste helper for${DEV_PORT:-3000}inProcfile.devon regular installs - a safe update to
config/database.ymlfor common development/test database names -
.pi/worktree.json— only with--pi; see Pi worktrees
With --yolo, the installer also:
- generates
bin/ob - skips
Procfile.dev.worktree.example - replaces the existing
web:entry inProcfile.devwith the DEV_PORT-aware command whenProcfile.devalready exists - updates
config/puma.rbto useport ENV['DEV_PORT'] || ENV.fetch('PORT', 3000)when it still uses a supported defaultPORTbinding - updates
mise.tomlor.mise.tomlto load.envfrom[env]when either file already exists
Usage
bin/wt # auto-pick a name from bundled *.txt lists
bin/wt my-feature # use an explicit worktree name
bin/wt --skip-setup my-feature # create now, run setup later
bin/wt --dry-run my-feature # preview the full setup without changing anything
bin/wt --print-env my-feature # preview DEV_PORT and WORKTREE_DATABASE_SUFFIX
bin/wt setup # rerun setup for the current checkout/worktree
bin/wt setup my-feature # rerun setup for a managed worktree by name
bin/wt setup ../my-project.worktrees/my-feature # setup a specific checkout without cd-ing into it
bin/wt doctor # audit install/config drift and basic worktree health
bin/wt update --dry-run # preview safe maintenance fixes
bin/wt update # apply safe maintenance fixes for managed files
bin/wt update --pi # also add the pi-worktree hook in .pi/worktree.json
bin/wt teardown --dry-run # preview this worktree's Overmind and development/test databases
bin/wt teardown # stop Overmind and drop worktree databases; keep checkout/branch
bin/wt remove my-feature # teardown a worktree, remove it and delete its local branch
bin/wt delete my-feature # alias for `bin/wt remove`
bin/wt remove --force my-feature # also delete an unmerged local branch
bin/wt prune # remove merged worktrees created by wt
bin/wt help # same as --help; never creates a worktree
bin/wt version # same as --version
bin/ob # open http://localhost:$DEV_PORT/
bin/ob contact # open http://localhost:$DEV_PORT/contact
bin/ob '/contact?from=nav' # open a local route with query params
bin/ob --print-url '?from=nav' # print the resolved URL without opening a browserOptions
| Flag | Description |
|---|---|
-h, --help, help
|
Show the help message |
-v, --version, version
|
Show the script version |
--dry-run [name] |
Preview worktree creation or cleanup without changing anything |
--skip-setup |
Create a worktree without running setup steps |
--force |
Force branch deletion for bin/wt remove / bin/wt delete
|
--env, --print-env <name>
|
Preview DEV_PORT and WORKTREE_DATABASE_SUFFIX
|
--pi |
With bin/wt update, create or merge .pi/worktree.json for the pi-worktree extension |
Default behavior
By default bin/wt:
- creates a sibling directory next to your app:
workspace/my-project.worktrees/<name> - uses a branch named
🚂/<name> - creates new branches from
origin's default branch - auto-picks names from bundled
.txtfiles when no explicit name is given - retires bundled names so they are not picked twice
- bootstraps a worktree-local
.envwith deterministicDEV_PORTandWORKTREE_DATABASE_SUFFIXvalues - runs setup automatically after creation: credential linking,
bundle install,yarn installwhen applicable, the Playwright Chromium build for the pinnedplaywrightpackage when it is missing, bothdb:preparesteps, test asset precompile, and a finalbin/rails assets:clobber
workspace/
├── my-project/
└── my-project.worktrees/
├── feature-auth/
├── bugfix-123/
└── experiment/
WT_WORKSPACES_ROOT or config.workspace_root overrides the destination root and uses the layout <root>/<project>/<name>.
Worktrees inside the repository
Set config.worktrees_dir to keep worktrees inside the main checkout instead, in <main checkout>/<worktrees_dir>/<name>. This is the layout the pi-worktree extension uses, so bin/wt and Pi share one place for worktrees:
Rails.application.config.x.rails_worktrees.tap do |config|
config.worktrees_dir = '.worktrees'
config.branch_prefix = 'worktree' # wt and pi-worktree both create worktree/<name>
endmy-project/
├── app/
├── config/
└── .worktrees/
├── feature-auth/
└── bugfix-123/
-
worktrees_dirmust be a relative path inside the repository, such as.worktreesortmp/worktrees. It always resolves against the main checkout, also when you runbin/wtfrom inside a worktree. - If
.gitignoredoes not already ignore the directory,bin/wtadds/.worktrees/to.git/info/excludewhen it creates a worktree, sogit statusin the main checkout stays clean. When it cannot write that file it prints a warning; add the directory to.gitignoreyourself. -
bin/wt remove,bin/wt pruneandbin/wt doctorwork in this layout too.bin/wt doctorwarns when the directory is not ignored. -
worktrees_dircannot be combined withworkspace_root.WT_WORKSPACES_ROOTstill overrides both for a single run. - Tools that scan the main checkout's file tree (for example RuboCop, file watchers or Tailwind content globs) can pick up files from the worktrees. Exclude the directory there if needed.
Setup command
bin/wt setup reruns setup for the current checkout. Run it from inside a linked worktree created by bin/wt, a worktree created manually with git worktree, or a checkout prepared by another tool.
If the checkout was created by bin/wt, you can also point at it by managed worktree name from the main app checkout:
bin/wt setup my-featureIf you do not want to cd first, pass an explicit checkout path:
bin/wt setup ../my-project.worktrees/my-featureThis is the recovery path when you want to create first and bootstrap later:
bin/wt --skip-setup my-feature
cd ../my-project.worktrees/my-feature
bin/wt setupbin/wt setup --dry-run previews the same steps without changing files.
Cleanup commands
bin/wt also supports cleanup commands for worktrees it manages:
-
bin/wt teardown— stop the current worktree's Overmind and drop its development/test databases, keeping the checkout and branch -
bin/wt remove <name>— run the same teardown, remove the named worktree and delete its local branch -
bin/wt delete <name>— alias forbin/wt remove <name> -
bin/wt prune— remove merged worktrees created bybin/wtin bulk
bin/wt remove and bin/wt prune can be run from the main checkout or any sibling worktree in the same repository family.
bin/wt remove refuses to remove the worktree you're currently in, and by default only deletes a local branch after confirming it is already merged into origin's default branch. Use bin/wt remove --force <name> when you intentionally want to delete an unmerged local branch too.
bin/wt prune only targets linked worktrees created by bin/wt whose local branches are already merged into origin's default branch. It skips the main checkout, skips the checkout you're currently in, and asks for confirmation before deleting the batch.
Teardown command
Run bin/wt teardown inside a linked worktree (including manually created or Pi worktrees). It boots that app separately in test and development (so environment-specific files and database URLs are resolved correctly), checks all task-enabled development/test database configurations and Rails' protected-environment metadata, stops Overmind with overmind quit --socket .overmind.sock if that local socket exists, and drops only those validated databases through Rails' database tasks. Production databases and the checkout/branch are left alone. Multi-database configurations (primary, cache, queue, cable, etc.) are supported; replicas and configurations with database_tasks: false are not dropped.
Teardown refuses the main checkout, a missing/blank WORKTREE_DATABASE_SUFFIX, and any database whose name does not contain that suffix. For SQLite, the suffix must be in the filename, not merely a parent directory. All names are validated before Overmind or any database is touched. A working app bundle for both environments and bin/rails are required; install missing gems or fix unsafe database configuration before retrying. Both environments are booted even for a dry-run, so application initializers still execute. Rails' production/environment-mismatch protection cannot be bypassed with DISABLE_DATABASE_ENVIRONMENT_CHECK. The resolved test configuration is passed privately to the development process; all safety checks complete before Overmind is stopped or any database is dropped.
The worktree's .env may define a literal WORKTREE_DATABASE_SUFFIX (unquoted or single/double quoted, optionally with export). Teardown never generates a suffix. When run inside the worktree it can also use the current environment if .env does not define one. remove reads the target's .env and starts Rails with only the caller's OS/Ruby runtime variables, never its suffix, database URLs, application settings or bundle. Any other required application settings/credentials must therefore be loadable by the target app itself (for example with dotenv or Rails credentials). Ensure that target file contains the actual suffix used to create its databases.
bin/wt remove / delete run this same teardown before deleting the worktree or branch. If validation, Overmind shutdown, or a database drop fails, removal aborts and keeps the checkout and branch. Database drops are not transactional: databases already dropped before a later failure cannot be restored by this command. If only the branch remains (the worktree directory is already gone), removal only deletes the branch. prune retains its existing Git-only behavior; run teardown on its candidates first if they have databases.
Use bin/wt teardown --dry-run to list the local Overmind socket and exact development/test database names without stopping or dropping them. To preview both teardown and checkout/branch removal, use --dry-run with bin/wt remove:
bin/wt remove --dry-run feature-authIf you want to see what bin/wt prune would clean up before saying yes, use --dry-run:
bin/wt prune --dry-runMaintenance commands
bin/wt also includes a small maintenance surface for installer drift and checkout health:
-
bin/wt doctor— audit managed files plus basic Git/worktree health without changing anything -
bin/wt update --dry-run— preview safe file-based fixes -
bin/wt update— apply safe file-based fixes for supported managed files
bin/wt doctor checks the generated initializer, bin/wt, optional bin/ob, supported config files such as config/database.yml, Procfile.dev, config/puma.rb, and mise.toml/.mise.toml, plus basic repository/worktree conditions like resolving origin's default branch and spotting stale registered worktree paths.
bin/wt update is intentionally conservative: it only applies safe file-based fixes for managed templates and supported config shapes. It does not delete branches, remove worktrees, or rewrite ambiguous custom files automatically.
bin/wt doctor and bin/wt update only look at .pi/worktree.json once it already runs bin/wt setup, or when you pass bin/wt update --pi.
Pi worktrees (pi-worktree extension)
If you create worktrees from Pi with pi --worktree or /worktree, rails-worktrees can configure them to run bin/wt setup automatically. This is opt-in:
bin/rails generate worktrees:install --pi
# or, in an app that already has rails-worktrees installed:
bin/wt update --piBoth commands create or update .pi/worktree.json:
{
"linkEnvFiles": false,
"postCreate": [
"bin/wt setup"
]
}.pi/worktree.json is read by the pi-worktree npm extension (pi install pi-worktree), not by Pi core. Without that extension, the file has no effect. pi-worktree places worktrees in .worktrees/ inside the repository by default. Set config.worktrees_dir = '.worktrees' and config.branch_prefix = 'worktree' to have bin/wt use the same directory and branch names; see Worktrees inside the repository.
linkEnvFiles: false matters. By default, pi-worktree symlinks the main checkout's gitignored .env* files into a new worktree before postCreate runs. bin/wt setup keeps existing .env values, so every Pi worktree would then reuse the main checkout's DEV_PORT and WORKTREE_DATABASE_SUFFIX, and setup would write through the symlink into the main checkout. With linking disabled, each worktree gets its own .env and database identity.
Existing configuration is merged, not replaced:
- other keys (
dir,branchPrefix,preRemove, …) and custompostCreatehooks are preserved;bin/wt setupis appended once and never duplicated -
linkEnvFilesis only added when it is absent; an explicit"linkEnvFiles": true, invalid JSON, or a non-listpostCreateis left untouched with a message explaining what to change by hand - no
preRemovehook is added automatically, and the worktree directory and branch prefix stay under your control
To opt into environment cleanup when Pi's pi-worktree extension destroys a worktree, add bin/wt teardown to preRemove (preserve any existing hooks):
{
"linkEnvFiles": false,
"postCreate": ["bin/wt setup"],
"preRemove": ["bin/wt teardown"]
}The extension runs preRemove from inside the worktree. Teardown only cleans the environment; the calling tool remains responsible for removing the checkout and branch. Keep linkEnvFiles: false so a worktree does not share the main checkout's .env. Preview the cleanup inside the worktree with bin/wt teardown --dry-run first.
pi-worktree runs each postCreate step with a 120-second timeout. If a first bundle install or db:prepare takes longer, run bin/wt setup again inside the worktree.
Updating old bin/wt wrappers
Versions before 0.7.1 generated bin/wt as a Ruby shebang wrapper. On hosts where Ruby is provided by mise but not already on PATH, that legacy wrapper can fail with /usr/bin/env: ruby: No such file or directory before rails-worktrees can activate mise.
Use the current gem executable once to repair the app-owned wrapper. Scope mise discovery to the checkout and treat in-repository mise configs as trusted for this recovery shell, so mise exec loads Ruby from the same configuration the repaired wrapper will use:
# From the Rails app root
root=$(pwd -P)
trust_root=$(
unset GIT_DIR GIT_WORK_TREE GIT_COMMON_DIR GIT_INDEX_FILE \
GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES
git -C "$root" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$root"
)
trust_root=$(cd "$trust_root" && pwd -P)
MISE_CEILING_PATHS=$(dirname "$trust_root")
MISE_TRUSTED_CONFIG_PATHS="$trust_root"
export MISE_CEILING_PATHS MISE_TRUSTED_CONFIG_PATHS
mise exec -C "$root" -- ruby -v
mise exec -C "$root" -- gem install rails-worktrees --no-document
mise exec -C "$root" -- ruby -e 'load Gem.bin_path("rails-worktrees", "wt")' -- update --dry-run "$root"
mise exec -C "$root" -- ruby -e 'load Gem.bin_path("rails-worktrees", "wt")' -- update "$root"
git status --short
git diff --statReview the diff before committing. wt update applies every safe managed maintenance fix it finds, so the resulting commit can include bin/wt plus other generated installer drift such as the initializer, database configuration, Procfile, Puma config, mise config, or bin/ob.
New installs and repaired apps use a bash bootstrap wrapper that activates mise from the app root before loading Ruby/Bundler, trusting mise configs only inside the repository root and bounding discovery above the checkout. That covers app-local, in-repository higher-scope, local override, and environment-specific mise configuration without trusting an unrelated caller directory or configs above the checkout. The wrapper also falls back to the globally installed wt executable for bootstrap-safe commands such as wt setup --dry-run when the app bundle is incomplete.
Interactive prompts
bin/wt handles several edge cases interactively:
- Branch already exists locally — asks whether to attach a new worktree to it
- Branch already exists on origin — asks whether to create a local tracking worktree
- Target directory already exists with matching branch — asks whether to reuse it
- Target directory already exists with a different branch — asks whether to remove and recreate it
-
Prune found merged worktrees created by
wt— asks whether to delete the batch -
Retired bundled name used explicitly — rejects it and suggests running
wtwith no argument
Name validation
Worktree names must not contain / or whitespace, must not be . or .., and must be a valid Git ref component.
These reserved words are commands, so bin/wt <word> never creates or reuses a worktree with that name and auto-picking skips them: help, version, doctor, setup, update, prune, remove and delete. bin/wt help and bin/wt version behave like --help and --version. An existing worktree with such a name (for example one created by bin/wt help in an older version) can still be cleaned up with bin/wt remove help or set up with bin/wt setup ../<project>.worktrees/help.
Configuration
The installer generates config/initializers/rails_worktrees.rb where you can override:
The initializer stays app-owned and gem-agnostic; rails-worktrees reads those settings whenever the gem is present in the current bundle groups.
| Option | Default | Description |
|---|---|---|
bootstrap_env |
true |
Write .env when creating a worktree |
workspace_root |
nil |
Override the destination root (sibling layout when nil) |
worktrees_dir |
nil |
Create worktrees inside the main checkout, e.g. '.worktrees'; see Worktrees inside the repository |
dev_port_range |
3000..3999 |
Port range for deterministic DEV_PORT allocation |
branch_prefix |
🚂 |
Prefix for worktree branch names |
name_sources_path |
bundled names/
|
Directory containing .txt name lists |
used_names_file |
~/.local/state/rails-worktrees/used-names.tsv |
TSV tracking retired names |
worktree_database_suffix_max_length |
18 |
Max length for generated database suffixes |
Database naming
The installer adds WORKTREE_DATABASE_SUFFIX to common development and test database names in config/database.yml. For a multi-database app, the target shape is:
development:
primary:
database: my_app_development<%= ENV.fetch('WORKTREE_DATABASE_SUFFIX', '') %>_primary
test:
primary:
database: my_app_test<%= ENV.fetch('WORKTREE_DATABASE_SUFFIX', '') %>_primaryIf your database.yml is too custom to patch safely, the installer leaves it alone and tells you what to update manually.
Environment bootstrap
When bin/wt creates a worktree it writes a worktree-local .env with:
-
DEV_PORT— deterministic port derived from the worktree name via CRC32, rotated throughdev_port_range, skipping ports already claimed by peer worktrees -
WORKTREE_DATABASE_SUFFIX— derived from the best available worktree identity (managed name when known, otherwise the current branch or checkout basename). When a readable suffix is already claimed by a peer checkout,bin/wtappends a shortDEV_PORT-based token to keep the databases isolated.
Existing .env values are never overwritten.
Browser helper
If you install with --browser or --yolo, the installer generates bin/ob.
bin/ob reads DEV_PORT from the current app or worktree's .env, falls back to ENV['DEV_PORT'], then falls back to 3000, and opens http://localhost:$DEV_PORT/... in your default browser.
Route examples:
bin/ob
bin/ob contact
bin/ob admin/users
bin/ob '/contact?from=footer'
bin/ob --print-url '?from=footer'bin/ob accepts a single optional local route argument. Query-only routes such as ?from=footer resolve to the app root, and full URLs such as https://example.com are intentionally rejected.
If DEV_PORT is missing from .env, bin/ob tells you when it falls back to ENV['DEV_PORT'] or 3000.
Browser opening currently uses:
-
openon macOS -
xdg-openon Linux and other Unix-like environments that provide it
For scripts and debugging, bin/ob --print-url [route] prints the resolved localhost URL without opening a browser.
By default, the installer does not edit your Procfile.dev, config/puma.rb, or mise config. It generates Procfile.dev.worktree.example with a ready-to-copy line:
web: env RUBY_DEBUG_OPEN=true bin/rails server -b 0.0.0.0 -p ${DEV_PORT:-3000}
If you run bin/rails generate worktrees:install --yolo, the installer applies the three common follow-ups for you when the files already exist:
- skip generating
Procfile.dev.worktree.example - replace the existing
web:entry inProcfile.dev - update
config/puma.rbtoport ENV['DEV_PORT'] || ENV.fetch('PORT', 3000)when it still uses a supported defaultPORTbinding - add
_.file = ".env"to the[env]section ofmise.tomlor.mise.toml
On a regular install, the follow-up message also suggests the same config/puma.rb edit when Puma still uses the default PORT binding.
Use a project-local env loader like mise with _.file = ".env" to keep values scoped per-worktree.
Development
After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.
Smoke testing
RSpec remains the main automated test suite. For installer and integration changes, you can also run a disposable Rails app smoke test:
bundle exec rake smoke_testThis smoke test:
- creates a temporary Rails app from a compatible Rails version
- installs
rails-worktreesfrom the current checkout path - runs
bin/rails generate worktrees:install --yolo - verifies
bin/wt,bin/ob, the generated initializer, that--yoloskips the Procfile example, yolo updates toProcfile.dev,config/puma.rb, andmise.toml,config/database.ymlpatching, and worktree.envbootstrapping - creates a temporary bare
origin, confirmsbin/wt smoke-branchcreates a real worktree, and confirmsbin/wt remove smoke-branchcan remove that merged worktree from a sibling worktree checkout
By default, the script cleans up all temp directories after the run. Set KEEP_SMOKE_TEST_ARTIFACTS=1 to keep them around for debugging, or set RAILS_WORKTREES_SMOKE_RAILS_VERSION to try a different compatible Rails version.
There is also a manually triggered GitHub Actions workflow named Smoke Test for running the same disposable-app verification in CI without slowing down the default pull request checks.
The workflow accepts optional ruby_version, rails_version, keep_artifacts, and verbose inputs. Enable keep_artifacts when you want the disposable app, bare origin, and worktree directories uploaded from CI for debugging, and enable verbose when you want shell tracing in the smoke-test log.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/asjer/rails-worktrees.
License
The gem is available as open source under the terms of the MIT License.