Laravel just released Vet, a dependency audit for Composer that shows you the code an update is about to write into vendor/ before it lands. Once you've looked at a package, Vet records that you trust it, so the next update only asks about what changed. It's in beta, and if you know cargo vet from Rust, it's the same idea for PHP.

Even though this is created by Laravel, it works with any project that has a composer.json, whether that's Symfony, WordPress, or plain PHP.

Supply chain attacks have already reached PHP. Hijacked accounts have been used to publish malicious tags on Packagist, and Composer's creators are covering what they changed in response at SymfonyCon this year. Vet gives you a chance to catch that kind of update before it runs in your app.

It Runs on Every Composer Update

Vet ships as a Composer plugin, so there's no step to add. It runs after every composer install and before every composer update writes anything. When an update brings in a package you haven't trusted, you get the diff right in your terminal:

❯ composer update

  to review (1)

  carbonphp/carbon-doctrine-types 3.1.0 → 3.2.0 .............. 2 files changed
  │
  │   ~ src/Carbon/Doctrine/DateTimeType.php
  │     -    public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?DateTime
  │     +    public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?Carbon

  Packages: 1 to review, 124 trusted

Vet exits with a non-zero status when any package is untrusted, so an unreviewed update fails your CI build until someone reads it.

Let Your Coding Agent Read the Diff

Reading every change by hand takes time. When you run ./vendor/bin/vet in a terminal, it offers to hand each package's changes to the coding agent already on your machine, such as Claude Code, Codex, Gemini, or opencode. The agent reports back one of four results:

  • PASS means it read every file and found no attack.
  • FAIL names each file and the reason, such as a file that posts your .env to an unknown host.
  • WARN means some of the reading is still yours, for example a file too big for the prompt or a .phar.
  • SKIP means nothing was sent, because no file changed.

You still make the call. Vet pre-selects the PASS rows, you read the FAIL and WARN ones, and nothing is written to the trust file until you answer. It also tells you how many prompts it's about to send, and how big they are, before the first one leaves your machine. The Composer plugin never runs the agent on its own.

The Trust File

Your decisions live in vet.json, next to composer.json, and you commit it. Each entry holds the version you reviewed and a hash of every file in the package:

{
    "schema": 4,
    "require": {
        "carbonphp/carbon-doctrine-types": {
            "version": "3.2.1",
            "hash": "tree-v2:0f158f3b909fc01e691ed5f5121186056232b049031e7d3a914676d49881ece5"
        }
    }
}

Because the hash covers the files and not just the version number, a package that ships the same version with different bytes stops being trusted, and Vet asks you to read the difference. That's the rewritten-tag case. If a tool writes into vendor/ on purpose, like Laravel Vapor rewriting framework config files during a deploy, you can list those paths under an ignore key.

Getting Started

Vet requires PHP 8.4 or newer. Install it as a dev dependency:

composer require laravel/vet --dev

Composer asks you to allow the plugin the first time. Then record a baseline of everything already in vendor/:

./vendor/bin/vet --init

From there, every update is checked against that file.

Check out the GitHub repo for the full docs.