# Parallel Claude Code Sessions in Laravel: A Copied .env Shares Your MySQL Database

**Author:** Mozex | **Published:** 2026-09-28 | **Tags:** Claude Code, Developer Tools, Laravel, PHP, Open Source | **Package:** mozex/laravel-worktree | **URL:** https://mozex.dev/blog/28-parallel-claude-code-sessions-in-laravel-a-copied-env-shares-your-mysql-database

---


I'd have a Claude Code session working in a project and want to get on with something else while it ran. A second session in the same checkout wasn't the answer: neither session would know the other was working, so they'd interfere with each other's work and end up reverting each other's changes. Running two test commands at once caused problems too, because both ran against one database.

Git worktrees fix the files: each session gets its own directory and branch. In a worktree with a copied `.env`, one `migrate:fresh` took the main checkout's `users` table in my test app from 1,000 rows to 0. I built [mozex/laravel-worktree](https://mozex.dev/docs/laravel-worktree/v1) for this problem: one command gives each worktree its own database, test database, `.env` and Herd site. If you'd rather script it yourself, read [the section on unsetting the parent's environment](#if-you-script-it-yourself-unset-the-parents-environment) first, because the obvious Artisan command still emptied the main database in my test.

Everything here ran on a fresh Laravel 13.33 app with MySQL 8.4 and PHP 8.5, served by Laravel Herd 1.30 on Windows 11, with version 1.6.0 of the package (it supports Laravel 11 to 13, PHP 8.2+, MariaDB and PostgreSQL; `WORKTREE_HERD=none` skips Herd).

<!--more-->

## On MySQL, a copied .env shares the main database

Claude Code creates worktrees itself. `claude --worktree feature-a` checks out a new branch under `.claude/worktrees/feature-a` and starts a session there. A fresh checkout has no `.env`, so [the Claude Code docs](https://code.claude.com/docs/en/worktrees) suggest a `.worktreeinclude` file that copies gitignored files like `.env` into every new worktree.

A copy is the problem: the worktree's `.env` still says `DB_DATABASE=wtpost` and `APP_URL=https://wtpost.test`, the same as the main checkout. On Laravel's default SQLite, an unset `DB_DATABASE` resolves to `database/database.sqlite` inside each checkout, so the worktree points at its own file, not the main checkout's.

I built the MySQL case by hand on a test app: `git worktree add` into `.claude/worktrees/feature-a`, the `.env` copied in, `composer install`, and 1,000 users in the main app's database. The app has a `/whoami` route that prints its checkout directory, database name, user count and `APP_URL`. Then I ran `migrate:fresh` inside the worktree:

```text
$ curl -s https://wtpost.test/whoami
{"checkout":"wtpost","database":"wtpost","users":1000,"app_url":"https:\/\/wtpost.test"}

$ cd .claude/worktrees/feature-a && php artisan migrate:fresh --force

 Dropping all tables .. 76.34ms DONE
 ...

$ curl -s https://wtpost.test/whoami
{"checkout":"wtpost","database":"wtpost","users":0,"app_url":"https:\/\/wtpost.test"}
```

Nothing in the `migrate:fresh` output names the database it dropped.

The URL is shared as well. The worktree's `APP_URL` points at `wtpost.test`, which Herd serves from the main checkout, and the worktree itself isn't served at all: Herd serves parked paths and linked directories, and `.claude/worktrees/` is neither. A session that opens the app in a browser to check its change is looking at the main checkout's code.

## Two test runs, one test database

Tests collide the same way when `phpunit.xml` names a real database. I pointed the test app's suite at `wtpost_testing`: 302 tests, 300 of them using `RefreshDatabase`, which runs `migrate:fresh` before a run's first test. On its own the suite passed. Then I started it in the main checkout and in the worktree at the same moment, three times. Five of the six runs failed, with 2 to 55 errors per run, each one about a table that didn't exist or already existed:

```text
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'wtpost_testing.migrations' doesn't exist
SQLSTATE[42S01]: Base table or view already exists: 1050 Table 'users' already exists
```

Two runs inside one checkout need a different fix, which became [mozex/laravel-test-lanes](https://mozex.dev/docs/laravel-test-lanes/v1).

## One command gives each worktree its own databases and Herd site

Cloning the project again for every feature would have meant creating its databases and linking a new Herd site each time. I found that frustrating and automated it. From the main checkout:

```bash
composer require mozex/laravel-worktree --dev
php artisan worktree:setup feature/login
```

`worktree:setup` creates the worktree next to the main checkout and links it in Herd, so `wtpost` on `feature/login` becomes `wtpost-feature-login.test`. It copies `.env`, rewrites the database name and the main host (`wtpost.test`, but not subdomains of it), and installs dependencies. Then it creates an application database and, when `phpunit.xml` points at a database server, a test database whose name it writes into that file. Last, it migrates and runs your build steps. On the test app it ended with this table (path shortened, borders removed):

```text
  Path            .../scratch/wtpost-feature-login
  Branch          feature/login
  URL             https://wtpost-feature-login.test
  Database        wtpost_feature_login
  Test database   wtpost_feature_login_testing
```

The `phpunit.xml` change is marked `skip-worktree` in the worktree's own git index, so `git status` stays empty and the test database name never lands in a commit.

Then I repeated both experiments in the new worktree. `migrate:fresh` there left the main app's `users` table at the 1,594 rows I'd reseeded it with. The two suites, started together three more times, passed six runs out of six, the main checkout's on `wtpost_testing` and the worktree's on `wtpost_feature_login_testing`.

Three setups on the same app, with warm Composer and npm caches and the Herd step left out (more on that below), took 24.8 to 25.9 seconds, most of it `composer install` and `npm ci`. With dependency copying turned on (`WORKTREE_COPY_VENDOR` and `WORKTREE_COPY_NODE_MODULES`), `vendor` and `node_modules` are copied from the main checkout whenever the lock files match byte for byte, and three setups took 10.6 to 11.0 seconds.

## Finishing a branch drops what setup created

`worktree:teardown` asks how you want to finish, or takes a flag:

| Flag | What happens before cleanup |
|---|---|
| `--pr` | Commits pending changes, pushes, opens a pull request with `gh` |
| `--into=main` | Merges the branch into `main` from the main checkout |
| `--abandon --force` | Throws the branch away |

Cleanup is the same each time: it drops the worktree's application and test databases, removes the Herd site and the worktree, and deletes the branch, except after `--pr`, where the pull request still needs it. It also drops the `_test_1` and `_test_lane1` style databases that `php artisan test --parallel` and Test Lanes create beside the test database. The names come from the worktree, not from the copied `.env`, and teardown refuses to drop a database that matches the main `.env`'s `DB_DATABASE`. Six teardowns, also without Herd, took 7.5 to 7.9 seconds, and afterwards the only `wtpost` databases on the server were `wtpost` and `wtpost_testing`.

## If you script it yourself, unset the parent's environment

This looks like a job for a small Artisan command: add the worktree, write its `.env` with a new `DB_DATABASE`, create that database, then run `composer install` and `migrate:fresh` inside the worktree with the `Process` facade. I wrote that command in the test app's `routes/console.php`:

```php
Artisan::command('naive:worktree {branch}', function (string $branch) {
    $path = dirname(base_path())."/wtpost-{$branch}";
    $database = "wtpost_{$branch}";

    Process::run(['git', 'worktree', 'add', $path, '-b', $branch])->throw();

    $env = file_get_contents(base_path('.env'));
    file_put_contents("{$path}/.env", preg_replace('/^DB_DATABASE=.*$/m', "DB_DATABASE={$database}", $env));

    DB::statement("CREATE DATABASE IF NOT EXISTS `{$database}`");

    Process::path($path)->run(['composer', 'install'])->throw();
    Process::path($path)->run(['php', 'artisan', 'migrate:fresh', '--force'])->throw();
});
```

`php artisan naive:worktree leak` exited 0. The new `.env` said `DB_DATABASE=wtpost_leak`, `wtpost_leak` had no tables, and the main app's users table had gone from 1,594 rows to 0.

Three things line up to cause it. Laravel's environment loader (phpdotenv, set up in `Env::getRepository()`) writes the `.env` values into `$_ENV`, `$_SERVER` and, through `putenv()`, the process environment. Symfony's Process, which sits under Laravel's facade, builds a child's environment from `$_ENV` and `getenv()`, so the child starts with all of them. And phpdotenv, as Laravel sets it up, doesn't overwrite a variable that's already set, so the child `php artisan` keeps the parent's `DB_DATABASE`. matt-h reported this in 2022 as [laravel/framework#44278](https://github.com/laravel/framework/issues/44278), with a workaround; nunomaduro of the Laravel team replied that processes inherit the environment by default, and that `false` removes a variable, as [the Process docs](https://laravel.com/docs/processes) also say.

That issue's workaround is the fix. Unset every key of the parent's `.env` for the child:

```php
$unset = array_fill_keys(array_keys(Dotenv::parse($env)), false);

Process::path($path)->env($unset)->run(['php', 'artisan', 'migrate:fresh', '--force'])->throw();
```

I reseeded the main table with 500 users and ran a copy of the command with `use Dotenv\Dotenv;` at the top and `->env($unset)` on both calls. It migrated the new database and left all 500 in place. The package does the same for every command it runs inside a worktree.

## Windows asks before Herd links, and Redis needs one config line

On Windows, Herd creates a site link from an elevated PowerShell (`Start-Process ... -Verb RunAs` in its CLI; Herd's [Windows changelog](https://herd.laravel.com/docs/windows/changelog) for 1.0.1 says "`herd link` now prompts for elevated permissions"). With Windows' default UAC settings, expect a prompt each time a worktree is linked. That's why my timings leave the Herd step out.

And the package doesn't know about your Redis prefix. Two worktrees with the same `REDIS_PREFIX` share Redis keys. After `php artisan vendor:publish --tag=worktree-config`, `env.replace` in `config/worktree.php` rewrites any key you list:

```php
'replace' => [
    'REDIS_PREFIX' => '{value}{slug}_',
],
```

`{value}` is the key's current value and `{slug}` the worktree's name with underscores, so `REDIS_PREFIX=wtpost_` in the main `.env` became `wtpost_wtpost_feature_login_` in the worktree.

## Starting a Claude Code session in the new worktree

`--print-path` sends progress to stderr and prints only the new path, so this creates the worktree and moves you into it:

```bash
cd "$(php artisan worktree:setup feature/login --print-path)"
claude
```

Claude Code's own `--worktree` flag, its subagent worktrees and the desktop app's parallel sessions still make plain worktrees and copy files through `.worktreeinclude`, so with `.env` in that file they share the main MySQL database. Start a session that needs its own database with the two lines above. I create my worktrees from a Warp tab button that runs the same setup-and-`cd` step in Git Bash on Windows (the config is in the docs); if Claude Code feels slow in Git Bash, [I measured two fixes for that](https://mozex.dev/blog/24-two-fixes-made-claude-code-15x-faster-per-command-on-windows). The package also ships a Laravel Boost skill that gives Claude Code the package's commands and tells it to prefer them over setting up a worktree by hand.

What else do your parallel sessions trip over? If it's something the package should handle, I'd like to hear about it.