Originally published at hafiz.dev
Herd 2 came out on 5 November, and its headline feature is workspaces: "isolated copies of a site that live on their own git branch and are served on their own domain." The pitch is parallel coding agents. Give each agent its own copy of the app and let them work without stepping on each other.
I ran parallel Claude Code sessions on git worktrees back in March, and the files were never the hard part. The hard part was everything the files point at. So I installed Herd 2.1.0 (Homebrew already ships 2.1), created a fresh Laravel 13.40 app, made five workspaces from it and tried to break them. Herd prints a database warning every time you create one, so I tested whether it holds. And the database wasn't the only thing two copies of the app ended up sharing. All output below is real.
What a workspace actually is
The app lives in ~/Herd/orders, so Herd serves it at orders.test. One command creates a workspace:
$ herd workspace feature-a
✓ Creating Workspace
✓ Cloning Directory (this can take a moment)
✓ Linking Site
✓ Updating .env
✓ Finishing Up
Workspace [feature-a] is ready at [http://feature-a.orders.test].
WARNING The workspace shares the database of [orders] — migrations and destructive operations affect the parent site.
Pass `--database=clone` for a copy of the parent database or `--database=empty` for a fresh one.
cd "/Users/hafizzeeshanriaz/Library/Application Support/Herd/devtools/clones/8543c00e/feature-a-aa198b31"
It isn't a git worktree. Herd makes a copy-on-write clone of the whole folder (APFS clones files without duplicating the bytes until one side changes them), puts it in Herd's own application support folder, and gives it a full .git directory on a new branch named after the workspace. The new site answers at feature-a.orders.test straight away.
The clone takes everything, untracked files included. So the workspace has its own .env and its own vendor/. Herd then rewrites the .env, but only the keys that already exist. Here's the whole difference between the two files:
$ diff .env <workspace>/.env
5c5
< APP_URL=http://localhost:8000
---
> APP_URL=http://feature-a.orders.test
One line. Herd's workspace docs say it also touches SESSION_DOMAIN, SANCTUM_STATEFUL_DOMAINS and DB_DATABASE when those are set, and only DB_DATABASE when the workspace gets its own database. A fresh Laravel app sets none of them.
The database warning, tested
Every herd workspace run ends with the same warning: the workspace shares the parent's database, so migrations and destructive operations affect the parent site. herd workspaces agrees and lists feature-a with a database of shared. So I put one user in the parent and ran the most destructive command an agent is likely to run.
$ php artisan tinker --execute="echo App\\Models\\User::count();" # parent
1
$ cd <workspace> && php artisan migrate:fresh --force
Dropping all tables .. 4.81ms DONE
INFO Preparing database.
Creating migration table .. 2.00ms DONE
INFO Running migrations.
0001_01_01_000000_create_users_table .. 3.35ms DONE
0001_01_01_000001_create_cache_table .. 1.65ms DONE
$ php artisan tinker --execute="echo App\\Models\\User::count();" # parent again
1
The parent still has its user. Laravel's default SQLite connection doesn't set DB_DATABASE, so each app reads database/database.sqlite relative to its own folder, and the clone took its own copy of that file (different inode, same 98,304 bytes). Herd's warning describes the database Herd thinks it's sharing. The app was never sharing it.
With an absolute database path
That changes the moment DB_DATABASE holds a value that means the same thing in both folders. An absolute SQLite path does. So does the database name of every MySQL or PostgreSQL app, and Herd copies the .env as-is in shared mode. I added an absolute path to the parent's .env and created a second workspace:
DB_CONNECTION=sqlite
DB_DATABASE=/Users/hafizzeeshanriaz/Herd/orders/database/database.sqlite
$ php artisan tinker --execute="echo App\\Models\\User::count();" # parent
1
$ cd <workspace feature-b> && php artisan migrate:fresh --force
Dropping all tables .. 3.29ms DONE
INFO Preparing database.
Creating migration table .. 2.10ms DONE
$ php artisan tinker --execute="echo App\\Models\\User::count();" # parent again
0
Gone. The workspace dropped every table in the parent's database. I only tested SQLite, because Herd's MySQL and PostgreSQL services need Herd Pro, but the mechanism is the same .env line. A DB_DATABASE=orders on MySQL points both sites at the same orders database.
--database=clone fixes it. Herd copies the database and points the workspace at the copy:
$ herd workspace feature-c --database=clone
✓ Creating Workspace
✓ Cloning Directory (this can take a moment)
✓ Linking Site
✓ Preparing Database
✓ Updating .env
✓ Finishing Up
Workspace [feature-c] is ready at [http://feature-c.orders.test].
The workspace uses its own SQLite database — a copy of the parent site's.
cd "/Users/hafizzeeshanriaz/Library/Application Support/Herd/devtools/clones/8543c00e/feature-c-fe2986fe"
$ cd <workspace feature-c> && php artisan migrate:fresh --force
Dropping all tables .. 3.26ms DONE
$ php artisan tinker --execute="echo App\\Models\\User::count();" # parent
1
The workspace's .env now has DB_DATABASE set to the copy inside the workspace folder, so migrate:fresh wiped the copy and left the parent alone. There's also --database=empty, which creates a fresh database and runs your migrations unless you pass --no-migrate.
Herd Studio, the browser panel Herd 2 adds for running agents next to a site, shows the same list. The cloned workspace gets a DB copy badge. The others get nothing, feature-a included, even though it was the one with its own database all along.
The queue goes wherever the database goes
This is the one I didn't expect. A fresh Laravel app uses QUEUE_CONNECTION=database, so a shared database also means a shared jobs table. In a shared workspace, I did what an agent building a feature would do. I created a job class that only exists in the workspace and dispatched it. Then I ran the parent site's worker:
$ cd <workspace feature-b> && php artisan tinker --execute="App\\Jobs\\SendWeeklyReport::dispatch();"
$ cd ~/Herd/orders && php artisan queue:work --once # the parent site's worker
2026-11-06 12:17:24 App\Jobs\SendWeeklyReport .. RUNNING
2026-11-06 12:17:24 App\Jobs\SendWeeklyReport .. 2.44ms FAIL
$ php artisan queue:failed # parent
2026-11-06 12:17:24 01e3dde9-e667-4bd1-8971-f810f14bc45d database@default App\Jobs\SendWeeklyReport
The parent's worker picked up the workspace's job and failed on it, because the class only exists in the workspace. The exception in the parent's failed_jobs reads:
ErrorException: Illuminate\Queue\CallQueuedHandler::commandShouldBeDebounced(): The script tried to access a property on an incomplete object. Please ensure tha
It's the same class of bug as a tenant's job running in the wrong tenant. The job is fine. It just ran in the wrong app. With two agents changing job payloads in parallel, the failures land in the parent's failed_jobs, and the parent never dispatched any of them.
Unpushed commits are left out
The last surprise isn't about the database. Herd's settings have workspaceBaseFromOrigin turned on (Settings → Advanced in the app), so a workspace starts from the last pushed state of your branch. I pushed main, committed an Invoice model without pushing, then created two workspaces:
$ git log --oneline -2
97a52bc Add Invoice model (not pushed)
c8084d6 Fresh Laravel app
$ git status -sb | head -1
## main...origin/main [ahead 1]
$ herd workspace feature-d
Workspace [feature-d] is ready at [http://feature-d.orders.test].
WARNING The workspace shares the database of [orders] — migrations and destructive operations affect the parent site.
WARNING The workspace starts from the last pushed state of [main] — your 1 unpushed commit(s) are not included. Push first or re-create it with `herd workspace --local` if you need them.
$ cd <workspace feature-d> && git log --oneline -2 && ls app/Models
c8084d6 Fresh Laravel app
User.php
$ herd workspace feature-e --local
Workspace [feature-e] is ready at [http://feature-e.orders.test].
$ cd <workspace feature-e> && ls app/Models
Invoice.php
User.php
Herd does warn about it, and --local includes the unpushed commit. But an agent handed a workspace without that flag starts from code that's missing your latest commit, and nothing in the workspace itself tells it so.
How I'd set it up for agents
Four of the six results come down to one line in .env.
-
Create every agent workspace with
--database=clone. It isolated the database in the setup where shared mode wiped the parent, and the queue follows, since thejobstable lives in that database.--database=emptygives a fresh one instead. -
Don't read the warning as the answer. It said "shared" for a workspace that wasn't, and it would say the same for one that was. Check
DB_DATABASEin the workspace's.envinstead. -
Push first, or pass
--local. Otherwise the agent works on your last pushed commit. -
Delete workspaces when the agent is done.
herd workspace:deletedrops a workspace's own database unless you pass--keep-database. Deleting a shared workspace left the parent's database alone in my test, and Herd kept the branch in the parent repository.
If you start agents from Herd Studio instead of the CLI, the choice moves into a menu under the prompt box. A new workspace there takes its database mode from Herd's settings, under Workspaces, so check that setting before you start an agent there.
One caveat for my own setup. Herd only runs on macOS and Windows, and its FAQ says Linux isn't ready yet. I moved my day-to-day development to a remote dev box at the end of August, so workspaces only help me when I'm working on the Mac.
FAQ
Are Herd 2 workspaces git worktrees?
Not by default. herd workspace makes a copy-on-write clone of the folder with a full .git directory on a new branch. Herd can also serve a worktree you created yourself with git worktree add, and herd workspaces lists those as worktrees that aren't served yet.
Do Herd 2 workspaces need Herd Pro?
Not for what I tested. Workspaces, --database=clone and the delete command all worked on the free version of Herd 2.1.0 with SQLite. The MySQL and PostgreSQL services themselves are part of Herd Pro.
Does each workspace get its own database?
Only with --database=clone or --database=empty. The default is shared, which shares whatever DB_DATABASE points at. For Laravel's default SQLite file inside the project, that happens to be a separate copy anyway.
Does deleting a workspace delete my database?
A workspace with its own database has that database dropped unless you pass --keep-database. Deleting a shared workspace didn't touch the parent's database in my test.
Can two agents run migrations at the same time?
Yes, if each workspace has its own database. In shared mode they both migrate the parent's database, and the second migrate:fresh wipes whatever the first one just built.
Isolated files, shared state
Workspaces isolate the code well. Every workspace got its own files, branch and domain, with no setup. The database and the queue are only as isolated as DB_DATABASE makes them, and Herd's warning can't tell you which case you're in.


Top comments (0)