If you're using Claude Code for software development, you've probably hit this wall: you want to work on multiple features simultaneously, but you can only run one Claude session per project at a time. What if you could run three, four, or even five Claude Code sessions in parallel? Each working on a different feature, each with its own running application you can test in your browser.
This is exactly what I achieved with git worktrees and a bit of automation.
The Problem: One Project, One Claude, One Port
When working with Claude Code on a Rails application (or any web app), you typically have:
- One project directory
- One running development server on port 3000
- One Claude Code session
This becomes limiting when you want to:
- Work on a complex feature while Claude investigates a bug in parallel
- Test multiple experimental approaches simultaneously
- Keep one stable version running while iterating on another
The Solution: Automated Git Worktrees
Git worktrees let you check out multiple branches of the same repository into separate directories simultaneously. But setting them up manually is tedious: database isolation, port conflicts, environment configuration, and cleanup all need to be handled.
I built two scripts that automate this entire process.
Creating a Worktree: bin/worktree
bin/worktree feature-authentication
This single command:
- Creates a new worktree in a sibling directory (e.g.,
myapp-feature-authentication) - Assigns a unique port by scanning ports 3001-3099 for availability
- Creates an isolated database with a unique suffix
- Copies necessary configuration files like credentials
- Runs the full setup: bundle install, database creation, schema load, and seeding
The script is smart about port allocation. It checks both system-level port usage (via lsof) and ports claimed by other worktrees (via their .env files). No more port conflicts.
The Magic of Database Isolation
Each worktree gets its own database suffix:
myapp_development → main project
myapp_development_feature_auth → feature-authentication worktree
myapp_development_new_api → new-api worktree
This means you can run migrations, test data changes, and even break things in one worktree without affecting others.
Cleanup Made Simple: bin/worktree_rm
When you're done with a feature branch, cleanup is just as easy:
bin/worktree_rm
This presents an interactive menu of all your worktrees. Select the ones you want to remove, and the script:
- Removes the git worktree
- Deletes the associated branch
- Drops all related databases (including parallel test databases)
No orphaned databases cluttering your PostgreSQL instance.
Pro Tip: Lightning-Fast Database Seeding
Database seeding can be slow. Sometimes painfully slow on large applications. I solved this with a db:seed:restore task that loads from a SQL dump instead of running seed scripts.
The workflow:
- Run
db:seedonce to generate fresh seed data - Export it to a SQL dump
- Use
db:seed:restorein worktrees to load the dump directly
What used to take minutes now takes seconds. This is especially valuable when you're creating multiple worktrees in a day.
The Parallel Claude Code Workflow
With this setup, here's what a typical day looks like:
Terminal 1:
cd myapp
claude # Main Claude session working on urgent bug fix
bin/dev # Running on port 3000
Terminal 2:
cd myapp-feature-auth
claude # Second Claude session building authentication
bin/dev # Running on port 3001
Terminal 3:
cd myapp-api-refactor
claude # Third Claude session refactoring API
bin/dev # Running on port 3002
Each Claude Code session has full context of its worktree. Each application instance is accessible in your browser. You can review Claude's work, test changes, and provide feedback. All at the same time.
Why This Matters for AI-Assisted Development
Claude Code is powerful, but it works best when it can focus on one coherent task. By isolating different features into separate worktrees:
- Context stays clean: Each Claude session only sees relevant changes
- Parallel exploration: Test multiple approaches to the same problem
- Zero conflicts: No merge conflicts from simultaneous edits to the same files
- Easy comparison: Run both versions side-by-side in your browser
The Complete Scripts
Here are the full scripts. Drop them in your bin/ directory and make them executable with chmod +x bin/worktree bin/worktree_rm.
bin/worktree
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/inline"
require "fileutils"
gemfile(true, quiet: true) do
source "https://rubygems.org"
gem "tty-prompt"
end
class WorktreeCreator
PORT_RANGE = (3001..3099).freeze
def initialize(worktree_name)
@worktree_name = worktree_name
@directory_name = worktree_name.tr("/", "-")
@repo_root = `git rev-parse --show-toplevel`.strip
@worktree_base = File.dirname(@repo_root)
@project_name = File.basename(@repo_root)
@worktree_path = File.join(@worktree_base, "#{@project_name}-#{@directory_name}")
@prompt = TTY::Prompt.new
end
def run
validate!
select_base_branch
create_worktree
create_env_file
copy_configuration_files
setup_project
print_summary
end
private
attr_reader :worktree_name, :directory_name, :worktree_path, :worktree_base, :repo_root, :project_name, :prompt,
:base_branch, :port, :db_suffix
def validate!
abort "Error: Directory #{worktree_path} already exists" if Dir.exist?(worktree_path)
end
def select_base_branch
current = `git branch --show-current`.strip
options = [{ name: "main", value: "main" }]
options << { name: "#{current} (current)", value: current } unless current == "main"
@base_branch = prompt.select("Base branch for worktree:", options)
end
def create_worktree
puts "\nCreating worktree at #{worktree_path}..."
Dir.chdir(repo_root) do
system("git", "worktree", "add", worktree_path, "-b", worktree_name, base_branch)
end
end
def create_env_file
@db_suffix = "_#{directory_name.tr('-', '_')}"
@port = find_free_port
File.write(File.join(worktree_path, ".env"), <<~ENV)
DATABASE_SUFFIX=#{db_suffix}
PORT=#{port}
ENV
puts "Created .env with DATABASE_SUFFIX=#{db_suffix}, PORT=#{port}"
end
def copy_configuration_files
copy_configuration_file("credentials.yml.enc")
copy_configuration_file("master.key")
end
def copy_configuration_file(filename)
source = File.join(repo_root, "config", filename)
destination = File.join(worktree_path, "config", filename)
if File.exist?(source)
FileUtils.cp(source, destination)
puts "Copied config/#{filename}"
else
puts "Warning: config/#{filename} not found in source repository"
end
end
def find_free_port
used_ports = collect_used_ports
PORT_RANGE.find { |p| !used_ports.include?(p) } || PORT_RANGE.first
end
def collect_used_ports
(system_used_ports + worktree_claimed_ports).uniq
end
def system_used_ports
PORT_RANGE.select { |p| port_in_use?(p) }
end
def worktree_claimed_ports
pattern = File.join(worktree_base, "#{project_name}-*", ".env")
Dir.glob(pattern).reject { |f| f.start_with?(worktree_path) }.filter_map do |env_file|
::Regexp.last_match(1).to_i if File.read(env_file) =~ /^PORT=(\d+)$/
end
end
def port_in_use?(port)
system("lsof", "-i", ":#{port}", out: File::NULL, err: File::NULL)
end
def setup_project
Dir.chdir(worktree_path) do
run_step("Running bundle install...", "bundle", "install")
run_step("Creating databases...", "bin/rails", "db:create")
run_step("Loading schema...", "bin/rails", "db:schema:load")
run_step("Seeding database...", "bin/rails", "db:seed:restore")
run_step("Creating parallel test databases...", "bin/rake", "parallel:create", "parallel:load_schema")
end
end
def run_step(message, *command)
puts message
system(*command)
end
def print_summary
puts <<~SUMMARY
Worktree created successfully!
Location: #{worktree_path}
Branch: #{worktree_name} (from #{base_branch})
Database suffix: #{db_suffix}
Port: #{port}
To start working:
cd #{worktree_path}
bin/dev # Starts on port #{port}
SUMMARY
end
end
if ARGV.empty?
puts "Usage: bin/worktree <worktree-name>"
puts "Example: bin/worktree feature-auth"
exit 1
end
WorktreeCreator.new(ARGV[0]).run
bin/worktree_rm
#!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/inline"
require "yaml"
require "erb"
gemfile(true, quiet: true) do
source "https://rubygems.org"
gem "tty-prompt"
end
class WorktreeRemover
def initialize
@prompt = TTY::Prompt.new
@repo_root = `git rev-parse --show-toplevel`.strip
@project_name = File.basename(@repo_root)
@db_config = load_database_config
end
def run
worktrees = fetch_worktrees
abort "No worktrees found (excluding main repository)" if worktrees.empty?
selected = prompt_for_selection(worktrees)
abort "No worktrees selected" if selected.empty?
selected.each { |worktree| remove_worktree(worktree) }
puts "\nSuccessfully removed #{selected.size} worktree(s)"
end
private
attr_reader :prompt, :repo_root, :project_name, :db_config
def load_database_config
config_path = File.join(repo_root, "config", "database.yml")
return nil unless File.exist?(config_path)
dev_config = parse_database_yaml(config_path)["development"]
return nil unless dev_config
build_db_config(dev_config)
end
def parse_database_yaml(path)
yaml_content = ERB.new(File.read(path)).result
YAML.safe_load(yaml_content, permitted_classes: [], permitted_symbols: [], aliases: true)
end
def build_db_config(dev_config)
base_name = dev_config["database"].to_s.sub(/_development$/, "").sub(/_test.*$/, "")
{ base_name: base_name, username: dev_config["username"], password: dev_config["password"],
host: dev_config["host"] || "localhost", port: dev_config["port"] || "5432" }
end
def prompt_for_selection(worktrees)
prompt.multi_select(
"Select worktrees to remove:",
worktrees.map { |w| { name: format_worktree(w), value: w } },
per_page: 15,
echo: false
)
end
def format_worktree(worktree)
"#{worktree[:name]} (#{worktree[:path]})"
end
def fetch_worktrees
root = `git rev-parse --show-toplevel`.strip
parse_worktrees(`git worktree list --porcelain`).reject { |w| w[:path] == root }
end
def parse_worktrees(output)
output.each_line.each_with_object([current = {}]) do |line, worktrees|
case line
when /^worktree (.+)$/ then current = { path: ::Regexp.last_match(1) }
when %r{^branch refs/heads/(.+)$}
current.merge!(branch: ::Regexp.last_match(1), name: File.basename(current[:path]))
worktrees << current
end
end.drop(1)
end
def remove_worktree(worktree)
dir_name = worktree[:name].sub(/^#{Regexp.escape(project_name)}-/, "")
databases = find_databases("_#{dir_name.tr('-', '_')}")
puts "\nProcessing: #{worktree[:name]}"
report_databases(databases)
remove_git_worktree(worktree[:path])
remove_git_branch(worktree[:branch])
drop_databases(databases)
puts "Removed '#{worktree[:name]}'"
end
def report_databases(databases)
puts "Finding associated databases..."
return puts "No databases found" if databases.empty?
puts "Found #{databases.size} database(s) to remove:"
databases.each { |db| puts " - #{db}" }
end
def remove_git_worktree(path)
puts "Removing worktree..."
system("git", "worktree", "remove", path, "--force")
end
def remove_git_branch(branch_name)
puts "Removing branch '#{branch_name}'..."
system("git", "branch", "-D", branch_name, out: File::NULL, err: File::NULL)
end
def drop_databases(databases)
return if db_config.nil?
databases.each do |db|
puts " Dropping #{db}..."
system({ "PGPASSWORD" => db_config[:password] },
"psql", "-h", db_config[:host], "-p", db_config[:port].to_s, "-U", db_config[:username],
"-c", %(DROP DATABASE "#{db}";), out: File::NULL, err: File::NULL)
end
end
def find_databases(db_suffix)
return [] if db_config.nil?
pattern = "#{db_config[:base_name]}#{db_suffix}_%"
run_psql_query("SELECT datname FROM pg_database WHERE datname LIKE '#{pattern}';").split("\n").reject(&:empty?)
end
def run_psql_query(query)
`PGPASSWORD=#{db_config[:password]} psql -h #{db_config[:host]} -p #{db_config[:port]} \
-U #{db_config[:username]} -t -A -c "#{query}"`
end
end
WorktreeRemover.new.run
Getting Started
The scripts are written in Ruby and use the tty-prompt gem for interactive menus. They're designed to be application-agnostic. Just drop them in your bin/ directory and adjust the setup commands for your stack.
Key customization points:
PORT_RANGE: Change the port range to fit your environmentsetup_project: Modify the setup steps for your framework (the example shows Rails commands)- Database configuration: The scripts assume PostgreSQL but can be adapted for MySQL or other databases
Conclusion
Git worktrees aren't new, but combining them with automated port management, database isolation, and fast seeding creates a development environment that's perfectly suited for AI-assisted parallel development.
If you're serious about maximizing your productivity with Claude Code, this setup is a game-changer. No more waiting for one feature to finish before starting another. No more manual environment juggling. Just clean, isolated workspaces ready for parallel AI-assisted development.