rhiza-claude
Health Warn
- License — License: NOASSERTION
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 6 GitHub stars
Code Fail
- rm -rf — Recursive force deletion command in .github/workflows/ci.yml
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
Claude Code plugin marketplace for the rhiza plugin — slash commands for working in rhiza-managed repos.
rhiza-claude
A Claude Code plugin marketplace providing therhiza plugin — slash commands for working in rhiza-managed repos (template
sync, code-quality scoring, and README/doc upkeep).
Not only for Claude Code, and not only for rhiza-managed repos. The same commands
ship as a portable SKILL.md bundle
for other agents, and several of them work in any git repo, or with no repo at all. See
Outside a rhiza-managed repo.
📖 Documentation: https://jebel-quant.github.io/rhiza-claude/ — a dedicated
page for every command. Build it locally with make book.
What this is for
Ten repos need the same CI, Makefile, gates and docs build — and copied scaffolding
drifts. rhiza is that scaffolding kept once,
in a template repository. rhiza-claude is how a repo adopts it: sync from a
pinned template release, then get scored on the result.
Two repos, one boundary. The template owns CI, the Makefile and the docs base;
your repo owns source, tests, pyproject.toml and README prose. A sync
writes only template-owned paths — the ones the last sync recorded — so your code
can't be swept in. There is no blanket git add --all in the flow.
"rhiza-managed" is two files, answering different questions: .rhiza/template.yml
is a pointer (which template, which pinned ref — what we'd sync from), and.rhiza/template.lock is a record (repo, ref, SHA, timestamp, strategy, every managed
file — what actually arrived). They can disagree, which is why /rhiza:status reports
both and /rhiza:quality checks for both before scoring: the pointer says the repo
is managed, the record says a sync actually happened, and its gates are the ones that
sync delivers. With no record it drops to a degraded mode — template gates skipped,
your own gates run, design scored in full, and the report says so. It probes the
record rather than any file the sync wrote, because which files those are is a
property of the template version: /quality used to look for .rhiza/rhiza.mk, which
template v1.4 stopped shipping, and every fully synced v1.4 repo was told it had never
been synced. /rhiza:release needs neither.
The order still matters — /rhiza:init → merge → /rhiza:update → merge →/rhiza:quality. /init writes one file of its own and syncs nothing; /update
performs the first sync; /quality needs that content for its full assessment. So
"/init ran and no CI appeared" is the expected result of step one, and a /quality
run before the sync gives you the narrower score rather than nothing.
Two kinds of markdown, and the difference is enforced. skills/ holds
the twelve slash commands you invoke; prompts/ holds nine internal procedures theyRead — kept outside both so they can't be invoked directly. The procedures are where
shared behaviour lives, which is why /init and /update behave identically where they
overlap.
Why prose commands at all: deterministic work belongs in tested code, judgement
belongs in markdown. The bundled stdlib-only Python does what has one right answer
(parsing the lock, merging synced files, comparing versions); the markdown does what
needs a reading of your repo. CI gates the prose too, so a command naming a script or
flag that no longer exists fails the build instead of failing mid-task in front of you.
👉 New here? The documentation site
has the full introduction and a worked first run — empty directory to synced, scored
repo.
Install
/plugin marketplace add Jebel-Quant/rhiza-claude
/plugin install rhiza@rhiza-claude
Or, from a shell, make install runs the equivalent claude CLI commands:
make install
The commands then appear namespaced under the plugin: /rhiza:init,/rhiza:update, /rhiza:quality, /rhiza:docs. Type /rhiza
to have Claude Code autocomplete them.
Install a specific version
By default the marketplace tracks this repo's default branch, so /plugin install pulls the latest release. To pin to a specific published version,
append that version's git tag as a #<ref> suffix when you add the marketplace
(see the releases page
for available tags):
/plugin marketplace add Jebel-Quant/rhiza-claude#v1.0.0
/plugin install rhiza@rhiza-claude
The same #<ref> suffix works from a shell:
claude plugin marketplace add Jebel-Quant/rhiza-claude#v1.0.0
claude plugin install rhiza@rhiza-claude
Pinning happens at the marketplace layer, not per plugin — once the marketplace
is added, /plugin install uses whatever ref it points at. To switch versions,
remove the marketplace and re-add it at the tag you want, which works in either
direction — an older release as readily as a newer one:
/plugin marketplace remove rhiza-claude
/plugin marketplace add Jebel-Quant/rhiza-claude#<tag>
<tag> is any published tag from the releases
page. It is a placeholder
here on purpose: the two examples above pin the current release and are rewritten
by bump-my-version on every release, but this one has to name a different tag
to show that switching goes both ways — so a literal version here would be one no
release could keep current, and it silently aged three minor versions before
anyone noticed.
If the install fails with Invalid schema
/plugin install validates every marketplace you have configured before it
installs anything, so a manifest this repo has nothing to do with can stop the
rhiza install:
❯ /plugin install rhiza@rhiza-claude
⎿ Invalid schema: plugins.0.source: Invalid input, …
plugins.76: Unrecognized key: "displayName", … plugins.290.source: Invalid input
The indices name the culprit. This marketplace ships a single plugin, so there is noplugins.290 here to be invalid — that entry belongs to another configured
marketplace, in practice Anthropic's own claude-plugins-official, which is a few
hundred entries long. Its entries use source object forms (git-subdir, url,archive) and a displayName key that older Claude Code releases have no schema
for, so an out-of-date client rejects that manifest and the rhiza install is
collateral damage. /plugin marketplace add Jebel-Quant/rhiza-claude succeeding
while the install fails is the tell: the add validated this manifest and was happy
with it.
The fix is to update Claude Code, not to change anything here:
claude update
If you can't update, dropping the marketplace that fails to validate also unblocks
the install:
/plugin marketplace list
/plugin marketplace remove claude-plugins-official
Prerequisites
The commands drive a rhiza-managed repo with uv —
install it once:
curl -LsSf https://astral.sh/uv/install.sh | sh
You don't have to do this ahead of time, though: both /rhiza:init and/rhiza:update check for uv as their first step and offer to install it (with your
approval) on any platform. git and make are also used but are near-universal, and
the plugin's own bundled scripts are stdlib-only Python — no rhiza CLI required.
That last point cuts further than it looks: the deterministic half runs without Claude
Code at all. Every command's scripted work — the template sync, the status read, the
bootstrap, detaching — is a stdlib-only script with its own CLI, so it can be driven from
a shell script or a CI job with no LLM in the loop. What you give up is the judgement the
markdown supplies; what you get is the same code path the commands drive.
Without Claude Code is the map —
the command-to-script table, the sync step by step, and every exit code it can hand you.
And the judgement half runs under other agents. bundle/ holds the same twelve
commands and nine procedures in the open SKILL.md format, generated from plugin/ with
the Claude-specific bindings rewritten or restated — point your client at bundle/skills
and export RHIZA_ROOT.
With another agent covers what
the translation changes and what it cannot carry across.
Outside a rhiza-managed repo
You don't have to adopt the template to get use out of the plugin. Only the commands that
read or write .rhiza/ need a managed repo. The rest work in any git repo, and two of
them don't need a repo at all.
| Needs | Commands |
|---|---|
| Nothing: any directory, no git | /rhiza:maffay, /rhiza:completions (machine-level; completes make in any project) |
| Any git repo | /rhiza:docs, /rhiza:release, /rhiza:remote, /rhiza:fix, /rhiza:history |
| Any git repo, reduced scope | /rhiza:quality (runs in degraded mode) |
| A folder you want to make managed | /rhiza:init |
| A rhiza-managed repo | /rhiza:update, /rhiza:status, /rhiza:detach |
Here is what each one depends on instead of .rhiza/:
/rhiza:releaseneeds a repo that declares its version locations in[tool.bumpversion]. It deliberately doesn't check for.rhiza/./rhiza:remote,/rhiza:fixand/rhiza:historyneed a GitHub or GitLab origin and the
matchinggh/glabCLI. Every repo has pull requests, CI and issues, managed or not.
In a managed repo, the first two also recognise template-owned paths and send those fixes
upstream instead of patching them locally./rhiza:docsdetects the language and reads its manifest (pyproject.toml,Cargo.tomlorgo.mod). If.rhiza/is present it also uses the template ref and
the managed-file split. Without it, it simply leaves them out./rhiza:qualitywithout a synced template skips the template-delivered gates,
runs the targets your ownMakefiledocuments, and scores the design in full. The
report says it ran in degraded mode, and its score can't be compared with a managed
repo's.
Commands
/rhiza:init— make the current folder rhiza-managed. It writes one file
itself —.rhiza/template.yml, the pointer at a template repo and pinned ref — and
delegates the rest to the internal procedures (see Internals):
install-uv, then skeleton, then license. It detects platform/owner/name from an
existingorigin(or asks), picks the language (Python, Go or Rust — the three are
not equally supported; see Language support) and template
repo (jebel-quant/rhizafor all three — the template is multi-language, layering
apython-core/rust-core/go-corebundle on a neutralcore— or a fork) plus
its latest release as the initial pin —
checking that ref actually defines the profile it's about to name, since an
unsatisfiable pointer merges cleanly and then fails the first/rhiza:update— and
opens a PR on arhiza_init_<date>branch — never pushing to the
default branch (a brand-new repo's base branch is created by you). It runs no
sync and no gates: the template content (CI,Makefile, docs base)
arrives when you run/rhiza:updateafter the PR merges — a second PR./rhiza:update— sync the repo to the latest (or a given) template release:
bump therefin.rhiza/template.yml, run the bundled sync, resolve conflicts by
taking the upstream side, and open a PR containing only template-owned files —
the paths.rhiza/template.lockrecords, never a blanketgit add --all, so your
own source is never swept in. The template repo is read fromtemplate.yml, so a
fork works too. Runs no gates and files no issues: use/rhiza:qualityfor a scorecard./rhiza:quality— run the rhiza code-quality gate (lint, types, docs,
deps, security, tests, complexity, architecture) and score the repo. It also checks
the documentation for truth rather than presence: the>>>examples in your
docstrings, and every fenced block inREADME.md— shell parsed withbash -n,
Python withcompile(), and everypyconfence doctested as one>>>transcript
with its expected output inline.interrogatecan only tell you a docstring exists;
this is what tells you it is still right. Shell fences are never executed, and executing anything
at all is opt-in — a module it cannot import is reported unmeasured, never failed./rhiza:docs— create or refresh the repo's three top-of-repo documentation
files:README.md(with the standard badge set),CLAUDE.md, andmkdocs.yml.
Detects platform, owner/repo and project metadata at runtime, preserves hand-written
prose, and keeps the README'smake helptarget list in lockstep with the realMakefile. Badges are generated with omit, don't fake — a badge whose backing
fact isn't detected is never emitted. Writes files only; no commit, no PR./rhiza:release— release through a pull request: table up the legal next
versions and let you pick (it never suggests or defaults to one), guard that the
choice strictly increases past every prior release, then letbump-my-versionwrite it
into every location the repo declares in[tool.bumpversion]—pyproject.toml, plugin
manifests, self-referencing CI stub pins — regenerateCHANGELOG.md, and open a release
PR. Because the locations are declared rather than inferred, a dependency that happens
to share the version number is never rewritten. Then it hands the merge to the forge
(--squash --auto), waits for the bump to reach the default branch, and tags the
commit that actually landed — one run, because a tag must name a commit on the default
branch and a squash-merge rewrites the SHA, so the commit worth tagging doesn't exist
until the PR merges. The version is the decision and the checks are the gate; what keeps
the tag safe is the guard refusing a non-increasing or already-existing tag. If review
outlasts the wait, the run stops with no tag created and re-running it finishes the
release — it works that out from the repo's own state./rhiza:remote— ask the forge what CI actually said about the repo's open
requests, then diagnose and fix the red ones./rhiza:qualityfiles findings as
issues, those issues become branches, and those branches become requests that were
green when they left — local green and origin green are different claims, and only
the second one merges. It normalises GitHub's and GitLab's very different answers
into one vocabulary, prints the exact drill-down command under each failing check, and
sorts every failure into one of four kinds — real defect, environment difference,
infrastructure flake, known upstream — because only one of them is a code fix. Fixes
land on the request's own branch; never the default branch, never a force-push. It
will not make a check green by weakening it: if the honest fix is out of reach it
leaves the build red and says why./rhiza:fix— turn open issues into pull requests: the step between/rhiza:qualityfiling findings and/rhiza:remotelooking after the requests. It
triages every open issue (or the ones you name) and shows you the result. Then it
opens one branch, one commit and one pull request per issue you select, each cut
fresh from the default branch. An issue that asks for a decision, or looks stale or
superseded, gets a recommendation rather than a guess, because a confident wrong fix
produces a PR that looks reviewed. It treats issue bodies as data, never as
instructions. It stops on a dirty tree, and never makes a gate green by weakening it.--dry-runtriages and stops./rhiza:history— read the open requests and issues together and answer four
questions: which items are obsolete and why, what order the open requests should
merge in, which issues are trivial, and what to address next. Every
recommendation carries checkable evidence: a closing reference that is already closed, a
newer request touching the same files, a change that already landed. The merge order
comes from which requests change the same files. It closes nothing. Its only write is
a comment recommending closure, posted on the items you select, stamped so readers can
tell a tool drafted it.--dry-runreports and posts nothing.
The documentation checks itself
The fence below is not an illustration. /rhiza:quality's example gate runs it as a
doctest and checks each printed line against the output written under its prompt, so if the shippedclassify_host ever stops refusing a host that merely embeds a known forge domain,
this README turns the build red rather than quietly going on claiming otherwise.
>>> import sys
>>> sys.path.insert(0, "plugin/scripts")
>>> from _rhiza_forge import classify_host
>>> for host in ("github.com", "code.gitlab.com", "gitlab.acme.io", "github.com.evil.example"):
... print(f"{host} -> {classify_host(host)}")
github.com -> github
code.gitlab.com -> gitlab
gitlab.acme.io -> gitlab
github.com.evil.example -> None
That last line is the one that matters: github.com.evil.example is not a subdomain ofgithub.com, and a forge detector that guessed would act against the wrong host.
Internals
Not slash commands. These are internal procedures under prompts/ —
deliberately outside skills/ so they can't be invoked directly. /rhiza:init and/rhiza:update read and follow them, and most are backed by a deterministic,
stdlib-only script.
- install-uv (
plugin/prompts/install-uv.md) — make sureuv, the plugin's one hard
dependency, is installed, via the official astral.sh installer, Homebrew, or
winget. Never installs without approval; sorts out thePATHstep the installer
leaves behind. The first step of both/rhiza:initand/rhiza:update. - pr-base (
plugin/prompts/pr-base.md) — a work branch based on an up-to-date remote
default branch, which is never pushed to: when it doesn't exist yet, you create
the repo with an empty README rather than the command pushing one. Shared by/rhiza:initand/rhiza:update, so both behave identically. - skeleton (
plugin/prompts/skeleton.md→plugin/scripts/init_skeleton.py) —uv init --lib
when there's nopyproject.toml, then the shape the template's gates require: a
package docstring in place of uv'shello()placeholder, the description, and the[project.urls]and[dependency-groups]entries the synced pyproject gate
asserts. Idempotent and additive. The template never ships apyproject.toml, so
the quality gates need this to have run. - license (
plugin/prompts/license.md→plugin/scripts/set_license.py) — the SPDXlicense/license-filesmetadata inpyproject.tomlplus theLICENSEfile's
full text (MIT, Apache-2.0, BSD-3-Clause bundled). Overwriting an existingLICENSEneeds--force;noneclears the metadata. Never writes a deprecatedLicense ::trove classifier. - design-analysis (
plugin/prompts/design-analysis.md) — the complexity and architecture
evidence nomakegate measures: radon CC/MI, module sizes, the import graph, layering
direction, and cycles including ones hidden behind function-local imports. Gathers
evidence; doesn't judge. - scorecard (
plugin/prompts/scorecard.md) — the 1–10 rubric: the subcategory list, the
coverage bar, the findings format, the issue menu, and the scoping rule that stops
a managed repo being marked down for its own template. - python-version (
plugin/prompts/python-version.md→plugin/scripts/set_python_version.py)
— pinrequires-pythonand rewrite theProgramming Language :: Python :: X.Y
classifiers to the supported range (3.11–3.14; never a bare:: 3), and sync.python-version.
Repo utilities
Thin, read-only, stdlib-only commands backed by bundled scripts — they work
without the rhiza CLI installed, reading .rhiza/template.lock /.rhiza/template.yml directly where they need a repo at all. None of them writes
anything.
/rhiza:status— report both halves of the repo's rhiza state: whether.rhiza/template.ymlis valid (what you'd sync from), and what.rhiza/template.lockrecords as actually synced (repository, ref, SHA, timestamp,
strategy). They can disagree in both directions, so reporting one alone misleads.
Add--files(alias--tree) to list the managed files as a directory tree, or--checkto compare the pinned ref against the latest upstream release.
Read-only./rhiza:maffay— return a bonmot from a random Peter Maffay song, for the middle
of a long refactor. Takes an optional theme keyword (mut,sommer,nessaja, …) or
part of a song title. The draw lives inplugin/scripts/maffay.pybecause a model asked for
a random song reaches for the same two hits every time. Quotes the title line
only, attributed — no lyric bodies — and theFür uns:gloss beside it is ours, not
Maffay's. Needs no repo, no git and no network.
Machine setup
/rhiza:completions— install make tab-completion for bash, zsh or both, somake <TAB>lists a project's targets. The one command that writes outside a repo:
the destinations are under${XDG_DATA_HOME:-$HOME/.local/share}, which is also why it
isn't model-invocable — you have to name it. The completion is generic (it parses the
make database in the current directory), so install it once per machine, not once per
repo; this is the plugin taking over a job the template used to do by syncing four
identical files into every managed repo. The installed names are the shells' genericmake/_make, so a completion this plugin didn't write is reported and left alone
rather than overwritten —--forceis opt-in.
Destructive
/rhiza:detach— detach the repo from rhiza: delete every rhiza-managed file
listed in.rhiza/template.lock, prune the emptied directories, and remove the lock.
This is the only command that removes files wholesale; it prompts for confirmation
unless--forceis passed. It detaches a repo, not the plugin — the inverse of the
sync, not of the installation, so uninstalling the plugin via/pluginis a different
thing entirely and leaves every synced file in place.
Language support
/rhiza:init offers Python, Go and Rust, and all three point at the same multi-language
template. They are not equally supported, and it is worth knowing which you're
signing up for before the four-step bootstrap rather than after.
| Python | Rust | Go | |
|---|---|---|---|
/init, /update, /status, /release, /detach |
✅ | ✅ | ✅ |
| Local toolchain from the template | ✅ | ✅ cargo, clippy, nextest, llvm-cov, cargo-deny | ✅ go test, golangci-lint, govulncheck, revive |
| Hosted CI workflows | ✅ | ❌ none yet | ❌ none yet |
/quality gate list |
✅ known and named | ⚠️ discovered at runtime | ⚠️ discovered at runtime |
| Test-layout parity subcategory | ✅ | n/a | n/a |
| Executable-documentation gate | ✅ docstrings + README | ⚠️ README fences only | ⚠️ README fences only |
Python is the fully-supported axis. /rhiza:quality's gate list is the Python
profile — the one the plugin has actually run against. On a Rust or Go repo it probes
the Makefile with check_make_targets.py, scores the targets it discovers, and marks
the language-specific subcategories out-of-scope. That is deliberate: a hand-written
table of targets for templates this plugin has never run against would be prose
asserting things it can't back. But it does mean a Rust or Go scorecard rests on a
narrower base than a Python one, and /quality now says so in its own output.
There is deliberately no rust-github-project or go-github-project profile — those
are almost entirely CI workflows, and rhiza's github/gitlab bundles still ship
Python ones. Add hosted CI yourself until those land.
Layout
The shipped plugin lives in plugin/; everything that builds or checks it stays at
the repository root. .claude-plugin/marketplace.json points at it with"source": "./plugin", which is the documented way to keep a plugin in a subdirectory
of its marketplace repo.
Two of the directories inside are the spec's and two are this repo's.skills/ and hooks/ are discovery locations — Claude Code finds
components by those names at the plugin root, so they cannot be renamed. prompts/ andscripts/ are local conventions the spec has never heard of; prompts/ exists precisely
because it is not a discovery location, so a procedure kept there cannot be invoked as a
slash command.
All twelve commands are skills: plugin/skills/<name>/SKILL.md, where the directory
names the command, so skills/init/SKILL.md is what answers /rhiza:init. Check the
plugin docs rather than this table before
assuming what the spec requires.
| Path | Purpose |
|---|---|
.claude-plugin/marketplace.json |
Marketplace manifest listing the rhiza plugin. Stays at the repo root — that's where /plugin marketplace add looks. |
plugin/ |
The plugin as shipped. Everything below is inside it. |
plugin/.claude-plugin/plugin.json |
The rhiza plugin manifest. |
plugin/skills/ |
The plugin's twelve slash commands (<name>/SKILL.md, the directory naming the command). |
plugin/prompts/ |
Internal procedures the commands Read — deliberately not commands, so users can't invoke them. |
plugin/hooks/ |
hooks.json — a PreToolUse hook guarding Bash calls at runtime (compound make, force-push, push to the default branch). Fails open. |
plugin/scripts/ |
Bundled stdlib-only Python the commands and procedures drive, plus the non-Python assets they copy out (licenses/, completions/). |
tests/scripts/ |
Pytest suite mirroring plugin/scripts/ 1:1. Not shipped. |
docs/ |
The MkDocs site. Not shipped. |
paper/ |
A LaTeX introduction — the long form of the framing above, with figures captured from real command output (render_figures.py). make paper builds it; CI rebuilds it on every commit and publishes the PDF with the docs site. |
Inside a command, ${CLAUDE_PLUGIN_ROOT} resolves to plugin/, so"${CLAUDE_PLUGIN_ROOT}/scripts/sync.py" is unchanged by the move. Only the
source-checkout fallback paths gained the prefix: plugin/scripts/sync.py.
Licensing
rhiza-claude is source-available under the
PolyForm Noncommercial License 1.0.0, not an OSI open-source license.
- Free for noncommercial use: personal projects, study, hobby work, research, and
use by charities, educational institutions and government bodies. - Commercial use needs a commercial license. That includes running the plugin on
repositories you work on for a company. Write to
[email protected] to get one. - What it generates is yours. Files the plugin writes or copies into your
repository (skeletons, workflows,LICENSEfiles, configuration) are not covered
by these terms, whatever license your repository uses. - Earlier releases stay MIT. v0.17.4 and everything before it remain available
under the MIT License.
Contributing
Branch off main, make the change, and open a PR — CI runs the samemake fmt && make test you can run locally, so a green pair means a green PR.
CONTRIBUTING.md has the details: the prerequisites, the
checklist for adding or changing a command, and the commit conventions the
changelog is generated from.
Once a change lands on main, installed users pick it up the next time the
marketplace refreshes.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found