ai/skills/worktree-setup/SKILL.md
Set up git worktree database isolation for a project. Use when adding worktree support, parallel branch development, or worktree database setup. Investigates the project's database config and env loading, then creates bin/setup-worktree and bin/teardown-worktree scripts with per-branch database names.
npx skillsauth add kurko/dotfiles worktree-setupInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Guide the user through adding git worktree database isolation to their project. This enables running multiple branches simultaneously with separate databases, avoiding collisions between worktrees.
Before writing any code, investigate the project. Present findings to the user before proceeding to design.
Read the database config file:
| Framework | Config File | Notes |
|-----------|-------------|-------|
| Rails | config/database.yml | ERB supported, uses env names like development, test |
| Django | settings.py | DATABASES dict, often with dj_database_url |
| Node/Prisma | prisma/schema.prisma | DATABASE_URL env var |
| Node/Knex | knexfile.js | Per-environment config object |
| Phoenix | config/dev.exs, config/test.exs | Elixir config |
Determine:
myapp_development, myapp_test)Identify the env loading mechanism and its file priority order per environment.
Rails with dotenv-rails:
dotenv-rails version.env.development.local > .env.local > .env.development > .env.env.test.local > .env.test > .env (.env.local is EXCLUDED in test)bundle show dotenv-rails then read lib/dotenv/rails.rb, look for dotenv_files..env.development.local + .env.test.local (NOT .env.local)Node frameworks:
.env.local support (loaded in all modes including test).env.local is EXCLUDED in test (same behavior as dotenv-rails).
Use .env.test.local for test overrides..env by default, manual loading for othersDjango:
django-environ or python-dotenv: typically single .env, no per-environment splitOther:
direnv (.envrc), asdf (.tool-versions), or framework-specific mechanismsFind files that are gitignored but required for the app to start:
git ls-files --others --ignored --exclude-standard | head -30
Common boot-critical gitignored files:
config/master.key, config/credentials/*.key.env.local (if it contains non-DB secrets)These files must be copied into new worktrees by bin/setup-worktree (for manual
git worktree add) and listed in .worktreeinclude (for Claude Code worktrees).
Check how the project installs dependencies and whether the config is local:
Ruby/Bundler:
.bundle/config for BUNDLE_PATH — if set (e.g., vendor/bundle), this
file is gitignored and won't exist in the worktree. The setup script must copy it.bundle config list — if only the global config sets
the path, no copy is needed.bundle check || bundle install before any bin/rails
command. Worktrees share git history but NOT installed gems.Node:
package-lock.json (npm) or yarn.lock (Yarn)npm ci or yarn install before any framework commands.
Worktrees do NOT share node_modules/.Python:
requirements.txt, Pipfile.lock, pyproject.tomlRead existing setup/bootstrap scripts to match conventions:
bin/setup, bin/dev, Makefile, script/bootstrapsystem!, APP_ROOT, etc.)Check for local dev server routing tools:
~/.puma-dev/ directory (macOS). Symlink-based app discovery.Procfile, package.json scriptsPresent the plan to the user for approval before implementing. Cover:
Use a suffix approach with a DB_SUFFIX env var:
{base_name}_{slug}
Example for branch feature/user-dashboard:
myapp_development_feature_user_dashboardmyapp_test_feature_user_dashboardThe base names stay visible and readable in the database config file.
Slug derivation from branch name:
_ + 6-char SHA1 hash of the original branch nameMax slug length: Use a fixed conservative constant (e.g., MAX_SLUG = 40).
This leaves room for any reasonable database name prefix (e.g., myapp_development_)
within PostgreSQL's 63-char identifier limit or MySQL's 64-char limit. Do NOT
hardcode the database name prefix in the script — the script should not need to
know the database naming structure (that belongs in database.yml or equivalent).
PostgreSQL silently truncates identifiers over 63 chars, which breaks idempotency
checks that grep for the full name.
Use the project's EXISTING env loading mechanism. Do not invent custom file formats.
For Rails with dotenv-rails:
.env.development.local sets DB_SUFFIX=<slug> (loaded in development).env.test.local sets DB_SUFFIX=<slug> (loaded in test)database.yml appends the suffix: myapp_development<%= "_#{ENV['DB_SUFFIX']}" if ENV['DB_SUFFIX'].present? %>For Node with framework-native .env.local:
.env.local sets DB_SUFFIX=<slug> (or overrides DATABASE_URL directly)For other frameworks:
Present a list:
.env.local and .env.*.local if not presentLanguage: Match the project's bin/setup convention. Ruby for Rails, bash for
Node/Python. If bin/setup is Ruby, use Pathname, system!, APP_ROOT — same patterns.
Requirements (the script MUST):
Detect linked worktree: Compare git rev-parse --git-common-dir (stripped of
/.git suffix) with git rev-parse --show-toplevel. If equal, this is the main
worktree — print a message and exit (do not abort; this allows safe use from hooks).
GOTCHA: git rev-parse --git-common-dir returns a RELATIVE path (.git) in
the main worktree but an absolute path in linked worktrees. Always call
File.expand_path (Ruby) or realpath (bash) on the result BEFORE comparing or
stripping the /.git suffix. Without this, the regex /.git$ won't match the
bare .git and the main worktree guard silently fails — the script proceeds to
create databases and env files for the main branch.
Copy files listed in .worktreeinclude: Read the .worktreeinclude file from
the project root. For each listed path, check if it exists in the current worktree.
If not, copy it from the main worktree. Skip blank lines and lines starting with
#. This is the single source of truth for which files to copy — do NOT hardcode
file paths in the script.
Derive the main worktree path by stripping /.git from --git-common-dir —
the same derivation used in linked_worktree?. Both methods MUST share this logic
(DRY: linked_worktree? should delegate to main_worktree_path):
def main_worktree_path
common_dir = File.expand_path(`git rev-parse --git-common-dir`.strip)
common_dir.sub(%r{/\.git\z}, "")
end
def linked_worktree?
main_worktree_path != `git rev-parse --show-toplevel`.strip
end
GOTCHA: --git-common-dir vs --git-dir. --git-common-dir returns just
.git (or /abs/path/.git), NOT .git/worktrees/<name> — that's --git-dir.
The regex must strip just /.git\z, NOT /.git/worktrees/.*\z. Using the wrong
regex silently returns the .git directory as the "main worktree path", causing
file copies to look inside .git/ instead of the project root.
Copy bundler/package manager config: If the project uses a local dependency
path (e.g., .bundle/config with BUNDLE_PATH), copy the config file from the
main worktree so that bundle install respects the same path. Check if the file
exists in the main worktree's project-level config directory — if it's only set
globally, no copy is needed.
Install dependencies and build assets: Run bundle check || bundle install
(Ruby) or npm ci / yarn install (Node) BEFORE any framework commands. Worktrees
share git history but NOT vendor/bundle or node_modules/. This step must come
after copying the bundler config (step 3) so the install respects the correct path.
CRITICAL: Build frontend assets after installing JS dependencies. Worktrees do
NOT share app/assets/builds/ (or equivalent compiled output directories). Without
building, any test that renders a view will fail with asset-not-found errors (e.g.,
Sprockets::FileNotFound: couldn't find file 'application.js'). Check package.json
scripts for build and build:css (or similar) and run them. For Rails with
esbuild/tailwind: yarn build && yarn build:css. For Vite: npx vite build.
Derive slug from branch: Sanitize, collapse, truncate (see Design section).
For the SHA disambiguator on long branches, use a hash of the branch NAME
(Digest::SHA1.hexdigest(branch_name)[0,6]), NOT git rev-parse HEAD. Using
HEAD means the slug changes on every commit, breaking idempotency — setup would
create a new database after each commit.
Write env override files: Set DB_SUFFIX=<slug>. Behavior per file:
DB_SUFFIX=<slug>DB_SUFFIX=: append DB_SUFFIX=<slug>DB_SUFFIX=: skip (print message)Check databases independently: Check BOTH dev and test databases before creating.
For Rails, use bin/rails runner to attempt a connection per environment:
system({ "RAILS_ENV" => env }, "bin/rails", "runner",
"ActiveRecord::Base.connection_pool.with_connection { }",
out: File::NULL, err: File::NULL)
This is more reliable than parsing psql -lqt because it uses the actual
Rails config with the suffix applied.
Create databases and load schema:
db:create db:schema:load db:seedRAILS_ENV=test db:create db:schema:loaddb:schema:load not db:migrate — faster, no need to replay history.DB_SUFFIX explicitly in the environment hash for every
bin/rails command. Do NOT rely on dotenv loading the .env.*.local files —
this makes the dependency implicit and breaks if someone runs the command
outside of the setup script.RAILS_ENV explicitly — Rails db:create without RAILS_ENV
creates BOTH dev and test databases at once, but db:schema:load only loads
the development schema. This leaves the test database empty. Always run each
environment separately with explicit RAILS_ENV.puma-dev (optional): If ~/.puma-dev/ exists, create symlink
~/.puma-dev/{app}-{slug} pointing to the worktree directory. Convert
underscores to hyphens in the slug for domain names. Print the resulting
URL so the user can access it, e.g.: ==> puma-dev: https://{app}-{slug}.test
Print a final summary that includes:
The script MUST NOT:
Requirements:
bin/teardown-worktree <branch-name>". This prevents
accidentally deriving the slug from main and dropping the wrong databases.DB_SUFFIX explicitly in the environment hash for every bin/rails command.env.development.local, .env.test.local)Modify the database config to append DB_SUFFIX when present:
Rails (config/database.yml):
development:
<<: *default
database: myapp_development<%= "_#{ENV['DB_SUFFIX']}" if ENV['DB_SUFFIX'].present? %>
test:
<<: *default
database: myapp_test<%= "_#{ENV['DB_SUFFIX']}" if ENV['DB_SUFFIX'].present? %>
Zero behavioral change when DB_SUFFIX isn't set (main worktree).
Prisma (prisma/schema.prisma):
datasource db {
url = env("DATABASE_URL")
}
(Already ENV-driven; the setup script writes the full URL to .env.local.)
Created in the project root, checked into git. Lists gitignored files that should
be copied into new worktrees — one path per line, blank lines and # comments allowed.
config/master.key
Rules:
.env.development.local, .env.test.local)
— these are generated by bin/setup-worktree with branch-specific valuesvendor/bundle, node_modules — too large, package managers handle these.bundle/config here — it's handled by the dependency step (requirement 3)This file serves TWO consumers:
--isolation worktreebin/setup-worktree reads it (requirement 2) to copy the same files for manually
created worktreesBoth consumers use the same file so there is a single source of truth. The setup script
MUST NOT hardcode file paths — it should read .worktreeinclude to decide what to copy.
Ensure these patterns are present (append if missing):
.env.local
.env.*.local
After implementation, verify by walking through this checklist:
bin/setup-worktree — should print "not a linked worktree" and exitgit worktree add ../project-test-wt -b test-worktree-setupcd ../project-test-wt && bin/setup-worktree
DB_SUFFIXbundle exec rspec (or equivalent) in the worktree — uses worktree-specific test DBmake server (or equivalent) — uses worktree-specific dev DBbin/setup-worktree again — skips existing databases, skips existing env varsbin/teardown-worktree — drops databases, removes env filescd .. && git worktree remove project-test-wt
Note: This will likely require --force because untracked files (vendor/bundle,
copied secrets, generated env files) make the worktree "dirty". This is expected.
Run teardown first to clean up env files and databases, then force-remove.These are acceptable to leave for future work:
/storage.data-ai
Merge the current worktree branch into main and sync main back. Use when the user says "merge to main", "ship it", "merge and continue", or after completing a task in a worktree and wanting to continue with the next one.
tools
Synchronize AI agent skills, commands, configs, permissions, hooks, and instructions across Claude Code, Codex CLI, and other Agent Skills-compatible tools. Use when the user asks to pull skills from Claude into Codex, sync Codex work back to Claude, migrate agent commands, reconcile frontmatter, update permissions, or keep agent setup files in parity.
testing
Write or update UI-independent use cases for QA. Use when the user says "write use cases", "add use cases", "QA use cases", "update use cases", "compose use cases", or when starting implementation of a new feature (after plan approval). Also activates for "what should we test", "regression cases", or "use cases for QA".
documentation
Skill on how to write a task. Use when user asks you to write a task (for Asana, Linear, Jira, Notion and equivalent). Also activates when user says "create task", "write task", or similar task creation workflow requests.