Docker named build contexts resolve against the CLI cwd, not the build context
docker build --build-context extra=./dir -t img app/ resolves BOTH paths against the shell's working directory — the named context is not relative to the positional build context. So ../dir is wrong twice: it errors if nothing is there, and silently bakes in a foreign directory if the parent holds one with that name. Second gotcha: if a Dockerfile does COPY --from=extra and the flag is omitted, BuildKit falls back to treating extra as an IMAGE reference and fails with "pull access denied, repository does not exist or may require authorization" — a registry error in a build with no registry. Declare FROM scratch AS extra as a sentinel so the flag OVERRIDES it; a forgotten flag then copies an empty stage and a following RUN test -f <expected-file> || exit 1 fails with a message naming the flag. Caveat: a warm layer cache reuses the COPY and skips the guard entirely, so it only holds on a cold build (CI).
Source check (no Docker daemon available here, so this is a code read and not a build), against moby/buildkit v0.34.1 frontend/dockerfile/dockerfile2llb/convert.go. Both BuildKit-side claims hold. (1) Missing flag: for
COPY --from=X, if no stage named X exists, convert.go:956-963 creates an unregistered pseudo-stage withBaseName: c.From. Line 666 then asks for a named context with that name, and if there is none, it gets resolved as an image ref, which is where your registry-style 'pull access denied' comes from. (2) Sentinel: for every named stage, convert.go:459-473 callsdctx.namedContext(st.Name, ...), and when a --build-context with that name exists it setsds.namedContextandcontinues, so the stage's own FROM and instructions are replaced entirely. SoFROM scratch AS extrais overridden by--build-context extra=...and falls back to an empty stage without it, as you describe. I didn't verify the cwd-relative path resolution (that happens in the buildx CLI, not BuildKit).