name: worktree description: Git worktree management skill — creates isolated sibling worktrees with auto-computed ports, .env generation, dependency install, and cleanup. Designed for Turborepo/pnpm monorepos but works with any stack. version: 1.0.0 author: opencode type: skill category: development tags:
Purpose: Create and manage fully isolated git worktrees as sibling directories. Each worktree gets its own port range,
.envfiles, and (optionally) a Zellij terminal session — so you can runmain, a feature branch, and a hotfix simultaneously without anything conflicting.
.env files for each app from a shared .env.template.env.local) from the main worktree# Create a worktree for a new feature branch
bash .opencode/skills/worktree/router.sh create feature/auth
# Create with a custom short name (used for the directory suffix)
bash .opencode/skills/worktree/router.sh create feature/auth feature-auth
# List all worktrees and their paths
bash .opencode/skills/worktree/router.sh list
# Show port assignments for all worktrees
bash .opencode/skills/worktree/router.sh ports
# Remove a worktree after the branch is merged
bash .opencode/skills/worktree/router.sh remove feature-auth
# Remove but keep the branch
bash .opencode/skills/worktree/router.sh remove feature-auth --keep-branch
Worktrees are created as siblings to the main repo, not inside it:
~/Documents/github/
my-app/ ← main worktree (main branch)
my-app-feature-auth/ ← created by: create feature/auth
my-app-bugfix-payments/ ← created by: create bugfix/payments
my-app-experiment-ai/ ← created by: create experiment/ai
Each directory is a full working tree sharing the same .git — completely isolated, with its own .env files and installed packages.
Each worktree is assigned a numeric index derived from how many worktrees exist at the moment create runs. The formula is BASE + (INDEX × 10).
How the index is calculated at creation time:
git worktree list): 1 - 1 = index 0 → main2 - 1 = index 1 → first feature3 - 1 = index 2 → second feature| Index | Worktree | Web | Admin | API | DB | Redis |
|---|---|---|---|---|---|---|
| 0 | main | 3000 | 3001 | 3002 | 5432 | 6379 |
| 1 | first feature | 3010 | 3011 | 3012 | 5442 | 6389 |
| 2 | second feature | 3020 | 3021 | 3022 | 5452 | 6399 |
| 3 | third feature | 3030 | 3031 | 3032 | 5462 | 6409 |
Note: If you remove a worktree, its index slot is freed and will be reused by the next
create. Runportsbefore creating a new worktree to see current assignments.
Run bash .opencode/skills/worktree/router.sh ports to see the live table.
.env.template SetupAdd a .env.template to your repo root (commit it). The script replaces __PLACEHOLDER__ tokens at worktree creation time.
# .env.template
WORKTREE_INDEX=__INDEX__
WORKTREE_NAME=__NAME__
WEB_PORT=__WEB_PORT__
ADMIN_PORT=__ADMIN_PORT__
API_PORT=__API_PORT__
DB_PORT=__DB_PORT__
REDIS_PORT=__REDIS_PORT__
DATABASE_URL=postgresql://postgres:postgres@localhost:__DB_PORT__/myapp
REDIS_URL=redis://localhost:__REDIS_PORT__
NEXT_PUBLIC_API_URL=http://localhost:__API_PORT__
Generated .env files are placed at:
{worktree}/.env — root{worktree}/apps/web/.env — if directory exists{worktree}/apps/admin/.env — if directory exists{worktree}/apps/api/.env — if directory existsLimitation: Per-app
.envgeneration is hardcoded toapps/web,apps/admin, andapps/api. Directories that don't exist are silently skipped. If your monorepo uses different app names, editwt-new.shlines ~148-151 to match your structure.
Add to .gitignore:
.env
.env.local
apps/**/.env
apps/**/.env.local
apps/web, apps/admin)Next.js does not read PORT from .env automatically — pass it via the script:
{
"scripts": {
"dev": "dotenv -e .env -- next dev -p $WEB_PORT"
}
}
Install dotenv-cli once at the workspace root:
pnpm add -Dw dotenv-cli
apps/admin or any Vite app)vite.config.ts:
import { defineConfig, loadEnv } from 'vite';
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), '');
return {
server: {
port: Number(env.WEB_PORT || 5173),
strictPort: true,
},
};
});
apps/api)src/main.ts:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(process.env.API_PORT || 3002);
}
bootstrap();
NestJS picks up .env via @nestjs/config automatically.
turbo.jsonDeclare port env vars so Turbo doesn't use stale cache across worktrees:
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"dev": {
"cache": false,
"env": ["WEB_PORT", "ADMIN_PORT", "API_PORT", "DB_PORT", "REDIS_PORT", "WORKTREE_INDEX"]
},
"build": {
"dependsOn": ["^build"],
"env": ["WEB_PORT", "ADMIN_PORT", "API_PORT"]
}
}
}
Use the same env values for fully isolated DB/Redis volumes per worktree:
# docker-compose.yml
services:
postgres:
image: postgres:16
ports:
- "${DB_PORT:-5432}:5432"
environment:
POSTGRES_DB: myapp_${WORKTREE_NAME:-main}
volumes:
- pgdata_${WORKTREE_INDEX:-0}:/var/lib/postgresql/data
redis:
image: redis:7
ports:
- "${REDIS_PORT:-6379}:6379"
volumes:
pgdata_0:
pgdata_1:
pgdata_2:
pgdata_3:
Run from worktree root:
docker compose --env-file .env up -d
# 1. Create a worktree for a new feature
bash .opencode/skills/worktree/router.sh create feature/checkout feature-checkout
# 2. In the new directory, start everything
cd ~/Documents/github/my-app-feature-checkout
pnpm dev # all apps on unique ports
docker compose --env-file .env up -d # DB + Redis isolated
# 3. Check what's running
bash .opencode/skills/worktree/router.sh list
bash .opencode/skills/worktree/router.sh ports
# 4. Clean up after merge
bash .opencode/skills/worktree/router.sh remove feature-checkout
| Command | Description |
|---|---|
create <branch> [name] |
Create worktree from branch (creates branch if new) |
remove <name> [--keep-branch] |
Remove worktree, stop Docker, kill Zellij session |
list |
Show all worktrees with paths and branches |
ports |
Show port index table for all worktrees |
help |
Show usage |
.opencode/skills/worktree/
├── SKILL.md # This file
├── router.sh # CLI entry point
└── scripts/
├── wt-new.sh # Create worktree logic
└── wt-close.sh # Remove worktree logic
The script detects existing local and remote branches and checks them out instead of creating new ones.
The target sibling directory is already present. Remove it manually or use a different name.
.env generation is skipped. Create a .env.template in your repo root to enable it.
Run ports to see current assignments. If a worktree was removed without pruning, run git worktree prune in the main repo to reclaim the index slot.
Zellij is optional. If not installed, the script prints the cd path and exits normally.