SLOPSHOPPER

nebster

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

newbandprompt
v?no licenseupdated 2026-10-09NebsterSK/claude-settings
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · nebster
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ──────────────────────────────────────────────────────────────────────────────────────────────────── Opus 5.5 49% ctx 31% 5h ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Band
──────────────────────────────────────────────────────────────────────────────────────────────────── Opus 5.5 49% ctx 31% 5h
README

Nebster's Claude Code Plugin

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.

Install

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.

Keep it auto-updating

Third-party marketplaces do not auto-update by default. Enable it once so you get new agents/commands on startup:

  • Via UI: run /plugin, open the Marketplaces tab, select nebster-claude-settings, and enable auto-update.
  • Via managed settings (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.

What's inside

Agents

AgentPurpose
ReviewerSenior 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.
FixerApplies 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.
SEOOn-page and technical SEO — meta descriptions, keywords, structured data, Core Web Vitals.

Skills

Skills are invoked as slash commands namespaced under the plugin name.

SkillPurpose
/nebster:reviewDispatches 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:qaCode-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.
Review modes

/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.

Output styles

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.

StylePersona
ComradeComrade 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.
PeonOrc 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.

Session band (mod)

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.

Repository layout

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

Notes

  • The Reviewer targets a Laravel backend but stays framework-agnostic on the frontend (Vue, Livewire, React).
  • PR context and posting findings need the 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.
  • Validate changes before pushing with claude plugin validate . and claude plugin test ..
Source 2 files
hooks/register.tsx 164 lines
1import { 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}
164
types/index.d.ts 15 lines
1export 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