Skip to content

Running Go apply in a second go.work member writes a second replace for the same module@version, so every workspace build fails with "conflicting replacements" while apply, apply --check and VEX all report success #531

Description

[agent] Found by the scheduled Go modules bug-hunt routine (ledger #317).

Summary

A go.work workspace has two member modules that both depend on example.com/upstream v1.0.0, and each member has its own .socket/manifest.json with the same patch. Typical cases are a monorepo where every Go module is set up separately, or a root module plus ./tools.

  • Agent-mode apply --cwd a writes replace example.com/upstream v1.0.0 => ./.socket/go-patches/example.com/upstream@v1.0.0 into a/go.mod. In workspace mode a member's replace applies to the whole workspace, so both members now build PATCHED. That part is correct.
  • apply --cwd b then writes the same relative replace into b/go.mod. It resolves to a different directory (b/.socket/go-patches/…). Go rejects that:
    go: conflicting replacements for example.com/upstream@v1.0.0:
    	…/ws/a/.socket/go-patches/example.com/upstream@v1.0.0
    	…/ws/b/.socket/go-patches/example.com/upstream@v1.0.0
    use "go work edit -replace example.com/upstream@v1.0.0=[override]" to resolve
    
    From then on, every go build / go run / go test in the workspace fails.
  • Even so, both applies exit 0. apply --check reports "Patch redirects are in sync" (exit 0) in both members, and vex attests not_affected (exit 0) in both.

The root-module variant (go.work: use ( . ./tools ), applying in . and then in tools) behaves identically.

This differs from #458 and #393, where the build silently stays unpatched. Here socket-patch turns a working workspace build into a hard failure, and none of its own signals notice.

Impact

Running socket-patch per module in a Go monorepo, which is what you'd expect --cwd to be for, breaks the whole workspace build (local dev and CI). The apply --check CI gate stays green, and the VEX document attests not_affected for a product that can't currently be built.

Repro (Linux, hermetic file GOPROXY, same fixture shape as tests/e2e_golang_build.rs)

export GOTOOLCHAIN=local GOSUMDB=off GOENV=off GOFLAGS= GOPROXY=file://$T/proxy GOMODCACHE=$T/modcache
# example.com/upstream v1.0.0: Greeting() = "PRISTINE"; .socket/manifest.json + blob patch lib.go to "PATCHED"
# ws/a/go.mod: module example.com/a / go 1.21 / require example.com/upstream v1.0.0   (+ go.sum, main.go printing Greeting(), .socket/)
# ws/b/go.mod: module example.com/b / same require, same .socket/ manifest + blob
# ws/go.work:  go 1.21 / use ( ./a ./b )
cd ws
(cd a && go run .); (cd b && go run .)          # OUT: PRISTINE / OUT: PRISTINE
socket-patch apply --offline --cwd a            # exit 0
(cd a && go run .); (cd b && go run .)          # OUT: PATCHED / OUT: PATCHED   (a's replace already covers the workspace)
socket-patch apply --offline --cwd b            # exit 0, "1 of 1 targeted patch applied"
(cd a && go run .)                              # go: conflicting replacements for example.com/upstream@v1.0.0 … (exit 1)
(cd b && go run .)                              # same
socket-patch apply --check --offline --cwd a    # "Patch redirects are in sync (1 redirect checked).", exit 0
socket-patch apply --check --offline --cwd b    # same, exit 0
socket-patch vex --cwd a --offline --product pkg:golang/example.com/a -O v.json   # exit 0, "status": "not_affected"

Reproduced 2× for each variant (two members with no root go.mod, and root . + ./tools) on go 1.24.7, and once each on go 1.22.12 and 1.26.8.

Expected vs actual

  • Expected: docs/ecosystems.md ("Go: directory replaces and go.sum") says user-authored conflicting replacements make apply refuse rather than break the build, and that apply --check gives CI "a read-only audit that the committed redirects still match". README ("socket-patch vex") says the attestation only covers patches that are actually applied. When a go.work that uses the target module also uses another member that already has a Socket replace for the same module@version, apply should do one of two things. It could see that the module is already redirected workspace-wide and skip it (or reuse that copy). Or it could write the replace in go.work, which is the fix Go itself suggests, or refuse with a clear message. apply --check and vex should flag a member replace that conflicts with another workspace member's.
  • Actual: a second, conflicting replace, a broken build, and exit 0 from apply, apply --check and vex.

OS × version

OS go 2 members, no root go.mod root . + ./tools apply --check vex
Linux 1.22.12 fail (build broken, exit 0) not run in sync not_affected
Linux 1.24.7 fail (2×) fail (2×) in sync not_affected
Linux 1.26.8 fail not run in sync not_affected
Linux 1.16 / 1.17 n/a (no go.work before 1.18)
macOS / Windows any not run (probe branches are blocked by stale branches; Windows apply is also blocked by #346)

Vendored (vendor) and hosted modes weren't run this time. Vendored writes a per-member relative ./.socket/vendor/golang/<uuid>/… path, so it very likely conflicts the same way (unverified). Hosted writes a module-path replace (patch.socket.dev/gopatch/<uuid> <sver>), which is identical in both members, so Go should accept it.

First bad version: not a regression. Release 4.0.0 (npm linux-x64-gnu binary) behaves the same as main 61cfb9b.

Suspect code

  • crates/socket-patch-core/src/patch/redirect/golang_local.rs:169 apply_go_redirect → go_mod_edit::ensure_replace_entry (line 304): edits only the --cwd go.mod, with no look at an enclosing go.work or at sibling members' replaces.
  • crates/socket-patch-core/src/patch/redirect/golang_local.rs:452 verify_go_redirect_state (apply --check): checks only the local go.mod and its copy.
  • crates/socket-patch-core/src/vex/discover/golang.rs:103-120: reads only the cwd's go.mod / go.work. A parent go.work and its other members aren't read, so the wiring_conflict gate (crates/socket-patch-cli/src/commands/vex_sources.rs:109) never sees the conflict.

Activity

  1. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p2 (Go). Shares root cause with #458, #393: the Go redirect (patch/redirect/golang_local.rs apply_go_redirect / verify_go_redirect_state, and vex/discover/golang.rs) never reads the enclosing go.work. It edits only the --cwd go.mod and treats that replace as the effective one. Here that means a second member's replace conflicts with the first member's replace, which already applies workspace-wide. Will be fixed together. Not a duplicate: #458 and #393 leave the build silently unpatched, while this one breaks the workspace build.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 2, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Still reproduces on main bf0e0d1 in agent mode (2/2, go 1.24.7). Vendored mode is affected too.

    go.work use ( ./a ./b ), both members require example.com/upstream v1.0.0. A local mock patch service (SOCKET_VENDOR_URL) serves the granted module zip:

    (cd a && socket-patch vendor)         # exit 0
    (cd b && socket-patch vendor)         # exit 0
    (cd a && go run .)                    # go: conflicting replacements for example.com/upstream@v1.0.0:
                                          #   …/a/.socket/vendor/golang/<uuid>/example.com/upstream@v1.0.0
                                          #   …/b/.socket/vendor/golang/<uuid>/example.com/upstream@v1.0.0
    (cd b && socket-patch vendor --check) # exit 0
    • Reproduced 2/2.
    • Each member gets its own replace … => ./.socket/vendor/golang/<uuid>/… with a different absolute target. The workspace refuses to build, while both vendor runs and vendor --check report success.
    • Hosted mode is still untested here.

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:goGo modulespriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions