Teaches you to use AI coding agents well, instead of just using them.
It watches for the habits that quietly degrade output β vague prompts, unverified changes, a context window filling with dead ends β and explains what to do differently and why. Works with Claude Code, Codex, and OpenCode.
π¨π¨ tutor: you asked for a change without naming how it gets verified. Without a
signal (test output, exit code, screenshot) you are the only error detector,
reviewing code that merely looks right.
Most of it costs zero tokens: messages go to your terminal, never into the model's context. A tool that spent context lecturing you about context would be self-defeating.
- What it does
- Install
- What it checks
- Design rules
- Tool support
- Configuration
- Extending it
- Status and limitations
Three layers, in ascending order of cost and capability.
At session start it measures your setup against documented limits and says what is costing you context. Not just that a file is too long, but why that matters:
π¨π¨π¨ tutor: 184 agent definitions total roughly 17,250 tokens of frontmatter
(threshold 15,000). Descriptions load on every request, because the model
needs them to choose what to delegate. Move detail into each agent's system
prompt, which loads only when that agent runs.
Every prompt is checked before the model sees it, and the session transcript is checked alongside it. At most one nudge per turn, and usually none.
Severity is one emoji repeated: π¨π¨π¨ means acting now saves real time or money, π¨ means ignore it freely if you disagree.
/tutor runs a deeper pass: the audit, plus content-aware advice that cites specific
lines, plus a critique of how the session has gone.
skill 'graphify': 4 lines are written as sequential one-off steps (L132, L452, L573,
L580). A skill body stays in context for the rest of the session, so "now do step 3"
still sits there long after step 3 is done. Write standing instructions instead.
There is also an opt-in mode where the assistant judges each prompt as it answers. That one does cost context, so it is off by default. See Configuration.
claude plugin marketplace add neteye-platform/ai-tutor-plugin
claude plugin install tutor@wuerth-tutorThen /reload-plugins, or start a new session.
You will be asked to trust the workspace, because the plugin registers hooks. That is expected and correct: hooks are code that runs on your machine.
mkdir -p ~/.codex/tutor
cp -r scripts ~/.codex/tutor/
cp SKILL.md ~/.codex/tutor/ # the on-demand /tutor review
cp codex/hooks.json ~/.codex/hooks.json # merge by hand if you already have oneThen, inside Codex, run:
/hooks
This step is not optional and it is the one people miss. Codex will not execute any non-managed hook until you have explicitly approved it, so before you do, a fresh install looks broken: nothing happens and no error appears.
Three things worth knowing about Codex's trust model:
- Trust is per hook, not per workspace. Claude Code asks once for a folder; Codex tracks each hook separately.
- Editing a hook re-triggers the prompt. Pull an update that changes
coach.pyand everyone is asked again. That is the model working, not a bug. --dangerously-bypass-hook-trustshould stay unused. Its own help says it is "intended only for automation that already vets hook sources".
If your organisation sets allow_managed_hooks_only = true in requirements.toml,
Codex ignores all user and project hooks, and the tutor will not run unless deployed
through the managed config layer. That setting is only honoured in requirements.toml.
TOML equivalent, if you prefer config.toml to hooks.json
[[hooks.SessionStart]]
matcher = "startup"
[[hooks.SessionStart.hooks]]
type = "command"
command = "python3 ~/.codex/tutor/scripts/lint.py"
timeout = 10
statusMessage = "tutor: checking setup"
[[hooks.UserPromptSubmit]]
[[hooks.UserPromptSubmit.hooks]]
type = "command"
command = "python3 ~/.codex/tutor/scripts/coach.py"
timeout = 5mkdir -p ~/.config/opencode/plugins ~/.config/opencode/tutor
cp -r scripts ~/.config/opencode/tutor/
cp SKILL.md ~/.config/opencode/tutor/ # the on-demand /tutor review
cp opencode/tutor.js opencode/package.json ~/.config/opencode/plugins/The plugin resolves the scripts relative to its own location, so keep that layout or
edit the SCRIPTS constant at the top of tutor.js.
Both analysis scripts run standalone, with no dependencies beyond python3:
python3 scripts/lint.py # size and budget checks
python3 scripts/advise.py # content advice, with line numbersRun them from a project directory. They auto-detect which agents you use by looking for
~/.claude, ~/.codex, ~/.config/opencode, CLAUDE.md and AGENTS.md, and report
only on what they find.
Paths and command names adapt per tool, so a Codex user is never told to run
/doctor.
| Check | Threshold | Why it matters |
|---|---|---|
Context file length (CLAUDE.md, AGENTS.md) |
200 lines | Loads on every request, so every extra line is a recurring cost |
| Nested context files | 3 or more | They merge silently, so the total is invisible unless you audit each level |
SKILL.md length |
500 lines | Once invoked, the body stays in context for the rest of the session |
SKILL.md size |
~5,000 tokens | Above this it is truncated after compaction, keeping only the start |
| Agent frontmatter total | ~15,000 tokens | Descriptions always load, since they drive delegation choices |
| MCP server count | 5 or more | Tool-selection accuracy degrades past roughly 30β50 loaded tools |
MCP alwaysLoad |
any | Defeats deferred loading: right for small toolsets, wrong for broad ones |
advise.py adds content-level checks: lines that restate what the model already does,
rules that would be better as a hook than as prose, material readable from the repo
itself, emphasis markers spread so widely they no longer emphasise, skill steps written
as one-off instructions, and agent definitions that do not restrict tools:.
Fifteen nudges. Seven read the session transcript, so they react to what actually happened rather than to how a sentence was worded:
| Nudge | Trigger | Severity |
|---|---|---|
| Context pressure | 75% or more of the window used | π¨π¨π¨ |
| Compaction thrashing | Three or more automatic compactions in one session | π¨π¨π¨ |
| Repeated failures | Three consecutive failed tool calls | π¨π¨π¨ |
| Unverified edits | Files changed with nothing test-shaped run since | π¨π¨ |
| Infinite exploration | 12+ consecutive reads with no edit, across 25+ calls | π¨π¨ |
| Repeated reads | Same file read three or more times | π¨ |
| Shell-heavy | 80%+ of 30+ calls were shell commands | π¨ |
Eight read the prompt text:
| Nudge | Trigger | Severity |
|---|---|---|
| Correction spiral | Three corrections in a row | π¨π¨π¨ |
| No verification | A change requested with no test, build, or proof named | π¨π¨ |
| No error detail | Something reported broken with no message or trace pasted | π¨π¨ |
| Unbounded rewrite | A refactor request with no scope limit | π¨π¨ |
| Vague prompt | Under seven words with no file, symbol, or error named | π¨ |
| Bundled asks | Several unrelated requests in one message | π¨ |
| Hedged wording | Two or more hedges, leaving no clear target | π¨ |
| Long session | Forty or more turns | π¨ |
Only one nudge fires per turn, whichever is most urgent, and never the same one twice in a session. Transcript signals are checked before prompt-shape ones, because observed behaviour is stronger evidence than phrasing: during a real problem you want to hear about the problem, not about your writing style.
Worth reading before modifying anything:
- Silent by default. A coach that comments every turn gets muted. Nudges fire only when a measurable threshold trips.
- Once per session per nudge. Repetition turns advice into noise.
- Never blocks. Always exits 0. This is teaching, not policy enforcement β use permissions or a sandbox for real boundaries.
- Teach the mechanism, not the rule. "Keep your context file short" does not transfer to new situations; "it loads on every request, so every line is a recurring cost" does.
- Countable checks only, in the scripts. Line counts and token budgets are
measurable. Judgement calls are left to
/tutor, where a model can read real context, because a regex guessing at tone will be wrong and irritating. - Never name a command the host tool lacks. Advice citing a missing command teaches nothing and costs trust.
All three hosts run the same Python, so thresholds and wording live in one place. What differs is how each invokes it and how it shows you the result.
| Capability | Claude Code | Codex | OpenCode |
|---|---|---|---|
| Config audit at session start | β
SessionStart |
β
SessionStart |
β
session.created |
| Live prompt coaching | β
UserPromptSubmit |
β
UserPromptSubmit |
β
chat.message |
| Context-pressure nudge | β | β | β |
| Standalone CLI scripts | β | β | β |
| How messages reach you | systemMessage |
systemMessage |
TUI toast |
| Terminal bell on urgent nudges | β | β untested | β |
| Coloured status line | β | β no such feature | β no such feature |
Claude Code and Codex are near-identical: both use hooks.json, the same event names,
the same systemMessage / additionalContext split, and the same exit-2 blocking
convention. The Python runs unmodified on both.
OpenCode is architecturally different β it has no shell-command hooks, and plugins are JavaScript modules loaded in-process. The bundled plugin shells out to the same scripts and renders their output as TUI toasts.
How the context-pressure nudge works everywhere, given no tool exposes usage to hooks
No tool passes context-window usage to hooks. On Claude Code, context_window is a
status-line-only field, absent from every hook payload. Codex and OpenCode have no
equivalent at all. Hooks are told what happened, never how full the window is.
But every hook on all three receives transcript_path, and transcripts record
per-response token usage. So scripts/context_usage.py reads the tail of the
transcript, finds the most recent usage record, sums the input-side token counts, and
divides by the model's window. One mechanism, three tools, no per-tool bridge.
It degrades honestly: if the transcript is missing or unreadable the function returns
None and the nudge stays quiet rather than firing on a guess.
Two caveats:
- It is an estimate. Counts come from the last completed response, so the figure lags the current turn slightly.
- The window size is inferred, and 1M is opt-in. A model name alone does not imply extended context, so only an explicit marker earns the larger figure. Better still, if the session has ever compacted, its own compaction boundary is used as the real ceiling. Getting this wrong is not cosmetic: an earlier version reported a session at the brink of compaction as 17% full instead of 84%.
- Formats differ. The parser handles Claude Code's shape and tolerates variants. If
a tool changes its transcript format the reading degrades to
Nonerather than to a wrong number.
| Variable | Default | Effect |
|---|---|---|
TUTOR_REVIEW |
unset | 1 enables assistant-judged prompt review. Costs context. |
TUTOR_REVIEW_MODE |
teach |
teach, brief, or off |
TUTOR_STATE_DIR |
host tool's config dir | Where per-session state is kept |
Prompt review is the one part that is not free. To have judgement applied to a prompt, the request must go where the judge can see it, which means the model's context. It is capped at three notes per session, and saying "stop tutoring" turns it off for the rest of the session.
Every threshold is a named constant at the top of its script. If a nudge is too chatty,
raise its threshold or delete its block in pick_nudge(). To quieten things generally,
raise CONTEXT_WARN_PCT and SESSION_TURN_HINT in scripts/coach.py.
Hook messages cannot be coloured: hooks run with no controlling terminal, and the
terminalSequence field explicitly rejects colour sequences. The status line does
support ANSI colour, so the gauge is separate:
{
"statusLine": {
"type": "command",
"command": "~/.claude/plugins/tutor/scripts/statusline.sh"
}
}Add that to ~/.claude/settings.json for a context bar coloured by pressure, cache hit
rate, model name, and the most recent tutor note. Remove it by deleting the key.
Adding a nudge takes two edits:
- Add a block to
pick_nudge()inscripts/coach.py, positioned by urgency, returning(key, message). - Add the key to
SEVERITY(1β3) andSHORT(the status-line one-liner).
If it needs a fact about the session rather than the prompt, add a counter to
signals() in scripts/transcript.py. Keep those strictly countable: that module
reports what happened and never interprets it, because a nudge built on a judgement call
will eventually fire wrongly and get the whole tool muted.
python3 tests/test_robustness.pyHooks run on every prompt, so a crash breaks the user's turn. This asserts that every script survives malformed input β empty stdin, garbage, wrong JSON types, unreadable transcripts, a 10,000-word prompt β always exits 0, writes nothing to stderr, and emits only valid JSON. It found three real crashes on first run.
scripts/
lint.py size and budget checks
advise.py content-aware advice, with line numbers
coach.py the nudges, and the order they fire in
transcript.py counted behavioural signals from the transcript
context_usage.py context-window usage from a transcript
review_prompt.py opt-in assistant-judged prompt review
statusline.sh coloured gauge (Claude Code only)
tests/ robustness checks
.claude-plugin/ Claude Code plugin and marketplace manifests
hooks/hooks.json Claude Code hook config
codex/hooks.json Codex hook config
opencode/tutor.js OpenCode plugin (shells out to scripts/)
SKILL.md the on-demand /tutor review, read by all three hosts
This is young software. Being straight about that:
- The thresholds are informed guesses. 75% context, three corrections, seven words, twelve reads. They have not been validated against a real cohort, so expect to tune them. Every one is a named constant for exactly that reason.
- Some nudges will misfire. The hedging check will flag a politely-worded prompt that was perfectly clear. That is why nothing ever blocks, and why low-severity nudges are explicitly ignorable.
- The session critique is the weakest part. A model reviewing the session it is
inside cannot see what it failed to notice, and has a stake in the verdict.
SKILL.mdsays so, and offers a fresh subagent instead. - Windows is unsupported. Hooks are cross-platform but
statusline.shis bash. - The terminal bell on Codex is untested.
terminalSequenceis documented for Claude Code; Codex may ignore the unknown field or reject the payload. Hence the β.
Verified against Claude Code 2.1.218, Codex CLI 0.145.0, and the OpenCode 1.18.x line.
Some behaviour referenced in SKILL.md is gated on later Claude Code versions:
/autocompact needs 2.1.221+, cache TTL settings need 2.1.242+.
Bug reports and threshold data from real use are especially welcome.
Thresholds and guidance come from the Claude Code docs, Codex docs, and OpenCode docs.
The reasoning about attention degradation draws on Liu et al., Lost in the Middle (2023) and Chroma Research, Context Rot (2025).
Where this plugin and the official docs disagree, the docs win.
Dual licensed under MIT and Apache 2.0, at your option.