Repository navigation
Composer global-home fallback checks ~/.composer before the XDG home, so scan -g finds no Composer 2 global packages when a stale ~/.composer exists and composer isn't on PATH #586
Description
[agent] Found by the scheduled Composer bug-hunt routine (ledger #321).
Summary
When composer global config home can't be run (composer isn't on PATH, for example when it's installed as composer.phar or behind an alias), get_composer_home falls back to composer_home_candidates, added in #442 for #438. That function tries ~/.composer before $XDG_CONFIG_HOME/composer / ~/.config/composer and returns the first directory that exists. Composer 2 uses the opposite order. On an XDG system (any XDG_* env var set, or /etc/xdg present, i.e. most Linux distros), Factory::getHomeDir tries the XDG dir first and only falls back to ~/.composer. Composer 1 does use ~/.composer first.
So on Linux with Composer 2, any leftover ~/.composer directory (even an empty one, e.g. from the Composer 1 era) hides the real global home. scan -g prints "No global packages found." and exits 0, and apply -g / vex -g silently do nothing to the real global install.
The doc comment at composer_home_candidates ("elsewhere: ~/.composer when it exists, else the XDG location") states Composer's rule backwards for Composer 2.
Impact
Globally installed Composer tools (phpunit, php-cs-fixer, laravel/installer, …) go silently unpatched, with a success exit code, on Linux machines that have both directories and no composer on PATH. Same symptom class as #438.
Repro (Linux, PHP 8.3, Composer 2.10.3 phar, main 203e092)
B=$(mktemp -d); mkdir -p $B/home/.composer $B/home/.config/composer $B/proj $B/nopath
ln -s "$(command -v php)" $B/nopath/php
export HOME=$B/home; unset COMPOSER_HOME XDG_CONFIG_HOME
php composer-2.10.3.phar global config home # -> $B/home/.config/composer
php composer-2.10.3.phar global require psr/log:1.1.4 -n
ls $HOME/.config/composer/vendor/composer/installed.json # the real global install
cd $B/proj
# a local mock answering POST /patch/batch on 127.0.0.1:8765 logs the queried purls
PATH=$B/nopath socket-patch scan -g -e composer --json --proxy-url http://127.0.0.1:8765
# -> "scannedPackages": 0, no /patch/batch request ("No global packages found." in text mode, exit 0)
PATH="$PATH" socket-patch scan -g -e composer --json --proxy-url http://127.0.0.1:8765 # composer on PATH (control)
# -> "scannedPackages": 1, batch queries pkg:composer/psr/log@1.1.4
rmdir $HOME/.composer; PATH=$B/nopath socket-patch scan -g -e composer --json ... # control
# -> "scannedPackages": 1The same happens with XDG_CONFIG_HOME=$B/xdg and the global install in $B/xdg/composer.
Expected vs actual
- Expected:
-goperates on the global install Composer itself uses (get_composer_homemirrorsFactory::getHomeDir, per its doc comment and the On Windows, scan -g finds no Composer global packages in the default %APPDATA%\Composer home, so apply -g and vex -g silently do nothing #438 fix). Composer 2:$XDG_CONFIG_HOME/composeror~/.config/composerfirst when XDG is in use, then~/.composer. - Actual:
~/.composerwins whenever it exists, and 0 packages are scanned with exit 0.
Matrix (Linux, PHP 8.3; each scan run twice, same result)
| Composer | Composer's own home (both dirs exist) | socket-patch, no composer on PATH |
control: on PATH | control: no ~/.composer |
|---|---|---|---|---|
| 1.10.28 | ~/.composer (both cases) |
matches Composer (not affected) | n/a | n/a |
| 2.2.30 | ~/.config/composer / $XDG_CONFIG_HOME/composer |
0 packages ×2 layouts | n/a | n/a |
| 2.10.3 | ~/.config/composer / $XDG_CONFIG_HOME/composer |
0 packages ×2 layouts | 1 package | 1 package |
macOS: untested. Composer's useXdg() is normally false there (no XDG_* vars, no /etc/xdg), so ~/.composer first is correct, but a macOS user with an XDG_* variable set would hit this. Windows isn't affected (%APPDATA%\Composer comes first).
First bad version
This isn't a regression in #442. The pre-#442 fallback (v4.0.0) already probed ~/.composer before ~/.config/composer. #442 added $XDG_CONFIG_HOME but kept the order.
Suspect code
crates/socket-patch-core/src/crawlers/composer_crawler.rs:407-434 (composer_home_candidates: home.join(".composer") at :424 is pushed before the XDG candidates at :428/:432), consumed at :387-390. A fix needs to mirror useXdg() (any XDG_* env var, or /etc/xdg exists) and, when it's true, put the XDG dir first. Composer 1's order differs, but without a composer binary the version can't be detected cheaply, so preferring the Composer 2 rule (or crawling every existing candidate) seems the safer choice.
Activity
composer: run 9 ledger (#586)
mikolalysenko commentedon Oct 2, 2026
[agent] Triaged:
priority:p2(Composer). This isn't a duplicate. #438 / #442 added the fallback but kept the Composer 1 order, and I found no open PR. The cause is confirmed incomposer_home_candidates(crawlers/composer_crawler.rs), which pushes~/.composerbefore the XDG candidates.Generated by Claude Code
mikolalysenko commentedon Oct 12, 2026
[agent] Claiming this issue (shared root cause: the Composer home fallback probes
~/.composerbefore the XDG home, which is Composer 1's order; Composer 2 checks the XDG home first on XDG systems). Branch: agent/fix-composer-xdg-home-order. Claim-ID: 2026-10-12T07:21:23Z-9d3c73Generated by Claude Code
mikolalysenko commentedon Oct 12, 2026
[agent] Draft PR: #1421
Generated by Claude Code
Find Composer 2 global home before ~/.composer
Tighten Composer XDG home detection