---
title: "MGS1020: output owned by two targets"
description: Fires when two targets in the same project declare the same output glob, so whichever runs last wins and the other target's drift gate fails on the next run.
tags: [MGS1020, magusfile, outputs, ownership, generated, drift, formatter]
---

# MGS1020: output owned by two targets

Two targets in the same project declare the same output glob, so both write the
same bytes and the result depends on which ran last.

```text
[fail] output ownership: 1 output glob(s) declared by more than one target
    docs: output glob "gen/**" is declared by format and generate
```

## Why this is not an ordering problem

The instinct is to add a dependency edge and settle the order. That cannot work,
and seeing why is the whole point of this code.

Say `generate` writes `gen/**` and `format` rewrites it:

- Run `generate` then `format`, and the formatter's bytes are what land on disk.
  The next `generate` regenerates unformatted output, sees it differ from what is
  committed, and fails its drift gate.
- Run `format` then `generate`, and the formatting is immediately undone. The
  formatter's own check fails instead.

Whichever runs last wins, and the loser's gate fails on the next run, at every
possible ordering. Ordering resolves a producer and a consumer. Here there are
two producers of the same bytes, which is an ownership violation, and no
dependency edge fixes it.

## The rule

**A generated file has exactly one owning target: the one that declares it.**

If generated output needs to be formatted, the generator formats it, as the last
thing it does. It still owns the final bytes, and its drift gate compares
formatted output against formatted output. This is why generated Go needs no
special handling here: `mockery` and `protoc` emit gofmt-clean output already.

Excluding generated trees from a formatter is the weaker fallback, correct when
nothing else needs to read the output. Every formatter in this workspace does it
(see `.markdownlintignore` and the `!src/gen/**` entries in `biome.json`),
because a lint rule fixed in generated output is a fix in the wrong place: it
belongs in the generator, which will overwrite the edit on its next run anyway.

## Fixing it

- Decide which target owns the glob and remove the other's declaration.
- If the second target genuinely needs to touch part of the file rather than
  produce it, declare `ctx.modifiesExistingFiles(...)` instead. That records a
  read-and-amend relationship, and magus will neither snapshot, replay, nor
  clean those bytes. See [Files a target edits rather than
  produces](../../../concepts/cache.md#files-a-target-edits-rather-than-produces).
- If the two really are one step, merge them into one target.

## What this does not catch

Only **declared** writes are visible. A formatter that rewrites a tree without
declaring `ctx.writesFiles(...)` is invisible to a static check, which is the
reason formatters are also excluded by configuration rather than relying on this
diagnostic alone.

## See also

- [MGS4002](../race/MGS4002.md): the cross-project sibling, where two _projects_
  declare the same output glob under the same target.
- [MGS1019](MGS1019.md): a generated file that records VCS state, and so restales
  itself on every commit.
- [Cache model](../../../concepts/cache.md#the-two-roles-of-an-output-maintainer-note):
  what an output means to the cache.
