The Psalm team tagged Psalm 7 on November 2, 2026. It runs on 64-bit PHP 8.2 through 8.6. These are the main changes:
- Security analysis runs by default, in the same pass as type checking and dead code detection
- A new purity model based on Hack's capabilities
- Dead code detection that works from a graph of references
- Template defaults
- Typed support for the PHP 8.5 pipe operator and PHP 8.6 partial function application
- Framework plugins bundled with Psalm, plus daily releases
What's New
Security Analysis On by Default
Psalm now stores taints as a bitmask and resolves the taint graph over compact summaries of each function. The maintainers say that makes security analysis up to 10x faster. Combined analysis (#11384) runs type checking, security analysis, and dead code detection in one run, and that change is what turns taint analysis on by default (#11399).
Psalm also tracks taints further than before. New issue types cover MongoDB queries (TaintedNosql), prompt injection (TaintedLlmPrompt), and URL query and path injection. Taint now follows values through closures, generators, traits, and by-reference parameters:
$run = function (string $c): void { shell_exec($c); }; $run((string) $_GET['cmd']); // TaintedShell, through the closure
Psalm also ships taint stubs for Guzzle, PSR-7, Monolog, MongoDB, and other libraries. A stub only loads when your code uses that library.
If your CI only ran type checking before, the first Psalm 7 run may report taint issues you haven't seen before.
Capabilities Replace the Old Purity Annotations
The new purity model is based on Hack's contexts and capabilities. A function declares which side effects it may have, such as io, time, or write-props, with pure and impure at the two ends (#11984):
/** @psalm-capabilities time */ function today(): string { return date('Y-m-d'); // no issue } /** @psalm-pure */ function todayWrong(): string { return date('Y-m-d'); // ImpureFunctionCall: ... requires time }
Calling date() reads the clock, so the function needs the time capability. Purity templates let a function take its purity from what you pass it, so array_map() counts as pure when its callback is. The Purity Model docs explain the whole system.
Graph-Based Dead Code Detection
Psalm now finds unused code on a graph of references (#11939). Code counts as used only when Psalm can reach it from an entry point. That means two classes that only call each other both get reported as UnusedClass.
Template Defaults
A template can now declare a default type, as in @template TError of Throwable = RuntimeException. With that default, Result<int> resolves the error type to RuntimeException, where Psalm 6 raised MissingTemplateParam. Library authors can add a template to a class without breaking the types their users already wrote.
PHP 8.5 and 8.6
Psalm runs on PHP 8.6 and infers the type of every stage in a pipe chain:
function lowercaseWords(string $title): string { return $title |> explode(' ', ?) |> strtolower(...); // InvalidArgument }
explode() returns a list of strings and strtolower() expects a single string, so Psalm reports that stage.
Bundled Plugins and Daily Releases
The PHPUnit, Symfony, Laravel, Mockery, and sabre/event plugins now ship with Psalm and turn on without any setup. Psalm also moves to daily releases, so fixes go out the day they land and you don't have to track dev-master to get them.
Upgrade Notes
UPGRADING.md lists every breaking change, and here are the highlights that might affect typical setups:
- Psalm 7 requires 64-bit PHP 8.2 through 8.6.
@psalm-mutation-freeand@psalm-external-mutation-freestill work, but capabilities replace them in the docs.@psalm-purecode that calls builtins liketime()orrandom_int()now gets reported.- Psalm requires
psalm/plugin-phpunit^0.21. - Psalm's version number no longer tracks the plugin API. Plugin authors should require the psalm/psalm-plugin-api metapackage with a caret range (#11739).
The release notes say upgrading is simple for most projects. To install:
composer require --dev vimeo/psalm:^7