The overlooked Git feature every Laravel developer should be using
Stop stashing, reinstalling dependencies, and rebuilding assets every time you switch branches. Git worktrees let you maintain multiple isolated Laravel environments simultaneously, eliminating context-switching friction.
Laravel developers run into the same friction point again and again: constant context switching between branches. You're deep into a feature, migrations are half-run, dependencies are installed, and the server is running smoothly. Then Slack lights up. Production is down. It needs attention immediately.
The usual ritual begins. Stash your changes, check out main, pull the latest updates, create a hotfix branch. Composer reinstalls dependencies. NPM rebuilds assets. You fix the bug, push it, then switch back to your feature branch. Pop the stash. Hope nothing conflicts. Reinstall dependencies again because they changed. Restart the development server. Wait for Vite to rebuild.
Ten minutes of context switching for a two-minute fix.
There is a better way, and it has been built into Git for years: worktrees.
What Git worktrees actually are
Here's the simple version: worktrees let you check out multiple branches of the same repository into separate directories simultaneously. Not multiple clones. Not some fancy tool you need to install. Just Git, doing something most developers don't know it can do.
Instead of one working directory tied to one branch, you get multiple working directories, each on different branches, all sharing the same Git history and object database.
Think of it like this: your Git repository has two parts. The object database (all your commits, trees, blobs, the actual history) and the working directory (your checked-out files, your Laravel app, vendor, node_modules, all of it). Normally, these are bundled together. Worktrees split them apart. Multiple working directories, one shared history.
That's why they're fast and use minimal additional disk space compared to cloning the repo multiple times.
The real problem this solves
Let me paint you a picture from last month. I was refactoring a payment processing system to Consider a common scenario. You are refactoring a payment processing system, splitting it into a more modular structure. It is focused work. Multiple files are open and the mental model of the system is fully loaded.
Then a production alert comes in. Authentication tokens are expiring early for mobile users. It is a critical bug and it needs to be fixed immediately.
Without worktrees, the usual stash-and-switch routine begins. With worktrees, you simply change directories.
cd ../my-app-hotfixThat directory is already on main. Dependencies are installed. Its .env file points to a separate database. You fix the token expiration issue, commit, push, and return to the refactoring branch without touching a single file in your feature work.
No stashing. No reinstalling. No resetting your development environment.
That's the power of worktrees.
The commands you actually need
The entire worktree feature comes down to four commands. Create a worktree from an existing branch:
git worktree add ../my-app-feature feature-xCreate a new branch and its worktree at the same time:
git worktree add -b feature-y ../my-app-feature-yList your worktrees:
git worktree listRemove a worktree when it is no longer needed:
git worktree remove ../my-app-featureA practical setup keeps the main working directory as the base and creates additional worktrees for everything else. Hotfixes, feature branches, upgrades, and experiments each live in their own directory, typically named after the project and the branch purpose.
Why Laravel makes this even more valuable
Laravel apps are heavy. Not in a bad way, but in a realistic way. Switching branches isn't just changing some text files. It's:
- Composer dependency changes that trigger reinstalls
- NPM dependency changes and asset rebuilds
- Database migrations that might differ between branches
- Config files that affect how your app behaves
- Environment settings for different features
- Queue workers that need restarting
- Cache that needs clearing
Branch switching in a Laravel app feels like rebooting your entire development environment.
Worktrees let you keep each environment stable. One worktree per branch means one stable environment per feature. No rebuilds. No reinstalls. No surprises.
How to actually use this
Let me show you an example current project structure:
laravel-saas/ # main branch, stable
laravel-saas-billing/ # feature branch for subscription overhaul
laravel-saas-api/ # API redesign spike
laravel-saas-upgrade/ # Laravel 12 upgrade sandboxEach directory is a worktree. Each runs independently. I have four terminal sessions open, each cd'd into a different directory. The billing feature runs on port 8000. The API redesign on 8001. Main stays on 8002. The upgrade sandbox on 8003.
# In laravel-saas-billing
php artisan serve --port=8000
# In laravel-saas-api
php artisan serve --port=8001This setup allows you to test billing changes, switch tabs, and immediately verify how the API redesign handles the same data. There is no waiting for dependencies to reinstall and no need to rebuild assets every time you move between branches. Context switching becomes immediate and predictable.
Environment isolation is critical
Environment isolation makes or breaks this workflow. If worktrees share the same database, cache, or queue configuration, they create more problems than they solve.
Each worktree should have its own:
- Database name
- Cache prefix
- Queue prefix
- Session configuration
- Port numbers
My .env files look like this:
# laravel-saas-billing/.env
DB_DATABASE=saas_billing_feature
CACHE_PREFIX=billing_
QUEUE_CONNECTION=redis
REDIS_QUEUE=billing_queue
SESSION_DOMAIN=billing.saas.test
APP_URL=http://localhost:8000# laravel-saas-api/.env
DB_DATABASE=saas_api_feature
CACHE_PREFIX=api_
QUEUE_CONNECTION=redis
REDIS_QUEUE=api_queue
SESSION_DOMAIN=api.saas.test
APP_URL=http://localhost:8001This configuration prevents one worktree from interfering with another's cache, queue jobs, or sessions. Without proper isolation, it becomes difficult to understand why one feature branch is processing jobs or data that belong to another.
The Laravel upgrade use case
This is where worktrees really shine. Framework upgrades are messy. Breaking changes, deprecation warnings, dependency conflicts. You need to experiment, but you can't break your main development environment.
Create a dedicated upgrade worktree:
git worktree add ../my-app-upgrade upgrade/laravel-12
cd ../my-app-upgradeThis gives you a complete Laravel installation that you can modify freely. Update composer.json, run the upgrade process, resolve breaking changes, adjust configuration files, and fix tests as needed. Meanwhile, the main worktree continues running the current version without interruption.
You can move between the two directories to compare behavior and determine whether an issue exists in both versions or only in the upgraded branch. Once the upgrade is stable, merge it. If it becomes too complex or unstable, remove the worktree and start again.
There is no need for complex branch switching or stashing half-finished upgrade attempts. It is simply a clean, disposable sandbox dedicated to the upgrade effort.
Worktrees and Docker
If you're running Laravel Sail or any Docker-based setup, worktrees become even more powerful. Each worktree can have its own docker-compose.yml with different ports, different database containers, different Redis instances.
# laravel-saas-billing/docker-compose.yml
services:
mysql:
ports:
- "3307:3306"
redis:
ports:
- "6380:6379"# laravel-saas-api/docker-compose.yml
services:
mysql:
ports:
- "3308:3306"
redis:
ports:
- "6381:6379"Completely isolated stacks. No port conflicts. No shared state. Each feature gets its own containerized universe.
The dependency question
Yes, you'll run composer install and npm install in each worktree. Yes, that means duplicate vendor and node_modules directories. That's the tradeoff.
But here's the thing: branches can depend on different package versions. Your feature branch might need a beta version of a package. Your hotfix branch needs the stable version from main. Shared dependencies would break that.
Speed it up by using Composer's caching:
composer install --prefer-dist --no-dev --optimize-autoloaderIn practice, the disk space cost is negligible on modern machines, and the time cost is worth it for the stability you gain.
What about that safety rule?
Git won't let you check out the same branch in multiple worktrees. If you try, it blocks you.
git worktree add ../test-duplicate feature-x
# fatal: 'feature-x' is already checked out at '../my-app-feature'This is a feature, not a limitation. It prevents repository corruption. Two working directories trying to modify the same branch simultaneously would be chaos. Git protects you from yourself.
Structure your workflow around distinct branches per worktree, and you'll never hit this.
When worktrees are not necessary
Worktrees are not required for every workflow. In some cases, a simple branch checkout is enough.
For very small changes, worktrees may add unnecessary overhead. When reviewing a pull request without running the code locally, a separate working directory is not essential. On smaller projects where branch switching is fast and does not trigger dependency or environment changes, the benefits are limited.
However, on larger Laravel projects where multiple features, hotfixes, and experiments run in parallel, worktrees quickly become part of the default workflow.
A standard setup
A common approach is to configure worktrees from the start of a new Laravel project:
# Main stable branch
cd laravel-app
# Hotfix worktree always ready
git worktree add ../laravel-app-hotfix main
# Feature work
git worktree add -b feature/user-roles ../laravel-app-rolesThe hotfix worktree remains available at all times, always tracking main. When a production issue arises, there is no need to create a new environment. You switch to the existing hotfix directory and begin working immediately.
Feature worktrees are temporary. Once a feature is complete and merged, remove the worktree and delete the branch. The structure stays organized and predictable.
The mental shift
The hardest part of adopting worktrees isn't the commands or the setup. It's changing how you think about Git branches.
Most developers think: one repo, one directory, branches are checkouts I switch between.
With worktrees you think: one repo, multiple directories, branches are environments I switch between.
That mental model shift is subtle but significant. You stop thinking about stashing and branch hopping. You start thinking about parallel environments and focused workspaces.
Why almost nobody uses this
Git worktrees have been around since 2015. Almost a decade. Yet I'd bet less than 5% of Laravel developers use them regularly.
I think it's because Git doesn't advertise them. They're not part of the normal Git workflow tutorials. They're not in the GitHub UI. Most developers learn Git, get comfortable with branches and checkouts, and never realize there's another way.
But once you try worktrees, especially on a real Laravel project with all its weight and complexity, going back to single-worktree development feels unnecessarily limiting.
Wrapping up
Git worktrees aren't magic. They're just a different way of organizing your working directories. But for Laravel developers who regularly context switch between features, hotfixes, upgrades, and experiments, they remove a massive amount of friction.
No more stash juggling. No more dependency reinstalls every time you switch branches. No more waiting for assets to rebuild. Just stable, isolated environments that let you focus on the work instead of fighting your tools.
Try it on your next feature branch. Create a worktree, set up the environment, and see how it feels to context switch without the overhead. You'll find it hard to go back.