magus-cli
v7.9.1
Published
TUI tool for managing Claude Code plugins, MCPs, and configuration
Maintainers
Readme
magus
Manage a Claude Code setup — plugins, MCP servers, skills, CLI tools and the binaries plugins depend on — from a TUI, a CLI, or a committed team manifest.
Install
bun add -g magus-cli # recommended
npm install -g magus-cli # also worksUpdates go through magus itself, not the package manager:
magus upgradeupgrade advances magus. update advances your dependencies — the
plugins, CLI tools and skills your profile declares. Same split as every package
manager, so the two are never confused.
You get a self-contained binary for your platform (an optionalDependencies
package gated on os/cpu). On a platform without a prebuilt binary the
launcher falls back to running from source under Bun, so the install still works.
Two ways to use it
Interactive — run magus with no arguments for the TUI. Good for
exploring what is available and toggling things on one machine.
Declarative — commit a .claude/profiles.json and run magus install.
Good for making a teammate's machine match yours. This is the
team configuration guide.
Commands
magus Open the interactive TUI
install [profile] Install a profile's plugins, binaries, skills and
env at the manifest's PINNED versions, then activate
it. No argument = every profile.
--check Report drift only, write nothing (CI gate)
--yes, -y Skip the confirmation prompt
update [profile] Bring the ACTIVE profile up to date: install what it
declares but this machine lacks, and advance every
"latest" pin to the newest published version.
--check Report only, write nothing (exits 1 if anything is
missing or known to be behind)
--yes, -y Skip the confirmation prompt
profile list Show every profile; ● marks the active one
profile show <name> Print a profile's fully resolved closure
profile switch <name> Make <name> active: render its settings.json,
.mcp.json and models.json, install its skills
profile init Write .claude/profiles.json from this project's setup
doctor [--fix] Check binary deps, the active profile, conventions
claude [args...] Check for plugin updates (1h cache), then run claude
--force, -f Force the update check
upgrade Update magus itself
--version, -v Version + update check
--help, -h This list
--theme light|dark Force the palette. Otherwise: MAGUS_THEME, then
TERM_THEME, then a 250 ms terminal query, then
COLORFGBG, then dark. Only the exact words
"light"/"dark" count.Light or dark
magus decides once at startup, first answer wins: --theme, then the
MAGUS_THEME variable, then TERM_THEME, then a bounded (250 ms) wait for
the terminal's OSC 10/11 reply — only when stdin and stdout are both TTYs — then
COLORFGBG, then dark. A word that is not exactly light or dark (so auto,
Light, an empty value) is no opinion and the next step runs; when the flag is
given a bad word, that is an error naming both accepted values.
TERM_THEME and MAGUS_THEME are read from the process environment, never
from a project's .env. Running from source needs --no-env-file, because Bun
otherwise autoloads the cwd's .env before any code runs; the package scripts
and the bin/magus.js source fallback already pass it, and the compiled
binary never autoloads. The OSC 10/11 query itself is emitted by OpenTUI
(0.1.107) when the renderer is created, regardless of the outcome; when an
earlier step answered, magus never awaits or uses the reply, so first paint
is not delayed and the terminal's answer has no influence.
install vs update
They differ in one thing: whether the pins are allowed to move.
| | install | update |
|---|---|---|
| Versions | exactly what the manifest pins | advances every "latest" pin |
| Scope | every profile (or the one named) | the active profile |
| Also does | materializes + activates the profile | nothing to the manifest |
Both install whatever the profile declares and this machine lacks, so update
covers the "I ticked it but never installed it" case on its own.
This is dependency management, not a package manager. There is no resolver, no lockfile and no version negotiation. The job is: everything the profile declares is installed, and current.
That matters for exact pins. claude plugin install takes no version argument —
it installs whatever the marketplace publishes now — so a pin naming anything
else cannot be satisfied. update plans against the version it can actually
get, notes the shortfall, and moves on:
dev@magus = 9.9.9 (manifest pins 4.6.1; the marketplace offers only 9.9.9)It records what actually landed, never the pin, and it does not fail over
the difference — an unsatisfiable pin is a standing property of the manifest,
not something a given run did wrong. Pin drift is gated where it always was:
magus install --check with strictVersions in the manifest.
update only ever moves forward. Nothing here can downgrade — the installer
takes no version — so an install that is already ahead of what the marketplace
publishes is left alone rather than dragged back. A plan that tried would emit a
step that runs, reports success, changes nothing, and reappears on every
subsequent run.
--check fails on things that are missing or known to be behind. It
deliberately does not fail on the two items that get re-fetched every run
regardless: an already-installed skill (a manifest skill ref carries no content
hash, so there is nothing to compare) and a present unpinned binary (no version
probe exists, only present/absent). A gate that can never pass gets switched
off, so it only fires on real drift.
Neither command needs a manifest to exist first. In a repo without
.claude/profiles.json, both offer to write one describing the project as it
stands — that is magus profile init, which you can also run on its own.
Adoption reads the project's own .claude/settings.json; if that enables
nothing it falls back to your user-scope settings and says so, because the file
is meant to be committed and one machine's global set is not a team contract.
Machine-only keys are stripped, and any settings value naming a path under your
home directory is flagged for review.
--yes alone will not create a manifest. It means "don't ask me about the
command I ran", and someone running magus update --yes in CI did not ask to
author their team's committed contract. Creating it needs a human at the prompt,
or magus profile init.
magus doctor exits non-zero when it finds a problem, so it works as a CI
check. So does magus install --check.
What the TUI covers
| Screen | What it manages |
|---|---|
| Plugins | Install, enable, disable; shows version changes and newly installed plugins |
| Skills | Browse and install skills from configured skill repos |
| MCP | Add MCP servers from a curated catalog |
| Settings | Claude Code settings, from a catalog of known keys |
| Profiles | Saved plugin sets (the TUI's own, older profile concept) |
| CLI Tools | Install and update CLI tools via the right package manager; a tool a plugin installs from a release shows as managed by that plugin |
| Git State | Gitignore conventions and repo hygiene |
| Alias | Shell alias + flag management for launching claude |
| Styles | Communication style presets, composed into one native output style |
Navigate with ↑/↓ or j/k, Enter to select, r to refresh, ? for help,
q/Escape to go back. Number keys 1–9 jump straight to a screen.
Styles
Claude Code activates exactly one output style at a time. The Styles screen (9)
composes several rule blocks into that one style, so choices that would otherwise
compete for the single slot end up in one file.
One verbosity axis — pick exactly one of direct, explanatory, or terse —
plus any number of free modifiers:
| Preset | Effect |
|---|---|
| direct | Lead with the answer, minimal preamble |
| explanatory | Teach as you go, more context per step |
| terse | Shortest useful output |
| no-slop | Bans filler, hype, and AI vocabulary |
| asd-ste100 | ASD-STE100 Simplified Technical English: sentences that survive one read |
| evidence-first | Claims must carry a citation, command output, or file reference |
| calibrated | State confidence honestly; no false certainty |
| plain-language | Prefer plain words over jargon |
| structured | Headings, tables, and lists over long paragraphs |
| terminology | Enforce project-specific vocabulary, filled in from your codebase |
Two verbosity presets is the one combination that does not work: terse says "do
not explain unless asked" and explanatory says "explain the specific choice", so
together they cancel and the model resolves the contradiction differently every
turn. Everything else stacks.
The presets live in src/data/styles/*.md and are compiled into the binary, so
they are always the version this magus was built from.
What gets written
.claude/output-styles/composed.md the composed rules (generated — do not hand-edit)
.claude/settings.json "outputStyle": "composed"
.claude/style.json the declaration — commit this to share itWith a profile active the file is named composed-<profile> and the choice is
also recorded in .claude/profiles.json, because magus install rebuilds
settings.json from the manifest and would otherwise drop it.
style.json is the reviewable statement of what the project wants; composed.md
is the artifact Claude Code reads. A teammate who clones the repo gets the
declaration, and magus tells them when their local artifact does not match it.
Styles you already have
Output styles you wrote yourself compose too — magus finds them in
~/.claude/output-styles/ and .claude/output-styles/ and lists them alongside
the presets. Community styles can be fetched from their source repository on an
explicit action, and are cached outside ~/.claude/output-styles/ so a re-fetch
never overwrites something you wrote.
Built-in styles ship inside the Claude Code binary rather than on disk, so there
is no file to import until you capture one. --discover names the set the
installed version actually has — it was Concise, Explanatory, Learning and
Proactive on Claude Code 2.1.239, and Anthropic adds to it:
bun scripts/capture-builtin.ts --discover # what Anthropic ships today
bun scripts/capture-builtin.ts --all # capture every built-in
bun scripts/capture-builtin.ts --check # what fell behind after an upgradeEach capture runs one real claude -p round trip behind a transparent proxy and
records the system prompt Claude Code actually sent, writing
~/.claude/output-styles/builtin-<name>.md. Captures are per machine and stamped
with the Claude Code version they came from — re-run --check after every
upgrade. Nothing is committed to this repo: the text is Anthropic's, and it
changes on their release schedule, not ours.
Coding rules stay on
Every generated style sets keep-coding-instructions: true. Without that flag
Claude Code drops its own coding-discipline rules from the system prompt, and a
setting about how to communicate has no business switching off how code gets
written. force-for-plugin is never set either — that would override your own
/output-style choice.
Files magus touches
| Path | Owner | What magus does |
|---|---|---|
| .claude/profiles.json | you, committed | the one source of truth; records every change to the live config |
| .claude/profiles/active.json | magus, gitignored | which profile is active, and what magus last wrote |
| .claude/settings.json | magus + Claude Code, gitignored | in profile mode, rendered from the active profile; Claude Code's edits are recorded back into it |
| .claude/models.json | magus, gitignored | in profile mode, rendered from the active profile's routing |
| .claude/skills/<name>/ | magus or you | a skill the active profile lists is installed and gitignored by magus; any other folder is yours and is never touched |
| .claude/settings.local.json | you, gitignored | env values collected during install |
| .claude/style.json | you, committed | the project's declared style — written on apply |
| .claude/output-styles/composed*.md | magus, committed | the generated style Claude Code reads |
| .mcp.json | magus + Claude Code, gitignored | in profile mode, rendered from the active profile; Claude Code's edits are recorded back into it |
| ~/.claude/plugins/* | Claude Code | never written directly — always via the claude CLI |
magus never hand-edits Claude Code's own registries
(installed_plugins.json, known_marketplaces.json, the plugin cache). Those go
through claude plugin ... so Claude Code stays the single writer.
Development
bun install
bun test # full suite
bun run typecheck
bun run lint
bun run src/main.tsx # run from source
bun run build:binaries # cross-compile all platform binaries + their packagesTests run per-file isolated in CI (bun run test:ci) because mock.module
leaks across files in a shared process.
Do not bump @opentui past 0.1.x. 0.4.x breaks bun build --compile: it
resolves tree-sitter workers eagerly, bypassing the OTUI_ASSET_ROOT override,
so a bare import "@opentui/core" crashes at import time in the compiled
binary. Re-run the compile spike before changing that pin.
Releasing
Tag-driven. .github/workflows/magus-release.yml fires on tools/magus/v*:
- verify — install, typecheck,
test:ci, version and optional-deps gates - binaries — a matrix building each target on its native runner
(
bun --compilecannot cross-compile in CI because opentui ships a per-platform native library) - publish — platform packages first, main package last, so its
optionalDependenciesalready resolve
# bump "version" AND the three optionalDependencies to match
bun run check:optional-deps
git commit -am "feat(magus): vX.Y.Z - description"
git tag -a tools/magus/vX.Y.Z -m "Release message"
git push origin main --tagsAuth is npm OIDC trusted publishing — the workflow references no secrets. Adding
a new platform target needs a one-time manual publish to create the package
name (OIDC can only authenticate against a package that already exists); see
docs/npm-trusted-publishing-setup.md.
License
MIT
