---
title: "MGS4007: undeclared source modified"
description: Fires when a target rewrites a file it declared as a source without declaring it with ctx.modifiesExistingFiles, so the cache key that identified the result no longer describes the inputs that produced it.
tags: [MGS4007, race, cache, sources, charms, formatter, outputs, declarations]
---

# MGS4007: undeclared source modified

A target finished successfully, and one of the files it declared as a **source**
had different content afterwards than before. The target did not declare that file
with `ctx.modifiesExistingFiles(...)`.

```text
[MGS4007] .:format modified 1 file it declared as sources; declare them with
ctx.modifiesExistingFiles(...) or stop writing them: types/diagnostic.go
```

## Why this is not a style rule

A cache key is a claim: _these inputs produced this result_. Sources are hashed
before the target runs, so a target that rewrites its own sources invalidates that
claim in the same breath it makes it. The stored entry describes a tree that no
longer exists, and nothing can reproduce it - not a teammate, not CI, not the same
machine a second later.

magus already noticed. `recordOutput` recomputes the key after every run and
declines to persist the key's explanation when it disagrees, because those lines
would blame the wrong input. That branch has always existed; until MGS4007 it only
whispered at debug level, so the run still passed and the entry was still stored.

## The charm trap this was built for

Charms patch a command's arguments. A charm cannot say whether the resulting tool
writes, which is exactly how a check turns into a write with nothing in its name
changing:

```text
default_charms: [rw]        # magus.yaml - every local run carries it
"rw": append(["-w"])        # the op's charm - the tool's autofix flag
```

Compose those and a target called `lint` rewrites the tree on a developer's
machine, while CI - which strips default charms - runs the identical target
read-only. Neither run can reveal the other. That divergence is the defect;
the spelling of the charm is not.

So this code is **charm-agnostic** on purpose. Keying it on the charm was
considered and rejected twice over: a rule that exempts a charm the caller typed
lets a mutating custom charm through merely because someone spelled it out, and a
rule that exempts `rw` specifically says nothing about the charms a workspace
defines for itself.

## Fixing it

**Declare the edit, when writing is the point.** A formatter genuinely modifies
existing files, and `ctx.modifiesExistingFiles` is the declaration that says so:

```buzz
export fun format(ctx: magus\Context, args: [str]) > void {
    ctx.modifiesExistingFiles("**/*.go", "go.mod", "go.sum");
    go["go-fmt"](ctx);
    go["go-mod-tidy"](ctx);
}
```

Declare it unconditionally, at the top of the target, even when a charm decides
whether the edit lands - the same shape `spells/golang/gomod.buzz` uses for
`go.mod`. The globs fold into the source set either way, so the check-only variant
is unaffected.

**Or stop writing, when it is not.** A target named for checking should not carry
a write. Hang the autofix off its own charm rather than a workspace default, so it
happens because someone asked:

```buzz
return Command{bin = "typos", args = args, charms = {
    "fix": append(["-w"]),   // not "rw": that one is a default charm
}};
```

## Why modifiesExistingFiles rather than writesFiles

`writesFiles` declares an **output**: magus snapshots it, replays it on a cache
hit, and `magus clean` removes it. That is wrong for a file the target amends but
did not author - a replay would restore stale bytes over someone else's edit, the
two-owner hazard [MGS1020](../magusfile/MGS1020.md) reports.

`modifiesExistingFiles` records read-and-amend instead: the globs join the source
set (so editing the file still invalidates the target that maintains it) and stay
out of the output set (so nothing snapshots, replays, or cleans them).

## What it checks, and what it does not

The check runs only after the target **succeeded**. A failed target has already
reported why it failed, and a second diagnostic on top would bury it.

It compares **content**, not mtimes: a tool that rewrites byte-identical output
has changed nothing a cache key can observe, and reporting it would train readers
to ignore the code.

A source that **disappeared** counts - deleting an input is a write, and the key
then describes a file that is not there. A source that **appeared** does not: a
target may legitimately produce a file some broad source glob would have matched,
which is [MGS1028](../magusfile/MGS1028.md)'s question rather than this one.

Only **declared** sources are visible. A target that rewrites a file no glob
claims is invisible here, the same limit [MGS1020](../magusfile/MGS1020.md)
records for static output checks.

## See also

- [MGS1020](../magusfile/MGS1020.md): two targets declaring one output - the
  ownership violation this one is the runtime counterpart to.
- [MGS1028](../magusfile/MGS1028.md): a file read but not declared, the
  under-declaration this one mirrors on the write side.
