---
title: Scope
description: The claim magus is built on - that a correct incremental build already requires a precise model of your repo, and everything else is a read of it. Includes the test for what belongs, the boundaries, and where the claim is strained.
tags:
  [scope, boundaries, design, philosophy, prior art, nx, dagger, bazel]
---

# Scope

A build tool that skips work has to know exactly what a change reaches. One that
replays a cached result has to know exactly what went into it. Both demand a
precise model of your repository: every project, every target, what each target
reads and writes, which tools it drives, at what versions.

magus builds that model because correctness leaves it no choice. Everything else
it does reads from it.

## One model, many reads

The model has to exist before magus can be trusted with a single skipped build,
so the expensive part is already paid for. Answering questions from it costs
almost nothing:

| verb                       | the question it answers                              |
| -------------------------- | ---------------------------------------------------- |
| `affected`                 | what does this diff reach                            |
| `query`, `explain`, `path` | what exists, and how do these relate                 |
| `describe file`            | is this generated, and by what                       |
| `refs`                     | where is this symbol defined and used                |
| the version window         | is the binary that ran inside the range you declared |

Take the version window, the newest of them. A tool's version already feeds the
cache key, so magus probes it on every build whether or not you declare a range.
Comparing that probe against a declared window took one comparison and about four
hundred lines, mostly tests and docs.

That is the shape of every verb above. None of them taught magus anything it was
not already forced to learn.

## The verb count

The design produces a long list, and the list is the first thing you notice.
magus runs your builds. It also caches them, tracks which projects a change
reaches, keeps a queryable graph of the repo, ships a daemon, serves a browser
console, exposes an MCP server, resolves secrets, sandboxes subprocesses, gates
dependency advisories, and checks the version of the toolchain a build ran on.

Read as a list, that describes a tool with no clear idea what it is. You have
watched build tooling accrete before: each addition defends itself, nobody
removes anything, and a few years on the command you type all day has a plugin
system, a release manager, and views about your changelog.

Building the wrong thing got cheap, too. You can generate coherent,
well-structured, completely misconceived code in minutes, so a design reading
well proves less than it used to. Someone still has to say no to work that would
otherwise ship.

The number of verbs matters less than where each one came from.

## The test

> Does this answer a question from what magus already knows, or does it require
> magus to learn something new about the world?

Reads stay small. Acquisitions are where a tool loses its shape, and you rarely
notice one arriving.

You can apply the test without trusting us. Ask what new thing the tool had to
learn, then read the diff.

Take the feature that prompted this page. The request was straightforward, fail
CI when the toolchain falls behind, and four designs died before one worked.

<details>
<summary>What got rejected, and why</summary>

A release-feed type in the binary, giving every tool a declared upstream feed so
magus could see which versions exist. Once magus has a releases concept it has to
answer what counts as a release, what LTS means, and when support ends. Three
opinions nobody asked it for.

A support-window table compiled in and refreshed by CI. End-of-life data is not
guaranteed to exist: around five of the eighteen tools in this repo publish a
support window, so the design serves a minority and degrades badly for everyone
else.

Hosting a version feed on the magus site. Still a network call, now to a domain
we control, and it commits a project with no revenue to keeping an aggregator
fresh forever.

Putting the policy in `magus.yaml`. Config merges a user-global tier beneath the
workspace, so a bound written in one person's private file would quietly gate
every workspace on their machine. It also offered no per-project granularity,
which a monorepo needs.

</details>

**One of those four is being reversed, and it should be named rather than
quietly dropped.** A signed registry of release and end-of-life data, hosted on
the magus site and fetched only when you ask for it, is the third rejection: it
is a network call to a domain we control, and it does commit a project with no
revenue to keeping an aggregator fresh. Neither objection has stopped being
true. What changed is that the failure mode is now visible instead of silent:
staleness is measured from a `generated_at` inside the signed file, so an
aggregator nobody is maintaining reports itself as old rather than serving
confident stale dates. The file is keyed by upstream product slug and carries no
magus concepts, so mirroring it is a copy rather than a port, and a mirror is
what you reach for when you would rather not depend on us. Declining is
`enabled: false`, and a declined install is silent forever rather than nagged.

The other three stand, and the registry does not smuggle them back:

- **No release-feed type in the binary.** The registry is data magus fetches, not
  a concept it implements. It republishes a third party's fields; it does not
  decide what counts as a release or what LTS means, and there is nothing in the
  binary that would have to answer those questions.
- **The data is still sparse.** Around five of the eighteen tools here publish a
  support window, and a registry does not create the other thirteen. It reports
  `-` for them. Serving a minority was only a fatal objection when the design
  degraded into a wrong answer; reporting "unknown" for most tools is honest.
- **The policy still is not in config.** What a source-of-truth URL is gets
  configured; what version range your project accepts does not, and stays where
  it was. Nothing the registry reports fails a build - it is a column in a
  report and a doctor line, never a gate.

Read what a signature does and does not prove before relying on it: it says the
file is the one our pipeline published, unmodified. It does not say any date in
it is correct. magus did not author this data and cannot verify it.

The surviving design is a version range you declare, compared against a version
magus already probes. It never learns which versions exist upstream and never
picks one. To find out when your range has gone stale, write that in your own
repo with `http`, `json`, and `semver`, the way `tools/audit.buzz` wraps the
advisory scanner. The binary supplies primitives. You supply knowledge about the
world.

## The line

Name a boundary before anyone wants to cross it, or naming it proves nothing.

magus will not select a version, install one, or move you to one. It compares
what ran against what you declared. An install, switch, or resolve verb under
`tools:` is the creep, and it will arrive with a good argument attached.

magus will not require a second toolchain to build your projects. No container
runtime, no language runtime to provision, no separate binary. The daemon carries
an asterisk; see below.

magus will not be recommended for install through a package manager belonging to
a toolchain it manages. Install a build tool with npm and you need a Node runtime
before you can run the thing managing your Node builds. When it breaks, you fix
it by upgrading the toolchain you were using magus to pin. Those failures are
oblique, hard to guard against, and there is rarely anywhere sensible to attach
an error explaining them. The same objection rules out `go install`. Anyone who
knows what they are doing will do it anyway; nobody should be pointed down that
path.

magus will not generate code your build depends on. Nothing it writes into your
repository has to exist or be current for `magus run build` to work. That promise
is narrower than "magus writes nothing into your repo", because it writes several
things; see below.

magus will not require an account or a subscription, and no capability sits
behind a paid tier. Nothing exists to upsell.

magus will not decide for you. It answers questions. You decide; an agent may
propose.

## The container question

"No container runtime" is one clause of one rule above, and it is the clause
with the best argument against it, so it gets its own section.

The argument is Dagger's, and it is a good one. Run every step in a container
against a pinned image and the environment stops being a variable. No more
"works on my machine": the machine is the same machine. That is a real property,
and it removes an entire category of support burden.

**A container gives you environment reproducibility, not hermeticity.** A pinned
image fixes what is installed. Inside it, a step can still reach the network,
read the clock, resolve a floating tag, or depend on filesystem ordering.
Nothing fails a build there because a step read a file it never declared. That
is what Bazel's sandbox buys - the undeclared file simply is not there, when the
sandbox is in use - and containerizing does not supply it. The certainty a
container produces is partly a feeling, and the feeling is
what makes the runtime dependency seem cheap.

**The dependency is disqualifying at this level specifically.** A task
orchestrator is the thing you reach for before anything else works. If it needs
a container runtime, it cannot be used to install or check that runtime; it is
unavailable on a locked-down laptop, a rootless runner, or an air-gapped
builder; and when the daemon is not up, the failure arrives oblique, far from
the cause, with nowhere sensible to attach an error explaining it. That is the
same shape as the rule about package managers above - a tool that arrives
through the thing it is meant to orchestrate has put itself downstream of it.

So magus pursues determinism without the runtime, through mechanisms that need
nothing installed: inputs are declared and hashed, the child environment is
rebuilt from an allowlist rather than inherited, every tool's version is part of
the cache key, and a declared version window fails the build when what
ran falls outside it. Where a container normalizes the world, magus describes it
precisely and notices when the description stops matching.

That is a harder route with more moving parts, and being specific about the
remaining gap matters more than the claim does. Undeclared **file reads** are
caught only by the [sandbox](concepts/sandbox.md), which is off by default and
has no kernel layer on macOS - so on the machine most of this is written on, an
undeclared read succeeds silently. **Network egress is not confined at all.**
Until both close, magus's determinism is "the inputs you declared are hashed and
the environment is scrubbed", which is weaker than what a container gives you on
the filesystem axis and stronger on the version axis. Weigh it that way when
you compare the two.

One thing this page must not imply, because the wording invites it: **magus has
no opt-in container isolation, and the `container` charm is not it.** In this
repo `magus run build:container` selects a different _artifact_ - it builds and
signs an image instead of a host binary - and the build itself still runs on the
host, unconfined. That is container-grade packaging, not container-grade
isolation. Reading the charm as "the Dagger guarantee, available per run" is a
misreading this section previously encouraged.

So the honest position is narrower than "you can have it when you want it": if
you need every step to run in a fixed environment, magus does not offer that at
any setting, and a container-native runner is the better tool. What magus offers
instead is a precise description of the environment plus a build that fails when
what ran falls outside what you declared.

## The knobs

The test above governs verbs. Options need their own, because they are the other
way a tool loses its shape, and the cheaper one - nobody blocks a pull request
over one more setting.

An option is not additive. Each independent switch doubles the number of states
the tool can be in, and the states nobody thought about are where the
bugs live, because no one wrote a test for a combination no one imagined. A
configuration surface large enough to be flexible is large enough that its author
cannot enumerate what it does. You have met the result: a build that works on one
machine, a setting three people cargo-culted from a blog post, and a maintainer
who cannot tell you what turning it off would break.

So the default is the product: correct behavior must never require a knob.
magus is opinionated where an opinion prevents a footgun: one
required target name, four reserved charms, `skip_cache` demanding a reason
string rather than a boolean, no fallback chain when a secret will not resolve.
Each of those removes states rather than adding them.

The honest numbers, because this is the section where a claim like that gets
tested. `magus.yaml` accepts about a hundred keys, container and leaf together,
and magus binds 45 of them to command-line flags. Five more globals - `-o`,
`--tee`, `-v`, `--quiet`, `--silent` - are display switches that answer to no
config key at all, so 50 flags stand in front of any subcommand's own. That is
not a small surface, and calling it zero configuration would be a lie.

What the claim rests on is the second number. magus's own `magus.yaml`, for a
ten-project polyglot repo that publishes containers, signs releases and runs a
sharded CI pipeline, sets five things:

```yaml
default_charms: [rw]
sandbox: { env: { passthrough: ["GO*"] } }
cache: { remote: { trusted_keys: ["..."] } }
required_version: ">= 0.4.0"
knowledge: { notes: { shared: notes }, vcs: { enabled: true } }
```

Four of those five are facts about this repository that no default could
supply - a trust key, an environment passthrough, a version floor, a notes
directory. Only `default_charms` is a preference. Every other key exists for a
workspace whose situation we did not anticipate, and the measure of whether that
is discipline or sprawl is whether we reach for them ourselves.

The rule that follows: **a new option must remove a failure, not enable a
preference.** If the answer to "what happens if I set this wrong" is "your build
is subtly different and nothing says so", that is a trap with a default.

Where this is strained: an option nobody can find is worse than one that does not
exist, because the escape hatch is real and the person who needs it is told it is
not. This section used to name `cache.include.os.enabled` and
`cache.include.arch.enabled` - the switches deciding whether host OS and
architecture key every cache entry - as that option, absent from the one page a
reader would consult. They are documented now.
[The cache-key reference](concepts/cache.md) carries both, says they default to
off, and says why: the manifest records the platform an entry was built on and
refuses a replay onto a different one whatever these are set to, so leaving them
out of the key is what buys an output reference naming the same run on a laptop
and on CI. `TestCacheIncludeDefaultsOff` pins that default against the pull to
flip it.

The general case is not fixed. Every key reaches the generated configuration
reference, which proves a key is documented, not that anyone can find it without
already knowing its name. Nothing gates a new option on being reachable from the
page describing the behavior it changes; that is still whoever wrote it thinking
to write it down twice.

## Where the claim is strained

The claim is that magus only reads a model it already had to build. Six places
strain it.

**It writes versioned artifacts into your repository.** `magus agent install`
writes agent skills into the directories you name (`.claude/skills/`,
`.agents/skills/`, `.opencode/skills/`), each stamped with a magus-internal
version. It does NOT write `AGENTS.md`: that file is yours, so install prints
the managed block for you to paste. `magus doctor` reports on every one of
those surfaces, the pasted block included:

```text
[pass] agent-skills: 4 install location(s) current with this binary
    .agents/skills: up to date (skill v47, schema v9)
    .claude/skills: up to date (skill v47, schema v9)
    .opencode/skills: up to date (skill v47, schema v9)
    AGENTS.md: up to date (skill v47, schema v9, content 00482e4b1658)
```

`MAGUS.md` lands at your repo root. The git merge driver writes your tracked
`.gitattributes`. The Dagger criticism below applies here too.

magus claims that none of it gates a build: delete every one and `magus run
build` still works, delete Dagger's bindings and nothing compiles. That
distinction is thinner than it sounds. Nothing in CI observes the installed
skills here either - they are gitignored renderings of the binary, invisible to
the drift gate, and only `magus doctor`'s agent-skills check reports them stale,
per workspace. Leaning on build-versus-CI is the move this page exists to catch.

**`magus init spell` writes source into your repository.** The scaffold it drops
at `spells/<name>/spell.buzz` is a real spell: the contract stubbed, each
function documented inline, a runnable test block. It satisfies the letter of
the rule above and is the closest thing in the tool to breaking it. Written
once, hand-edited from there, never regenerated and never compared against
anything, so nothing about a build depends on magus having produced it - which
is the whole distinction, and it rests on the word generated.

**The daemon runs long, and two surfaces do not work without it.** It ships
inside the binary, so you install nothing extra, and no build needs it. The
sharpest version of this entry has since been retired: `magus doctor` used to
probe bridge reachability and FAIL when no daemon was running, which made doctor
red on every machine in its ordinary state. It reports the bridge as skipped
now, with the daemon check immediately above it already saying the daemon is
down. What remains is real - the console and the MCP server both need one
running. "No second toolchain" holds for installation and holds less firmly at
runtime.

It got one degree less firm on 2026-09-02. `magus run` and `magus affected` now
START a daemon if none is up, because it owns the machine-wide build budget and
nothing else can arbitrate it, so a run is the first command that spawns a
long-lived process without being asked to. Two things keep this inside the
promise rather than outside it: no capability is lost when the daemon cannot
start (the run proceeds unarbitrated and says so once), and a daemon started this
way exits by itself after ten minutes of nobody needing it. Still nothing extra
to install; one more thing that runs. The reasoning, and the doctrine it amends,
are in [the daemon decision](guides/integrations/editor/design.md).

**The upgrade path runs on a server we operate.** `magus self update` fetches
`https://eli.gladman.cc/magus/public/release/index.json`. We sign the releases
and you opt into the check, and it remains infrastructure we control, in a
project that pitches not having any.

**The docs describe a `go install` path.** `docs/setup/mise.md` documents the
route the rule above says not to recommend, because you will find it anyway. It
carries a warning giving the structural reason. Documenting a route while telling
you to avoid it is a compromise, and a tension.

That page spent a long time discouraging `go install` for a cosmetic reason: it
cannot pass the `-ldflags` that stamp the version, so an installed binary
presents itself as an unstamped dev build. Two harder reasons sat underneath it.

One was a bug and is gone. `internal/compress` gated the native xz and zstd
implementations on the `cgo` build tag, `CGO_ENABLED` defaults to 1 wherever a C
compiler exists, and an ordinary install then demanded `liblzma` and `libzstd`
headers through pkg-config before dying without naming magus or the fix. No
maintainer ever hit it, because maintainers have the headers. The native codec is
opt-in now.

The other is not a bug and is not fixed: `go install` does not work at all. The
root `go.mod` requires the nested `libs/gopherbuzz` and `libs/diagnostics`
modules - each with its own `go.mod` - at `v0.0.0`, and resolves them only
through this repo's own local `replace` directives. Neither nested module has
ever been tagged, and `go install pkg@version` refuses outright to build any
module whose `go.mod` carries a replace directive unless it is the main module
of the build, so the install dies on the replace lines before dependency
resolution starts. Nothing on the consuming side gets around that; tagging the
nested modules is what would. The route the rule above says not to recommend is
therefore a route that currently does not run, which makes documenting it and
warning you off it less of a compromise than it reads as.

**magus's own console needs a Node toolchain to build.** The tool that ships as
one statically linked binary contains a project requiring pnpm and esbuild. That
concerns developing magus rather than using it, and it reads as hypocrisy when
somebody else finds it first.

A seventh sits close to the line without crossing it. A version range is policy
living inside a build tool, and you can reasonably say pinning belongs to a
version manager. A version manager pins what to install; magus checks what ran.
Those diverge more often than you expect, and only the second can fail your build
with a message naming the cause. magus already held the answer.

The repo artifacts and the update endpoint are rules stated harder than the code
earns, so we narrowed the rules. The daemon and the console toolchain are
deliberate trades worth re-examining. The codec gate was simply a bug, fixed
separately once someone looked.

## Where others drew it

Some of these projects shaped magus directly. The disagreements are specific.

Mage came first, and the name says so. magus began as something compatible with
it - magefiles in an adjacent project, exported functions as targets, build
logic in the language you already write - and evolved from there keeping that
shape whole. The divergence is the engine underneath. A magefile runs; a
magusfile is modeled, so the same declaration that dispatches a target also
feeds a cache key and an affected set. And Mage arrives through the Go toolchain
it drives, which is the loop the bootstrap rule above exists to end: magus
embeds its language precisely so the orchestrator installs through none of the
toolchains it orchestrates.

Nx comes up here because of proximity: it is what this project's author has used
most and most recently, so its edges are the memorable ones. Its project graph is
the closest prior art for affected sets. The disagreement is surface area. `nx
release` and `nx generate` do read that graph, but each arrives with its own
model stacked on top - release groups and version plans for one, a virtual
filesystem and schema-driven generators for the other - and the CLI registers
over fifty commands, with forty-odd first-party plugins beside them. Remote
caching, the capability many teams adopt a monorepo tool for, routes through Nx
Cloud, a product the same company sells. And every install channel Nx documents -
npm, Homebrew, apt - puts a Node runtime underneath the tool managing your Node
builds, which is the concrete case behind the rule above.

Dagger's SDK design is excellent and influenced magus's API. It costs a required
container runtime, and authoring a Dagger module generates bindings into your
repository.

Bazel can buy real hermeticity; the price is declaring your toolchain rather
than inheriting the host's. The trade is coherent and the cost is large.

Each of those took on a new capability. magus's additions read a model it already
had to build, and the test above is how to check any one of them against its diff.

The agent-harness world drew the line at the opposite extreme, and the contrast
is worth stating because magus keeps being read as a member of that category.
deepseek-harness's published decision notes sharpened the lease surface's
checkpoint and release semantics, which is why the architectural disagreement
deserves stating precisely. It makes every module a plugin - the model adapter,
the tool registry, the session log, the agent loop itself - so there is no
privileged core to patch: what remains is a small kernel, Cordis, owning the
context, the service registry, and plugin lifecycle, with a few services still
marked core rather than swappable. For a harness that is a defensible bet: a harness is an orchestration shell,
its behavior is supposed to be yours, and hackability is the product. magus made
the opposite bet for the opposite reason. A build tool's product is its verdicts:
the cache saying a replay is honest, the drift gate saying generated output
matches source, `ci` stripping the write charms whether or not anyone remembered.
A verdict a plugin can rewrite proves nothing. So the engine, the cache, the
graph schema, and the guard's evaluation are sealed on purpose, and every
extension point magus
does have - spells, the magusfile, charms, skills, config - is a DECLARATION the
sealed engine evaluates, auditable in a diff the way code loaded at startup never
is. The test for a proposed extension seam: it may change what magus does, never
what a verdict means. And one seam is absent deliberately rather than sealed:
there is no model adapter, because magus never calls a model.

## Unsettled

The engine carries complexity the constraint camp avoids by limiting their
configuration language. Starlark forbids unbounded loops so evaluation stays
deterministic and hermetic; Dhall goes further and guarantees termination. Nix
made a different trade - its language is Turing-complete, and the containment is
purity and a sandboxed store. The constraint camp's bet is that a language which
cannot express a footgun never fires one.

magus keeps the full language and bets on legibility instead. The documentation
was written before the announcement, not after it, and ships from day one. An
error magus raises is written to be read once and acted on: it carries a
diagnostic code, the code has a page, and the page says what tripped and what to
do next - so when the expressive language does something surprising, the way out
is in front of you rather than in a maintainer's head. The sandbox and the
content-addressed cache sit under that as the floor - what is enforced stays
enforced - but they are not the thesis. The bet is that legible failures remove
footguns without shrinking the language.

Nobody has settled that bet. A large enough body of magusfiles rots the way any
large program rots, and neither documentation nor error discipline rules it out.
If that happens, this page was wrong about something load-bearing, and it should
say so here.
