Nebster's Claude Code toolkit — Reviewer, Fixer, and SEO subagents, /nebster:review and /nebster:qa skills, Comrade, Peon and Mate output styles, plus a…

A Claude Code plugin marketplace shipping the nebster plugin — specialized subagents, skills (slash commands), and output styles (personas). Install once and — with auto-update enabled — Claude Code keeps them up to date from GitHub.
Add the marketplace and install the plugin:
/plugin marketplace add NebsterSK/claude-settings
/plugin install nebster@nebster-claude-settings
That's it — the agents, commands, and output styles are now available in every project.
Third-party marketplaces do not auto-update by default. Enable it once so you get new agents/commands on startup:
/plugin, open the Marketplaces tab, select nebster-claude-settings, and enable auto-update.settings.json): {
"extraKnownMarketplaces": {
"nebster-claude-settings": {
"source": { "source": "github", "repo": "NebsterSK/claude-settings" },
"autoUpdate": true
}
}
}
Because the plugin ships without a pinned version, every push to this repo is treated as a new version — enabled installs pick it up automatically.
| Agent | Purpose |
|---|---|
| Reviewer | Senior code reviewer / quality gatekeeper. Audits security, bugs, tech debt, performance, testing, and accessibility before production — and, on a PR, checks the diff against what the PR says it does. |
| Fixer | Applies a single scoped fix from a review finding — edits only the fix's footprint, adds a covering test for behavior fixes, makes no unrelated changes, reports back in one line. Spawned in parallel by the review command. |
| SEO | On-page and technical SEO — meta descriptions, keywords, structured data, Core Web Vitals. |
Skills are invoked as slash commands namespaced under the plugin name.
| Skill | Purpose |
|---|---|
/nebster:review | Dispatches the change set to the Reviewer subagent and relays its findings. Fixes them interactively on your own work, or reports read-only on someone else's PR — see Review modes. |
/nebster:qa | Code-style and static-analysis gate. Runs Larastan, Pint, ESLint, and Prettier and fixes every issue they surface. Does not run the test suite. Runs in an isolated subagent and only when you invoke it. |
/nebster:review tries the gh CLI first to read the PR behind your current branch — title, description, linked issue, and existing review comments — and hands that stated purpose to the Reviewer as a claim to verify against the diff (goal not met, scope creep, undocumented behavior change). No gh, or no PR? It says so and reviews the plain git diff.
Check the branch out yourself first — the command never runs gh pr checkout.
Fix mode — your own branch or your own PR. Findings with exactly one obvious fix that restores the code's evident intent (no product-rule, API, schema, or data decision) are auto-fixed in the background at any severity — Medium/Low immediately, Critical/High after a single confirm where you can hold some back. Everything that needs a decision is triaged one finding at a time, uncapped, with accepted fixes applied by background Fixer agents on a shared per-file queue. Closes with scoped tests and a status table.
Report-only mode — the PR author isn't you, or you passed --report-only. Nothing is edited: no fixers, no commits, no pushes. You get the full report, then an offer to post findings to the PR — you pick which ones, and the payload is shown for confirmation before anything is published. Posts as a plain review comment, never an approval or change request.
Arguments are positional: base branch first, PR second.
/nebster:review # current branch, auto-detects base and mode
/nebster:review develop # diff against a specific base
/nebster:review 1234 # a PR number or URL (base auto-detected)
/nebster:review develop 1234 # both
/nebster:review --report-only # force read-only on your own work
The older base=develop / pr=1234 forms still work.
Personas for the terminal reply only. Pick one with /output-style. Both keep Claude's coding instructions intact and stay out of everything that leaves the terminal — code, comments, commits, PR descriptions, docs, tickets. Code, paths, and error messages are never accented or translated.
| Style | Persona |
|---|---|
| Comrade | Comrade Claude Vladimirowich Claudowich, loyal servant of the Soviet Union. Heavy Russian accent, "we" not "I", bugs are imperialist saboteurs, tests are the five-year plan, git is state ceremony. |
| Peon | Orc peon from Warcraft 3. Broken peon English, "Work, work." / "Job's done!", bugs are Alliance scouts in the base, failing tests mean the base is under attack. |
A band above the prompt, under a dim divider: the session name (from /rename) in bold, then model, effort and output style, then context used, 5-hour and weekly usage — each green below 60%, yellow from 60%, red from 85%. It loads with the plugin; nothing to enable. Source in hooks/register.tsx.
This repo is both the marketplace and the plugin:
claude-settings/
├── .claude-plugin/
│ ├── marketplace.json # marketplace manifest (lists the plugin)
│ └── plugin.json # plugin manifest (no version → always latest)
├── agents/ # reviewer, fixer, seo
├── hooks/ # session band mod (hooks.json + register.tsx)
├── output-styles/ # comrade.md, peon.md
├── skills/ # review/SKILL.md, qa/SKILL.md
├── tests/ # mod tests (claude plugin test .)
└── types/ # mod state contract
gh CLI, authenticated (gh auth login). It's optional — without it /nebster:review falls back to a plain diff review./nebster:qa assumes a Laravel + JS/TS project with composer larastan, composer pint, npm run lint, npm run prettier, and npm run types scripts. Adjust to match your tooling.claude plugin validate . and claude plugin test ..hooks/register.tsx 164 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register } from 'claude-code'
3
4import type { Info } from '../types'
5
6const name = atom({ plugin: 'nebster', key: 'name' } as const, null)
7const info = atom({ plugin: 'nebster', key: 'info' } as const, {
8 model: null,
9 effort: null,
10 style: null,
11 percent: null,
12 fiveHour: null,
13 weekly: null,
14} as Info)
15
16// The latest `/rename` lives in the transcript as a `custom-title` row.
17export const lastTitle = (transcript: string): string | null => {
18 const lines = transcript.split('\n')
19 for (let i = lines.length - 1; i >= 0; i--) {
20 const line = lines[i]
21 if (!line?.includes('"custom-title"')) continue
22 try {
23 const row = JSON.parse(line) as { type?: string; customTitle?: string }
24 if (row.type === 'custom-title' && row.customTitle) return row.customTitle
25 } catch {}
26 }
27 return null
28}
29
30// `claude-opus-5-5[1m]` → `Opus 5.5`; anything else passes through.
31export const prettyModel = (id: string): string => {
32 const match = /^claude-([a-z]+)-(\d+)(?:-(\d{1,2}))?(?:-\d{8})?/.exec(id)
33 if (!match) return id
34 const [, family = '', major, minor] = match
35 const label = family.charAt(0).toUpperCase() + family.slice(1)
36 return `${label} ${major}${minor ? `.${minor}` : ''}`
37}
38
39// Green, then yellow from 60%, red from 85%.
40export const usageColor = (percent: number): 'green' | 'yellow' | 'red' =>
41 percent >= 85 ? 'red' : percent >= 60 ? 'yellow' : 'green'
42
43// A rate-limit window's use, rounded; null when the account reports none.
44export const windowPercent = (
45 rateLimits: readonly { kind: string; percentUsed: number }[],
46 kind: 'five_hour' | 'seven_day',
47): number | null => {
48 const window = rateLimits.find(limit => limit.kind === kind)
49 return window === undefined ? null : Math.round(window.percentUsed)
50}
51
52async function refresh($: EngineInterface) {
53 const config =
54 (await $.env.get('CLAUDE_CONFIG_DIR')) ??
55 `${(await $.env.get('USERPROFILE')) ?? (await $.env.get('HOME'))}/.claude`
56 const project = (await $.session.root()).replace(/[^a-zA-Z0-9]/g, '-')
57 const path = `${config}/projects/${project}/${await $.session.id()}.jsonl`
58 const title = lastTitle(await $.fs.read(path).catch(() => ''))
59 if (title !== null) await update($, name, () => title)
60
61 const { context, rateLimits } = await $.session.usage()
62 const model = prettyModel(await $.session.model())
63 await update($, info, current => ({
64 ...current,
65 model: current.model ?? model,
66 percent: context.percent ?? current.percent,
67 fiveHour: windowPercent(rateLimits, 'five_hour') ?? current.fiveHour,
68 weekly: windowPercent(rateLimits, 'seven_day') ?? current.weekly,
69 }))
70}
71
72// Before the first turn, effort and output style come from settings.
73async function seed($: EngineInterface) {
74 const settings = await $.settings.read()
75 const effort = typeof settings.effortLevel === 'string' ? settings.effortLevel : null
76 const style = typeof settings.outputStyle === 'string' ? settings.outputStyle : null
77 await update($, info, current => ({
78 ...current,
79 effort: current.effort ?? effort,
80 style: current.style ?? style,
81 }))
82}
83
84export const register: Register = on => {
85 on('session.start', async ($, e, next) => {
86 const result = await next(e)
87 await seed($).catch(() => {})
88 await refresh($).catch(() => {})
89 return result
90 })
91
92 on('session.append', { door: 'command' }, async ($, e, next) => {
93 const result = await next(e)
94 await refresh($).catch(() => {})
95 return result
96 })
97
98 on('turn.complete', async ($, e, next) => {
99 const result = await next(e)
100 await refresh($).catch(() => {})
101 return result
102 })
103
104 // Rate-limit windows move between turns too; the engine pushes each whole-point move.
105 on('session.measure', async ($, e, next) => {
106 if (e.changed.includes('rateLimits')) {
107 const fiveHour = windowPercent(e.rateLimits, 'five_hour')
108 const weekly = windowPercent(e.rateLimits, 'seven_day')
109 await update($, info, current => ({
110 ...current,
111 fiveHour: fiveHour ?? current.fiveHour,
112 weekly: weekly ?? current.weekly,
113 })).catch(() => {})
114 }
115 return next(e)
116 })
117
118 // Each main-loop request carries the model and effort it actually used.
119 on('turn.step', async function* ($, e, next) {
120 if (e.agentId === undefined) {
121 const model = prettyModel(e.model)
122 const effort = e.effort === undefined ? null : String(e.effort)
123 await update($, info, current => ({ ...current, model, effort })).catch(() => {})
124 }
125 return yield* next(e)
126 })
127
128 // Subagent prompts carry no style, so only a named one counts.
129 on('prompt.compose', async ($, e, next) => {
130 if (e.outputStyle !== null) {
131 const style = e.outputStyle.name
132 await update($, info, current => ({ ...current, style })).catch(() => {})
133 }
134 return next(e)
135 })
136
137 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
138 const current = await read($, name)
139 const { model, effort, style, percent, fiveHour, weekly } = await read($, info)
140 if (e.props.hasSurvey) return next(e)
141
142 const { Box, Text } = $.ui.resolve(e)
143 const details = [model, effort, style].filter(Boolean).join(' · ')
144
145 // A dim rule across the band's full width sets it apart from the chat.
146 return (
147 <Box flexDirection="column">
148 <Text dimColor>{'─'.repeat(Math.max(e.props.bodyColumns, 0))}</Text>
149 <Box>
150 {current !== null && (
151 <Text bold color="cyan">
152 {'> '}{current}
153 </Text>
154 )}
155 {details !== '' && <Text dimColor>{`${current !== null ? ' ' : ''}${details}`}</Text>}
156 {percent !== null && <Text color={usageColor(percent)}>{` ${percent}% ctx`}</Text>}
157 {fiveHour !== null && <Text color={usageColor(fiveHour)}>{` ${fiveHour}% 5h`}</Text>}
158 {weekly !== null && <Text color={usageColor(weekly)}>{` ${weekly}% 7d`}</Text>}
159 </Box>
160 </Box>
161 )
162 })
163}
164types/index.d.ts 15 lines1export type Info = {
2 model: string | null
3 effort: string | null
4 style: string | null
5 percent: number | null
6 fiveHour: number | null
7 weekly: number | null
8}
9
10declare module 'claude-code' {
11 interface PluginState {
12 nebster: { name: string | null; info: Info }
13 }
14}
15