18 min read

Laravel 13 Upgrade With Claude Code: A Safe Workflow

A Laravel 13 upgrade with Claude Code, step by step: CLAUDE.md notes, plan mode scoping, guarded Composer changes, test gates, and what a human must review.

By Richard Joseph Porter

  • laravel
  • php
  • claude-code
  • upgrade
  • ai-development

A Laravel 13 upgrade with Claude Code can be fast and boring, which is exactly what you want from a framework upgrade. It can also be fast and wrong: an agent that "fixes" a red test by editing the test, runs composer update with no constraints, or flips your session serialization and logs out every user on deploy day. The difference is not the model. It is the workflow you wrap around it.

This post is that workflow, for a developer or tech lead who has already decided to move a Laravel 12 app to 13 and wants Claude Code to do the mechanical work. It does not repeat the list of breaking changes. That lives in Upgrading to Laravel 13: what changed and what breaks, and you should have it open in another tab. If you are still deciding when to upgrade, start with the Laravel 12 end-of-life timeline instead.

Every Laravel fact below comes from the official Laravel 13 upgrade guide and release notes. Every Claude Code feature comes from the Claude Code docs. Where your codebase decides the answer, I say so.

The Split: What Claude Code Does and What You Do

The official guide estimates the upgrade at ten minutes, and Laravel's release notes say this cycle focused on minimizing breaking changes. So the risk in a Laravel 12 to 13 upgrade is rarely the framework code itself. It sits in three places: third-party packages that block the new constraint, configuration defaults that change behavior quietly, and code paths your tests do not cover.

Claude Code is fast at finding all three and at the mechanical edits that follow. It is risky when it is left to decide what those findings mean for your users. Here is the split I'd use:

Claude Code does You do
Read the upgrade guide and map each change to files in the repo Decide which config defaults to adopt
Run composer why-not, composer outdated, and grep sweeps Approve every composer update
Rename classes, update listeners, fix deprecations Review auth, sessions, queues, money, and migrations
Run the test suite after every step and report failures Decide whether a failing test is a real regression
Write commit messages for each small step Merge, deploy, and own the rollback

The rest of this post is how to enforce that split with Claude Code's own features instead of good intentions.

Step 1: Prepare the Repo Before the Agent Touches It

Get a green baseline

An upgrade without a passing suite on the current version gives you no signal. Every failure after the bump becomes ambiguous: did Laravel 13 break it, or was it already broken? Run the suite and save the result before anything else:

php -v                      # Laravel 13 requires PHP 8.3 or newer
php artisan about           # framework version, drivers, cache state
php artisan test            # must be green before you start

If you are on PHP 8.2, stop here. Laravel 12 supports PHP 8.2 through 8.5, but Laravel 13 needs 8.3 at minimum, so the runtime move is its own release. Composer will refuse to resolve Laravel 13 on 8.2 anyway.

If the suite is not green, fix that first, or quarantine the known failures in a list you hand to Claude so it does not "fix" them as part of the upgrade.

Branch and tag

git switch -c upgrade/laravel-13
git tag pre-laravel-13

The tag is your rollback point for both code and deploys. You will lean on it more than you expect, because Claude Code's checkpoints do not cover everything an upgrade changes (more on that under rollback).

Write upgrade notes into CLAUDE.md

Claude Code loads CLAUDE.md from the project root at the start of every session. For the duration of the upgrade, give it a short section that states the rules of this job. Keep it tight: the memory docs recommend staying under 200 lines per file, and an upgrade section only needs a dozen.

## Laravel 13 upgrade (temporary, remove after merge)

- Branch: upgrade/laravel-13. Rollback tag: pre-laravel-13.
- Source of truth: https://laravel.com/docs/13.x/upgrade. Do not rely on memory
  for Laravel 13 behavior; read the guide section before changing code.
- One change per commit. Run `php artisan test` after every change.
- If a test fails, stop and report it. Never edit or delete a test to make it pass.
- Do not change config/session.php `serialization` or config/cache.php
  `serializable_classes`. Report what you find; a human decides.
- Do not touch: app/Billing/**, database/migrations/**, routes/webhooks.php.
- Never run migrations or anything that writes to a database outside the test suite.

If you use Laravel Boost, php artisan boost:install manages a <laravel-boost-guidelines> block inside CLAUDE.md and preserves anything written outside it, so put your upgrade section outside that block. For more on structuring the file for Laravel work in general, see my Claude Code for Laravel workflow.

Turn the rules into permissions

Instructions in CLAUDE.md are guidance. Permission rules are enforced. Add a project .claude/settings.json for the upgrade branch:

{
  "permissions": {
    "allow": [
      "Bash(php artisan test *)",
      "Bash(composer why-not *)",
      "Bash(composer outdated *)",
      "Bash(rg *)"
    ],
    "ask": [
      "Bash(composer update *)",
      "Bash(composer require *)"
    ],
    "deny": [
      "Bash(php artisan migrate*)",
      "Bash(php artisan db:wipe*)",
      "Bash(git push *)",
      "Read(./.env)"
    ]
  }
}

A few details that matter here, all from the permissions docs:

  • A trailing * after a space also matches the bare command, so Bash(php artisan test *) covers php artisan test with or without arguments.
  • Bash(php artisan migrate*) has no space before the *, so it also catches migrate:fresh, migrate:rollback, and the rest of the family.
  • Deny wins over allow, and an ask rule prompts even when a narrower allow rule matches.
  • Rules match the command as written. A command phrased differently can slip past a pattern, so treat these as guardrails, not a sandbox. The real protection for your data is that the environment Claude runs in points at a throwaway database.

The ask rules on Composer are the most important lines. Every dependency change should pass in front of a human.

Add a lint hook

Hooks run your own scripts at fixed points in Claude's loop. A PostToolUse hook on Edit|Write can syntax-check every PHP file Claude touches. Add it to the same settings file:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/php-lint.sh",
            "args": []
          }
        ]
      }
    ]
  }
}
#!/bin/bash
# .claude/hooks/php-lint.sh  (requires jq)
FILE=$(jq -r '.tool_input.file_path')
[[ "$FILE" == *.php ]] || exit 0

if ! OUTPUT=$(php -l "$FILE" 2>&1); then
  echo "$OUTPUT" >&2
  exit 2
fi

Make it executable with chmod +x .claude/hooks/php-lint.sh. On PostToolUse, exit code 2 cannot undo the edit, because the tool already ran, but it shows stderr to Claude so it fixes the syntax error right away instead of discovering it three steps later in a test run.

Step 2: Scope the Upgrade in Plan Mode

Do not let the first session write code. Start Claude Code in plan mode, where it reads files and proposes a plan but makes no edits until you approve:

claude --permission-mode plan

You can also press Shift+Tab mid-session until the status bar shows plan mode. Then ask for a scoping report, not a fix:

We are upgrading this app from Laravel 12 to Laravel 13. Scope it; do not fix anything.

1. Fetch https://laravel.com/docs/13.x/upgrade and list every section in it.
2. Run `composer outdated --direct` and `composer why-not laravel/framework 13.0`.
   For each blocking package, say whether a Laravel 13 compatible release exists.
3. For each upgrade guide section, search this codebase for code it affects
   (class names, event properties, method calls, config keys). Cite file:line.
4. Propose a plan as a table with one row per guide section:
   section | impact level from the guide | affected files | proposed change |
   owner (agent or human) | how we verify it.
Mark anything touching auth, sessions, queues, payments, or migrations as human.
Once I approve the plan, your first action is to save the table as UPGRADE_PLAN.md.

The output you want is a map, not a summary. The guide's request forgery row should name the actual files, something like "VerifyCsrfToken referenced in tests/Feature/WebhookTest.php:14", rather than restating that the CSRF middleware was renamed. Searches like these are what Claude should be running behind each row:

rg -n "VerifyCsrfToken|ValidateCsrfToken" app/ bootstrap/ config/ routes/ tests/
rg -n "exceptionOccurred|QueueBusy" app/
rg -n "upsert\(" app/ database/
rg -n "function (array_first|array_last)" app/ bootstrap/

That is a sample, not the full audit. The what changed post covers each change and the full pre-upgrade checklist; the point here is that Claude runs the sweep and cites every hit, and you read the plan.

Use a read-only subagent for the sweep

On a large codebase, the grep sweep floods the main conversation with output you do not need to keep. A project subagent runs the search in its own context and hands back only the findings. Save this as .claude/agents/upgrade-auditor.md:

---
name: upgrade-auditor
description: Read-only auditor that maps Laravel upgrade guide sections to files in this repo. Use when scoping a framework upgrade.
tools: Read, Grep, Glob
---

You map framework upgrade guide sections to affected code. For each section
you are given, search the repository and report every match as file:line with
one sentence on why it is affected. Never propose edits. If a section has no
matches, say so explicitly.

Restricting tools to Read, Grep, Glob keeps the auditor read-only. That also matters for rollback, because Claude Code's checkpoints usually do not capture edits a subagent makes.

Laravel Boost's upgrade prompt

The upgrade guide itself points to an AI route: Laravel Boost ^2.0 ships an upgrade prompt that the guide calls /upgrade-laravel-v13. In Claude Code, MCP prompts appear in the / menu as /servername:promptname (MCP), and you can also type /mcp__servername__promptname. With Boost registered under its usual laravel-boost server name, that is /mcp__laravel-boost__upgrade-laravel-v13.

Two practical notes from Boost's source. The prompt only registers while the app is on Laravel 12.x, so run it before you bump the framework constraint. If you are still on Boost 1.x, moving to 2.x is the first Composer step, and it can happen before scoping. And it is a checklist plus the guide's content, injected into the conversation. It does not replace the plan-mode pass, the permissions above, or your review. I'd treat it as a second source to cross-check UPGRADE_PLAN.md against, not as the plan.

Step 3: Execute in Small, Verified Steps

Once you approve the plan, switch out of plan mode and work through it in this order. Each step is one commit, and each commit ends with a test run.

1. Composer constraints

The guide lists the dependencies to update in composer.json:

{
    "require": {
        "laravel/framework": "^13.0",
        "laravel/tinker": "^3.0"
    },
    "require-dev": {
        "laravel/boost": "^2.0",
        "phpunit/phpunit": "^12.0",
        "pestphp/pest": "^4.0"
    }
}

Only bump the ones you actually have, and do not change them all at once. Current Boost 2.x and Tinker 3.x releases both accept recent Laravel 12 versions, so they can move first, each in its own commit, while the suite still runs on the framework you know. Then the framework on its own:

# after Claude changes only the laravel/framework constraint
composer update laravel/framework --with-all-dependencies
php artisan test

Claude edits the constraint; you approve the update when the ask rule prompts. Naming the package keeps unrelated dependencies out of the commit. Change only the constraints for the step you are on: if composer.json asks for a new major of a package you did not name in a partial update, Composer refuses and tells you to list it. The Laravel 12 skeleton ships PHPUnit 11, so the move to PHPUnit 12 (or Pest 4) brings its own changes. Give it a separate commit and test run.

2. Packages that block the update

If Composer refuses to resolve, the composer why-not output from Step 2 already names the culprits. A package with a release that supports both Laravel 12 and 13 can move before the framework, one package per commit. A package whose new major only supports 13 has to move in the same update as the framework, so name it in that composer update command. Anything abandoned gets replaced or removed in its own commit first. Ask Claude to read each package's changelog for its Laravel 13 release and summarize anything that touches your code, then run the suite. Do not let it pin a dev-main branch or fork a package without asking you first; add that to the CLAUDE.md rules if it tries.

3. Code changes, one guide section at a time

This is where Claude Code saves the most time. Drive it from the plan, one row at a time:

Apply the "Request Forgery Protection" row of UPGRADE_PLAN.md only.
Replace direct references to VerifyCsrfToken and ValidateCsrfToken with
PreventRequestForgery. Then run `php artisan test`.
If anything fails, stop and show me the failure. Do not change tests to pass.

If you repeat that prompt for every row, turn it into a skill so you type it once. Save it as .claude/skills/upgrade-step/SKILL.md:

---
name: upgrade-step
description: Apply one row of UPGRADE_PLAN.md and verify it
disable-model-invocation: true
---

Apply only this row of UPGRADE_PLAN.md: $ARGUMENTS

1. Re-read that section of https://laravel.com/docs/13.x/upgrade first.
2. Make the smallest change that satisfies it.
3. Run `php artisan test`. If it fails, stop and report. Do not edit tests.
4. If it passes, show the diff and propose a one-line commit message.

Then run /upgrade-step QueueBusy Event Property Rename. Setting disable-model-invocation: true means the skill runs only when you call it, so Claude cannot decide to apply three rows at once.

4. Deprecations

Laravel 13 keeps VerifyCsrfToken and ValidateCsrfToken as deprecated aliases, so a green suite can still hide upgrade debt. Run a pass that fails on deprecations and let Claude clear what it finds:

vendor/bin/phpunit --fail-on-deprecation
# or, with Pest
vendor/bin/pest --fail-on-deprecation

When a step goes red

The rule in CLAUDE.md says stop and report, and this is the moment it pays off. A failing test after an upgrade step is either a real regression, a test that depended on old behavior, or a change you have not made yet. Only you can tell which. Ask Claude to explain the failure and point to the guide section it relates to, then decide. If a session gets long and noisy, /clear and start the next row fresh; the plan file and the git log carry the state.

What to Keep a Human On

Some rows in the plan should never be marked "agent", no matter how mechanical the diff looks. These are the areas where Laravel 13 changes behavior rather than names, or where a wrong guess costs real money or real users:

Area Laravel 13 change Why a human decides
Auth and CSRF PreventRequestForgery adds origin checks via Sec-Fetch-Site Webhooks, iframes, and cross-origin frontends need manual testing on staging
Sessions Skeleton default serialization is now json Switching from php invalidates every active session
Cache serializable_classes defaults to false Cached PHP objects fail when read back, not at deploy
Queues JobAttempted and QueueBusy payload changes; restored eager loads on serialized collections Listeners and telemetry can break silently; in-flight jobs cross versions
Money and data upsert() requires a non-empty uniqueBy; MySQL joined deletes now honor ORDER BY and LIMIT Batch jobs that wrote or deleted data can now throw, or behave differently
Migrations None required by the upgrade itself Schema changes are one-way unless you planned them; Claude does not run them

For each of these, Claude's job is to find every affected file and explain the change. Your job is to pick the behavior and verify it outside the test suite. The principle is the same one from my practitioner's guide to Claude Code on legacy codebases: treat the output like a capable new hire's pull request, and review the risky parts line by line.

Review and Rollback

Review commit by commit

Because every step is its own commit, the review is a walk through history rather than one huge diff:

git log --oneline pre-laravel-13..HEAD
git diff pre-laravel-13 -- composer.json config/ bootstrap/

Inside Claude Code, /diff shows the working tree changes, /code-review checks the current diff for correctness bugs, and /security-review reviews your branch against origin's default branch for issues such as auth problems. Run the security review after the request forgery changes. Do the review in a fresh session, so the reviewer is not the same context that wrote the code.

Know what /rewind can and cannot undo

Claude Code checkpoints every prompt, and /rewind (or Esc twice) restores Claude's file edits. The checkpointing docs are explicit about the limits, and they matter for an upgrade:

  • Changes made by Bash commands are not tracked. composer update rewrites composer.lock and vendor/ through Bash, so /rewind will not bring them back.
  • Edits made by most subagents are not restored either.
  • Checkpoints are not version control.

So git is the real rollback. To back out a dependency step:

git restore composer.json composer.lock
composer install

To abandon the whole attempt, reset the branch to pre-laravel-13. For deployment, the tag is the rollback: redeploy it rather than writing a revert under pressure. The deploy-side checklist (draining queues, pinning cache prefixes, shipping the session change separately) is in the deploy and rollback section of the what changed post.

Clean up

When the branch merges, delete the temporary upgrade section from CLAUDE.md, remove UPGRADE_PLAN.md, and decide whether the permission rules and lint hook are worth keeping for everyday work. The lint hook costs almost nothing to keep.

Frequently Asked Questions

Can Claude Code upgrade a Laravel 12 app to Laravel 13 on its own?

It can do most of the mechanical work: reading the guide, mapping changes to files, editing constraints, renaming classes, and running tests. It should not decide config defaults, approve dependency changes, or judge whether a failing test is a regression. Use plan mode for scoping, permission rules to gate Composer and migrations, and review every commit yourself.

Should I use Laravel Boost's upgrade prompt or write my own?

Use both. The official upgrade guide points to Boost ^2.0 and its /upgrade-laravel-v13 prompt, which only registers while the app is still on Laravel 12.x. It gives Claude the guide's content as a checklist. Your own plan-mode prompt adds what Boost cannot know: your file paths, your risky areas, and your rules for when to stop.

Does /rewind undo a composer update?

No. Claude Code's checkpoints track edits made with its file editing tools, not changes made by Bash commands. composer update changes composer.lock and vendor/ through Bash, so restore those with git (git restore composer.json composer.lock then composer install) and keep each dependency change in its own commit.

What if I am still on Laravel 11 or older?

Upgrade one major version at a time, because each Laravel upgrade guide only documents changes from the previous release. Run this workflow once per hop with its own branch, plan, and green suite. Laravel 11 is already past its security support date, so treat it as urgent work rather than roadmap work.

Key Takeaways

  • In a Laravel 13 upgrade with Claude Code, the agent handles the mechanical part well. The risk sits in packages, config defaults, and untested paths, so build the workflow around those.
  • Start from a green suite, a branch, a rollback tag, and an upgrade section in CLAUDE.md. Enforce the rules with permission rules, not just instructions.
  • Scope in plan mode first. Ask for a file:line map against the official guide, and use a read-only subagent for big grep sweeps.
  • Apply one guide section per commit, run the tests after each, and never let the agent edit a test to make it pass.
  • Keep auth, sessions, cache serialization, queues, money, and migrations on a human.
  • /rewind does not undo composer update. Git and the pre-laravel-13 tag are your rollback.

Laravel 12 security fixes end on February 24, 2027. With this setup, a Laravel 13 upgrade with Claude Code is a short project you can schedule, not an emergency.

Want a Second Pair of Eyes?

I have spent 14 years in PHP and Laravel, and a good share of it on upgrades. If you want help setting up this workflow on your codebase, a review of your UPGRADE_PLAN.md, or someone to run the upgrade while your team keeps shipping, get in touch through the contact form. Send your php artisan about output and your composer why-not laravel/framework 13.0 list, and I can usually give you a useful first answer.

Richard Joseph Porter - Senior PHP and Laravel Developer, author of technical articles on web development

Written by

Richard Joseph Porter

Senior Laravel Developer with 14+ years of experience building scalable web applications. Specializing in PHP, Laravel, Vue.js, and AWS cloud infrastructure. Based in Cebu, Philippines, I help businesses modernize legacy systems and build high-performance APIs.

Get in touch

Need help upgrading your Laravel app?

I specialize in modernizing legacy Laravel applications with zero downtime. Get a free codebase audit and upgrade roadmap.

Related articles