Repository navigation
uv project under a parent uv.toml / [tool.uv]: hosted and vendored scan add a [tool.uv] table to the project's pyproject.toml, which hides the parent settings from uv, so uv sync --locked fails and a plain uv lock re-resolves without them #1545
Description
[agent] Found by the scheduled uv bug-hunt routine (ledger #310).
Summary
uv reads project settings from the nearest directory at or above the project that holds a uv.toml or a pyproject.toml with a [tool.uv] table. A pyproject.toml without [tool.uv] is skipped, and the lookup goes on to the parent. That's the usual monorepo layout: one root uv.toml (or root [tool.uv]) sets exclude-newer, resolution, indexes, constraints and so on for several independent projects below it.
When such a project has no [tool.uv] of its own, hosted and vendored scan write [tool.uv.sources] six = … into its pyproject.toml. That new table makes the project's own pyproject the governing settings file, so uv stops reading the parent and every parent setting disappears at once. The lock socket-patch wrote still records the parent's options ([options] exclude-newer / resolution-mode), so:
uv lock --locked/uv sync --lockedfail ("The lockfile atuv.lockneeds to be updated"). uv explains why:Ignoring existing lockfile due to change in resolution mode: lowest-direct vs. highest, orResolving despite existing lockfile due to removal of global exclude newer.- A plain
uv lock/uv syncre-resolves the whole project without the parent settings (noexclude-newercutoff, default resolution, and the parent's index or constraints dropped). Other dependencies can silently move. scanexits 0success, and the venv stays unpatched (SOCKET_PATCHEDis 0) because the locked sync fails.
#1504 (#1501) already handles this lookup for the script lane (find_ancestor_uv_config), but treats the project lane as "a project stops that lookup at itself" (docs/testing/uv-compatibility.md, the #564 / #1501 bullet). That's only true when the project's pyproject already has [tool.uv]. Here, socket-patch is the one that adds it.
Repro (Linux, main 306ec81)
mkdir -p mono/app && cd mono
printf 'resolution = "lowest-direct"\n' > uv.toml # or: exclude-newer = "2024-01-01T00:00:00Z"
cd app
printf '[project]\nname = "app"\nversion = "0.1.0"\nrequires-python = ">=3.9"\ndependencies = ["six==1.16.0"]\n' > pyproject.toml
uv lock && uv sync --locked # lock records [options] resolution-mode = "lowest-direct"
socket-patch scan --mode hosted --yes # or --mode vendored: exit 0, success
uv lock --locked # FAILS: lockfile needs to be updated
uv lock --locked -v 2>&1 | grep -i ignoring # "change in resolution mode: `lowest-direct` vs. `highest`"The same happens with the parent setting in ../pyproject.toml under [tool.uv] instead of ../uv.toml.
Real uv alone shows the mechanism, without socket-patch: in the fixture above, append an empty [tool.uv] to app/pyproject.toml, and uv lock --locked fails the same way on 0.5.31 and 0.8.17.
I ran it against a local mock of the patch API / vendor server (the probe-branch mock, with a six 1.16.0 patch). Control: the same fixture without the parent config passes (uv lock --locked 0, uv sync --locked 0, SOCKET_PATCHED 1).
Expected vs actual
- Expected: per CLI_CONTRACT / docs/testing/uv-compatibility.md, a wired uv project keeps
uv sync --lockedgreen, and a setting that would defeat the wiring is refused before any file is written (the Hosted and vendored uv wiring ignoreno-sources = true: scan and vendor report success,uv sync --lockedthen fails, and a plainuv syncreinstalls the unpatched wheel #564 / uv PEP 723 script in a subdirectory: hosted and vendored wiring ignoreno-sources = truefrom a parentuv.toml/pyproject.toml, souv run --lockedfails and a plainuv runimports the unpatched package #1501 model). Either keep the parent's settings in effect, or refuse with a code naming the parent file, as the script lane now does. - Actual: exit 0
success, and the locked install is broken.
Ways to fix it (for maintainers to choose): refuse when the project has no [tool.uv] and an ancestor uv.toml / [tool.uv] exists; or copy the ancestor's settings into the new [tool.uv] (that changes semantics and has merge pitfalls, so a refusal is probably safer).
Matrix (Linux; macOS / Windows not probed: probe branches are blocked this run)
| uv | hosted, parent uv.toml |
hosted, parent [tool.uv] |
vendored, parent uv.toml |
vendored, parent [tool.uv] |
control (no parent) |
|---|---|---|---|---|---|
| 0.5.31 | fail | fail | fail | fail | pass |
| 0.8.17 | fail | fail | fail | fail | pass |
| 0.13.0 | fail | fail | fail | fail | pass |
I checked each cell with exclude-newer, and the hosted and vendored cells on all three versions again with resolution = "lowest-direct". Every run reproduced.
After the damage, vendored rollback restores both files and uv lock --locked is green again. Hosted rollback refuses hosted_restore_refused in this one-package fixture (no sibling registry package / exclude-newer), which is the documented restore limit, not this bug.
First bad release
Reproduces on the v5.0.1 release binary (both modes). v4.0.0 didn't wire this fixture against the mock, so I couldn't compare it. Not bisected further.
Suspect code
crates/socket-patch-core/src/patch/redirect/mod.rs:5948-5973(plan_python_metadata_with): the project lane callsrefuse_uv_no_sources/rewrite_project_metadataon the root files only, and never reads an ancestor config.crates/socket-patch-core/src/utils/python_script.rs:406/:431(root_governs_script,find_ancestor_uv_config): the ancestor lookup exists, but only the script lane uses it (hosted/engine.rsread_candidate_files).crates/socket-patch-core/src/utils/python_script.rs:602(rewrite_project_metadata) and the vendored uv wiring incrates/socket-patch-core/src/vendor/pypi_uv.rscreate the[tool.uv]table.
Activity
mikolalysenko commentedon Oct 14, 2026
[agent] Triaged: priority:p1 (uv, PyPI family). Confirmed on main
fc1b4f9:find_ancestor_uv_config/root_governs_scriptare called only from the script lanes (hosted/engine.rs,vendor/pypi_lock.rs); the project lane never checks for an ancestoruv.toml/[tool.uv]before adding[tool.uv.sources]to a pyproject that had no[tool.uv]. Follows #1501 / #1504 (script lane only), so not a duplicate. No open PR addresses it.Generated by Claude Code
mikolalysenko commentedon Oct 15, 2026
[agent] Claiming this issue (shared root cause: the uv project lane adds a
[tool.uv]table without checking for an ancestor uv.toml / [tool.uv] that it would shadow). Branch: agent/fix-uv-project-ancestor-config. Claim-ID: 2026-10-15T06:21:18Z-cdbb4eGenerated by Claude Code
Start fix for #1545
mikolalysenko commentedon Oct 15, 2026
[agent] Draft PR: #1566
Generated by Claude Code
Refuse uv projects that would hide parent settings
mikolalysenko commentedon Oct 15, 2026
[agent] uv bug-hunt routine (ledger #310): I tested draft PR #1566 (head
b34b23e) against real uv. It fixes the reported shapes, but it also refuses parent configs that uv does not pass down to a child project. maine76cb9bwires those correctly today, so the PR as written would regress them.What real uv inherits from a parent config. Setup: a child project with no
[tool.uv]under a parentpyproject.toml [tool.uv]oruv.toml. I ranuv lockin the child, then appended an empty[tool.uv]to the child and ranuv lock --locked. Results on uv 0.5.31 and 0.13.1:exclude-newer,resolution,index-url, a default[[tool.uv.index]]/[[index]][tool.uv]changes the lock, so refusing is rightconstraint-dependencies,override-dependencies,build-constraint-dependencies(in[tool.uv]or inuv.toml)override-dependencies = ["attrs==23.1.0"]still resolves attrs 26.1.0), and--lockedstays green after shadowingenvironments,required-environments,dev-dependencies,conflictsPR #1566 against main. Fixture:
app/pyproject.tomldepends onsix==1.16.0andattrs==24.2.0, under the parent named in each row. Both modes, uv 0.13.1 (rows 2–3 also on 0.5.31):e76cb9buv.toml:exclude-newer = "2026-10-01T00:00:00Z"--lockedfails (#1545)pyproject [tool.uv]:constraint-dependencies = ["attrs<25"]--lockedgreen; six patchedredirect_uv_project_unsupported, vendored exit 1pypi_uv_ancestor_settings)uv.toml:override-dependencies = ["attrs==24.2.0"]--lockedgreen; six patchedpyproject [tool.uv]:build-constraint-dependencies = ["setuptools<70"]--lockedgreen; six patched[tool.uv] package = false, parentexclude-newerUV_PROJECT_ONLY_KEYSincrates/socket-patch-core/src/utils/python_script.rsprobably needsconstraint-dependencies,override-dependenciesandbuild-constraint-dependenciestoo. Those keys only apply at a project or workspace root. A parent monorepo pyproject that pins its own overrides is a common layout, so this shape is realistic. (I didn't test lock-neutral settings such ascache-keys.)Repro sketch:
Generated by Claude Code