[agent] Found by the scheduled Composer bug-hunt routine (ledger #321).
Summary
When a Composer package was installed from source, vendor/<vendor>/<name>/ is a git clone with its own .git/. That happens with --prefer-source, with config.preferred-install: "source", and on a dist download failure where Composer falls back to source. The composer vendor backend copies the installed tree into .socket/vendor/composer/<uuid>/<vendor>/<name>@<version>/ with no exclusion, so the .git/ directory is copied too.
The vendored copy is then a nested git repository. git add records it as a gitlink (mode 160000) that points at the pristine upstream commit, and warns "adding embedded git repository". None of the patched files are committed. In a fresh clone the copy directory is empty. composer install then exits 0 while "mirroring" the empty path dist, so vendor/psr/log has no files and nothing reports the problem.
vendor itself reports success (applied: 1, exit 0), and the local tree looks fine. The breakage only appears on CI or on a teammate's machine.
Impact
- The patched package never reaches anyone else: the commit contains a gitlink, not the files.
- The fresh install is silently broken (an empty package,
composer install exit 0). Autoloading the package then fails at runtime.
- The routine's own run confirms the mechanism: two repo capstones fail locally whenever the fixture's psr/log gets installed from source.
composer_vendor_keeps_files_mirror_filters_would_drop and composer_vendor_fast_path_heals_legacy_copy both fail with "composer's path mirror dropped or changed .git/HEAD", because the vendored copy contains .git/HEAD. The capstones don't catch the git half: composer_e2e_common::fresh_checkout copies .socket/ with the filesystem, not through git.
vex on the fresh clone correctly refuses (omitting pkg:composer/psr/[email protected] from VEX: a patched file is missing), so there's no false attestation.
Repro
Offline vendor with a staged manifest, as in e2e_vendor_composer_build.rs:
export COMPOSER_ALLOW_SUPERUSER=1 # sandbox runs as root
mkdir -p src/proj && cd src/proj && git init -q
cat > composer.json <<'J'
{ "name": "acme/app", "require": {"psr/log": "3.0.2"} }
J
composer update -n -q --prefer-source # or "config": {"preferred-install": "source"}
ls -d vendor/psr/log/.git # present
# stage .socket/manifest.json + blob for pkg:composer/psr/[email protected] patching src/LoggerInterface.php
socket-patch vendor --json --offline --cwd . # exit 0, summary.applied = 1
ls -d .socket/vendor/composer/<uuid>/psr/[email protected]/.git # present (276K of upstream history)
printf '/vendor/\n' > .gitignore
git add -A # warning: adding embedded git repository: .socket/vendor/composer/<uuid>/psr/[email protected]
git commit -qm vendored
git ls-files -s .socket/vendor/composer/
# 160000 f16e1d5863e37f8d8c2a01719f5b34baa2b714d3 0 .socket/vendor/composer/<uuid>/psr/[email protected]
cd .. && git clone -q proj fresh && cd fresh
ls -A .socket/vendor/composer/<uuid>/psr/[email protected] # empty
composer install -n # "Installing psr/log (3.0.2): Mirroring from .socket/…" exit 0
ls vendor/psr/log # No such file or directory: nothing installed, patched file absent
This reproduced 3 out of 3 times: --prefer-source, config.preferred-install: "source", and a --prefer-dist install that fell back to source in the sandbox.
Expected vs actual
- Expected: CLI_CONTRACT.md's composer vendor row says
composer install works "from the lock alone, real copy not symlink", and docs/testing/composer-compatibility.md says "The patched copy is committed under .socket/vendor/composer/" and "A fresh checkout installs the patched bytes on every version". The copy should hold the package files only. A VCS metadata dir (.git, and by the same logic .svn / .hg) is not part of the package. Composer's own path mirror and composer archive skip it, which is exactly why the capstone sees .git/HEAD as "dropped".
- Actual:
.git/ is copied verbatim, the vendored copy becomes an embedded repo, and a fresh clone installs an empty package with exit 0.
Matrix
| OS |
Composer |
PHP |
Install from source → vendor → git clone → install |
| Linux |
2.8.12 |
8.4.19 |
fail (3/3) |
| Linux |
2.8.12 |
8.4.19, dist install |
pass in upstream CI. No .git in the installed tree, so not affected. |
| macOS / Windows |
— |
— |
not run. The copy is OS-independent; git's embedded-repo handling is the same everywhere. |
| Composer 1.x |
— |
— |
not run. --prefer-source clones with .git there too, so it's expected to behave the same. |
Tested on main f6b7fb9 (CLI 4.0.0, the latest release).
Suspect code
crates/socket-patch-core/src/vendor/composer_lock.rs:750: installed_dir.stage_into(&stage, None) copies the whole installed tree. The skip parameter only supports one file name (cargo uses it for .cargo-checksum.json, vendor/cargo.rs:507), so there's no VCS-dir exclusion.
- The service path extracts a dist zip, so it's unaffected. Only the local build from an installed source checkout is.
- Also worth checking: any heal/verify that inventories the copy (a
.git/ would change the copy's file set when git gc repacks objects).
[agent] Found by the scheduled Composer bug-hunt routine (ledger #321).
Summary
When a Composer package was installed from
source,vendor/<vendor>/<name>/is a git clone with its own.git/. That happens with--prefer-source, withconfig.preferred-install: "source", and on a dist download failure where Composer falls back to source. The composer vendor backend copies the installed tree into.socket/vendor/composer/<uuid>/<vendor>/<name>@<version>/with no exclusion, so the.git/directory is copied too.The vendored copy is then a nested git repository.
git addrecords it as a gitlink (mode 160000) that points at the pristine upstream commit, and warns "adding embedded git repository". None of the patched files are committed. In a fresh clone the copy directory is empty.composer installthen exits 0 while "mirroring" the empty path dist, sovendor/psr/loghas no files and nothing reports the problem.vendoritself reports success (applied: 1, exit 0), and the local tree looks fine. The breakage only appears on CI or on a teammate's machine.Impact
composer installexit 0). Autoloading the package then fails at runtime.composer_vendor_keeps_files_mirror_filters_would_dropandcomposer_vendor_fast_path_heals_legacy_copyboth fail with "composer's path mirror dropped or changed .git/HEAD", because the vendored copy contains.git/HEAD. The capstones don't catch the git half:composer_e2e_common::fresh_checkoutcopies.socket/with the filesystem, not through git.vexon the fresh clone correctly refuses (omitting pkg:composer/psr/[email protected] from VEX: a patched file is missing), so there's no false attestation.Repro
Offline vendor with a staged manifest, as in
e2e_vendor_composer_build.rs:This reproduced 3 out of 3 times:
--prefer-source,config.preferred-install: "source", and a--prefer-distinstall that fell back to source in the sandbox.Expected vs actual
composer installworks "from the lock alone, real copy not symlink", and docs/testing/composer-compatibility.md says "The patched copy is committed under.socket/vendor/composer/" and "A fresh checkout installs the patched bytes on every version". The copy should hold the package files only. A VCS metadata dir (.git, and by the same logic.svn/.hg) is not part of the package. Composer's own path mirror andcomposer archiveskip it, which is exactly why the capstone sees.git/HEADas "dropped"..git/is copied verbatim, the vendored copy becomes an embedded repo, and a fresh clone installs an empty package with exit 0.Matrix
.gitin the installed tree, so not affected.--prefer-sourceclones with.gitthere too, so it's expected to behave the same.Tested on main
f6b7fb9(CLI 4.0.0, the latest release).Suspect code
crates/socket-patch-core/src/vendor/composer_lock.rs:750:installed_dir.stage_into(&stage, None)copies the whole installed tree. The skip parameter only supports one file name (cargo uses it for.cargo-checksum.json,vendor/cargo.rs:507), so there's no VCS-dir exclusion..git/would change the copy's file set when git gc repacks objects).