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.
What got rejected, and why
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.
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, 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:
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 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:
[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.
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.