Agents
magus knows more about a workspace than any agent can rediscover by reading files: the project DAG, every target's inputs and declared outputs, which files are generated, what a diff affects, where churn and coupling concentrate.
The agent surface exposes that through three artifacts, and one rule governs all of them. Every tool answers a question from declared sources; none of them decides, plans, or injects itself into the agent's context. Answering is the tool's job, deciding is the model's.
| artifact | answers | freshness |
|---|---|---|
MAGUS.md |
WHAT is in this workspace (targets, counts, anchors) | regenerated with the workspace (magus describe graph -o markdown) |
| Skills | HOW to use the magus tool surface | ships with the binary; versioned, drift-checked |
| MCP surface | live answers (query, run, explain, logs) | host-managed; clients discover it at task start |
The split is deliberate: skills never mention workspace specifics, so they go stale only when the tool surface changes, and that staleness is detectable.
MAGUS.md is per-project as well as per-workspace. A nested project can commit
its own, generated by its generate target and scoped to that project's
targets, so an agent working inside one project gets a routing index sized to
it. The root index links each project that declares one instead of repeating its
targets, and a project's index links back up to the root and across to the
projects it depends on.
Hosts, not LLM providers
An LLM provider supplies a model; a host (or harness) decides which skills, MCP tools, hooks, and instruction files that model receives. Claude Code, Codex, Cursor, and OpenCode therefore differ at the host layer, even when they use models from the same provider. magus normalizes its guidance and guard verdict, not those host capabilities.
Set up your host
A full setup is two steps: install the guidance where the host reads it, then wire the guard hook. Both are on your host's page.
| host | skills | guard |
|---|---|---|
| Claude Code | .claude/skills/ |
shipped PreToolUse scripts via harness spell |
| Codex | .agents/skills/ + AGENTS.md |
shipped PreToolUse scripts via harness spell |
| Cursor | AGENTS.md only | one self-contained script |
| OpenCode | .opencode/skills/ |
TypeScript plugin (harness spell is skills-only) |
| Any other host | wherever it reads them | a collaborator-owned harness descriptor or small adapter |
What you wire is yours. Prefer a harness spell selected from the root magusfile
(import "ghcr.io/egladman/magus/spells/<id>"; then magus\harness.provider(<id>),
with the spell declared in magus.yaml and pinned in magus.lock;
Remote spells explains the pin).
Wire several providers when you bounce between hosts; each call appends.
To adapt a Buzz harness without modifying Magus source, copy the shipped
spell into the workspace and declare the copy as a path: override of the
registry path in magus.yaml. The import and magus\harness.provider(...)
stay as they are. Edit the workspace Buzz, merge what magus describe harness
prints, then run magus agent harness verify. That ownership switch is documented in
the workspace-rules skill
under "Adapting a Buzz harness" and in Recurring guard friction. Additive deny/advise
that is not host-shaped stays in magus\guard.shell({...}).
magus never writes host config. magus describe harness (no id) prints, for every
magusfile-wired provider, each entry its host files lack and the one magus buzz
command that merges them; you read the command and run it. magus describe harness <id>
targets one spell, and -o json prints the exact fragments. magus agent harness verify
reports whether the fragments are still present and runs the wired guard command. A host Magus has never heard of can
ship its own harness spell without a Magus release, by the same ownership switch
as adapting a shipped one; a portable adapter remains an option where a native
event contract is not available. Doctrine
records that trade and what it costs you.
This repository as a worked example
The magus repository wires every shipped provider on itself and commits what
that writes, so the wiring can be read whole rather than assembled from the
pages above. Its maintainers do not use every host; the files for the others are
kept as examples. magus agent harness verify reports whether each is still what
its spell writes, and the root tests grade the committed hook configs against
each host's vendored schema.
| file | what it shows |
|---|---|
magusfile.buzz |
the four magus\harness.provider(...) calls that select hosts |
.claude/settings.json |
Claude Code guard hooks, as the spell writes them |
.codex/hooks.json |
Codex guard hooks |
.cursor/hooks.json |
Cursor hooks, all routed through one script |
AGENTS.md |
the managed skills block between its magus:skills markers |
Installed skills are not committed, because magus agent install regenerates
them from the binary. The two under .claude/skills/ that are committed are
written by hand for working on magus itself.
The shared reference pages sit behind those: Skills for the
install surface, The guard for what is denied and why,
Guard hook templates for the files Claude Code
and Codex run, Session load adapters for reading a
host's own session log back into magus,
Attention hooks for magus session notify,
and Jobs and leases for the surface an agent uses when it
fans work out across several.
When a session loses its history
A host that compacts a long session replaces what happened with a summary of it, and the model works from that summary. The branch it is on, what it has already changed, which rules it agreed to, and the failure it was in the middle of all survive only as a retelling, and nothing in the transcript says how much each retelling lost.
magus session --brief answers that with state instead of prose: branch and
revision, commits not yet on the base ref, the dirty tree split by the same
classifier magus describe file uses, the live leases with the command that
binds each one, the last recorded run's failures with the ref that holds their
output, whether any hook config here invokes magus, and where this workspace's
rules live. When recurring guard evidence needs a human decision, it adds one
bounded line pointing to magus doctor (its recurring-guard-denials check).
Every line is read off the disk on the call, so none of it can
degrade; it restates no rule, because a rule copied into a context block is a
second copy to go stale.
magus prints it and your host places it. Wire
magus-rehydrate.buzz to whatever
event your host fires after compaction: SessionStart with matcher compact on
Claude Code and on Codex, where
--format json wraps the text in the reply Codex parses.
OpenCode has no hook to point at a file, so its plugin pushes
the same brief into the compaction prompt itself. Cursor is
the one host that cannot: preCompact returns a message for the person and
nothing for the model, so there run the command yourself and read the same thing.
The review belongs here rather than in a Stop hook: a stop event can record
evidence, but cannot reliably inject an actionable message into the model's next
context.
Parity across hosts
Every host gets the same RULES: they come from one binary, and none of them is per-host. What differs is how much of a verdict a host's hook surface can carry. Claude Code is the reference contract: parity means every supported adapter exercises the same guard decision cells and preserves the verdict semantics. It does not mean vendors expose the same tools, payloads, or lifecycle events.
Manual host end-to-end harness
The ordinary checks are deterministic: vendored schemas validate configuration and reply shapes, and the fixture-driven transport cases execute every guard decision cell against the shipped adapters. That proves the guard's decision and adapter behavior for controlled events; it does not prove that a locally installed vendor client discovers configuration or dispatches a real hook.
That separate, opt-in end-to-end check currently proves one narrow boundary: host discovery and delivery of a denied command. It is not a general host certification or a replacement for the deterministic cases:
MAGUS_HOST_E2E=1 magus run host-integration docs/guides/integrations/agents -- \
host-e2e/claude-code.json host-e2e/codex.json host-e2e/opencode.json
It is deliberately not part of ci or magus affected ci. The runner makes a
temporary Magus workspace and applies each supplied descriptor's isolated host
configuration, then asks the host to use Bash for the denied command. The guard must
intercept that call and render a deny; a broken hook can affect only the
temporary workspace. A missing selected host is reported as a structured skip,
while an installed host that never dispatches its hook is a failure. A run in
which every selected host skips is inconclusive and exits nonzero
rather than looking like a pass. Failures retain their workspace with the trace,
generated configuration, and capped host stdout/stderr logs for inspection.
Successful workspaces are deleted by default; set MAGUS_HOST_E2E_KEEP=1 to
retain them deliberately.
It can use local authentication and quota, so the environment variable is a second explicit confirmation. The descriptor is the extension seam: a user can add a host without changing the runner by declaring its binary, isolated setup, launch command, and evidence contract. A descriptor must represent a real host dispatch path; the runner never substitutes a fixture and calls that an end-to-end pass.
| command rules | declared-output rule | deny | advise | manual E2E: command discovery and block dispatch | MCP call rules | |
|---|---|---|---|---|---|---|
| Claude Code | yes | yes | yes | yes | yes | wired, rule-empty; live pending |
| Codex | yes | yes | yes | yes | yes | wired, fixture-verified; live pending |
| Cursor | yes | yes | yes | yes | undocumented | gated: beforeMCPExecution (advise unwired) |
| OpenCode | yes | yes | yes | yes | yes | not wired: sees call, tool name unconfirmed |
"Fixture-verified" means the adapter executed against this binary with a
controlled host event on stdin. "Manual-E2E-proven" means a locally installed
host discovered an isolated configuration, dispatched the git stash command
hook, and delivered its deny. Neither label proves filesystem-path delivery,
MCP delivery, or lifecycle delivery unless that cell says so; those remain
pending. "Wired, rule-empty" means the transport carries a verdict end to end
but the guard has no MCP-specific rule yet, so every call passes, not because
the channel is silent, but because nothing has been asked of it.
Both decisions now reach the model everywhere, which they did not until recently: one host was sent no advisory at all, one delivered it to the person on stderr, and one logged it. What still differs is WHEN. A host whose gating event carries a message only with a denial has to send the explanation on its post-tool event instead, which means the call has already run and that magus was asked about it twice, leaving two rows in the activity trail where the other hosts leave one.
What each host carries
One row per job magus does through a host event. A cell names the event that carries it, or the reason nothing does. This is the intended host capability mapping, not a claim that the manual command/deny E2E run has proven delivery of each row. Path, MCP, and lifecycle delivery remain pending unless the evidence matrix above explicitly marks them proven.
| job | Claude Code | Codex | Cursor | OpenCode |
|---|---|---|---|---|
| command guard, deny | PreToolUse Bash |
PreToolUse Bash |
beforeShellExecution |
tool.execute.before |
| command guard, advise | additionalContext |
additionalContext |
postToolUse.additional_context |
tool.execute.after |
| write guard, deny | PreToolUse on the edit tools |
PreToolUse on the edit tools |
preToolUse |
tool.execute.before |
| write guard, advise | additionalContext |
additionalContext |
postToolUse.additional_context |
tool.execute.after |
| MCP call guard | PreToolUse mcp__magus__.* (rule-empty) |
PreToolUse mcp__.* (fixture-verified; live dispatch pending) |
beforeMCPExecution (advise unwired; failClosed) |
not wired: tool name unconfirmed |
| post-compaction rehydration | SessionStart compact |
SessionStart compact (JSON envelope) |
not expressible after compact; sessionStart sets env + budgeted brief |
not wired: only a PRE hook exists |
| checkpoint | Stop |
Stop |
sessionEnd |
the session.idle bus event |
| lease provenance | PreToolUse on the sub-agent tool |
no event carries the handed prompt | subagentStart (unverified live) |
not wired: no confirmed tool id |
| read observation | PreToolUse Read |
not wired | not wired | not wired |
| tool-failure hint | not wired | not wired | not expressible: no response fields | not wired |
| session-load adapter | ships one | ships one | none written | ships one |
Two kinds of blank belong in that table and they are not the same. Not expressible is a host contract magus cannot reach through, and it is named rather than approximated. Not wired is a choice: a hook earns its place by changing a verdict or restoring state the model cannot otherwise get, and read observation changes neither: it records a path for the activity trail, which is worth one host's wiring and not four.
Everything else is additive. A host missing a file-write hook still gets every command rule, and adding one later changes no magus code, because the rules and the verdict already exist and only the wrapper is host-shaped.
Project references
A project reference is a workspace-relative path, written bare: pkg/api. That
is the one spelling to read, to write, and to quote back. What a project arg
prints is what a project arg takes, so a path out of any magus command pastes
straight back into the next one.
| form | means |
|---|---|
pkg/api |
that project, measured from the workspace root, from anywhere |
./pkg/api, .. |
measured from the current directory |
. |
the project the current directory is in |
project:pkg/api |
the same project as a GRAPH NODE, for explain, query, path |
Three rules cover the whole surface:
- A bare path is absolute enough. It is measured from the workspace root, so
it means the same project from any directory. Only the dot forms depend on
where you are standing, and when you are standing outside the workspace
entirely,
--root <path>measures them from the workspace it names, so a command written once keeps working when it is run from somewhere else. - Never rewrite a path magus printed. Every surface prints the bare
workspace-relative form (
-o name,-o json, logs, error messages), so it is already in the form the next command wants. Commands that take fuzzy search tokens rather than paths, such asmagus where, take the same bare text. - Quoting a project back to a user: prefer whatever magus printed. The
workspace root is the case that bites, because it is the one project whose
path is a bare
.; human output renders it as the repository's directory name, so a.never leaks into a sentence where it reads as punctuation.
The project: prefix in the last row is not a second path syntax. It is the
kind-prefixed node grammar the graph commands use where kinds mix, alongside
target:, spell:, and doc:; magus explain project:pkg/api disambiguates a
name that could be either. Commands that only ever take a project (run,
affected, ls, describe project) take the bare path.
A workspace://pkg/api reference still parses and resolves to the same
project, with a deprecation warning. It bought no reading the bare path did
not already have, and it is not what magus teaches or prints any more. Drop
the scheme wherever you find one written down.
Incremental review
Answer "I reviewed earlier, what changed since, and what do I need to look at now" without re-reading the whole workspace:
- At review time, record where you stopped:
magus vcs checkpoint -o nameprints the revision, or<revision>+<digest>when the tree was dirty: the digest says which dirty tree was reviewed, since the revision alone reads the same for every dirty tree built on it. - Later, pipe the delta through the annotated view instead of reading a raw
diff:
git diff <revision> | magus diff -reports each changed file's reach, public-surface exposure, and referents: the surrounding code worth a second look, not just the literal hunks.magus diffrefuses a git ref given positionally, on purpose (a swallowed ref once printed the reader's own edits as the answer), so the pipe form above is the only sanctioned spelling, and the refusal message says so. - Reviewing through a diff session carries this further: per-hunk viewed marks key off content digest, not position, so a hunk that has not changed stays marked reviewed and one that has resurfaces on its own.
Handing work to other agents
The same checkpoint identifies a piece of work handed to another agent, and it is one leg of a wider surface: a declared set of jobs, a console Jobs view that draws them, and a spawn recorded but never judged. magus records what an orchestrating agent says it intends and enforces none of it: ownership is settled by diffing against the checkpoint each job was handed. Jobs and leases covers that loop.
MCP
An MCP-connected host gets a curated agent-facing adapter to Magus operations,
not every CLI command. magus describe mcp-tools lists all of them with
parameters. See
MCP for transport and token setup, and
Knowledge graph for the graph the query tools
read.
Skills prefer the MCP tools and fall back to the CLI, so they work in both
connected and disconnected sessions. MCP availability is a host/integration
concern: configure it once for the host that owns it. An agent tries an exposed
MCP tool directly, then uses the CLI fallback if the tool is missing or fails.
magus status --probe=mcp checks the loopback HTTP listener, so it cannot judge
stdio or Unix-socket MCP. An agent must not manually start Magus merely to obtain
MCP.
No tool carries state across sessions: magus remembers nothing an agent told it,
and the agent's own harness owns what persists between sessions. Captured build
output is addressed by output references,
and the workspace's human-authored notes are read through magus notes and the
graph.
The CLI works without it
The CLI still reads the workspace, runs targets, uses the cache, and answers graph queries with no server running. What it lacks is MCP tool discovery, the warm graph and background indexes, and structured output retrieval.
An agent must not turn that into a blocker. If an MCP tool is missing or its
call fails, use the CLI fallback. For server-socket diagnosis,
magus status --probe=readiness checks whether this workspace is loaded on the
socket; it does not prove the host registered MCP. Restoring or changing host
MCP wiring is a user-owned integration action, not ordinary agent work.
Why a server, not a wrapper
If an agent can already run magus query in a shell, what does an MCP server
add? If the server only ran the CLI and handed back its stdout, the answer would
be nothing, plus a round trip. The difference is the context the agent has to
work with.
An agent working through a shell falls back on the habits it learned everywhere else: grep and cat over the files. Those return text matches. They do not return the project DAG, the declared outputs, the affected set, or the blast radius of a symbol, because none of that is written in the files; it lives in the graph the server keeps warm. So the agent reasons one layer below the structure it is trying to understand, and fills the gap by guessing: this file looks generated, these two packages probably change together. Those guesses are frequently wrong, and the agent has no way to check them.
The tools answer from what the workspace declares. Ask client (magus\describe.file)
about a path and it does not read the filename and infer; it checks the
project's own globs and reports role: output with the note "generated: never
hand-edit, regenerate." In one case an agent spent close to an hour working out
whether a committed gen/ file was safe to edit, running generate repeatedly,
planting sentinel writes, diffing timestamps, when one call to that tool would
have answered it in a line.
So the server adds three things a shell-out leaves on the floor. Discovery: the
tools arrive in the model's context with their descriptions and parameters, so
the agent knows they exist without reading --help first. Shape: results come
back structured and sized for a model, rather than a human-formatted table
wrapped in color codes and pagination it has to scrape and pay for by the token.
Ground truth: the server reports what the workspace declares, which the agent
can rely on, rather than what a text pattern happened to match, which it cannot.
None of this comes from the protocol. A server that only shelled out would be a wrapper whether or not it spoke MCP. MCP is how the graph reaches the model; the value is in the graph and the curation.