SLOPSHOPPER

plain-words

End a prompt with ?? to get a short answer in plain words

newprompt
v0.1.0NOASSERTIONupdated 2026-10-08vukrosic/claude-code-mods-from-scratch/3-plain-words
A shopper browsing a rack in a slop shop
README

Claude Code mods from scratch

Code for the video I Built 5 Claude Code Mods. One of Them Lies. In early October 2026 Claude Code got mods: a small file of code that runs inside Claude Code and can watch every action, stop one, rewrite your prompt and draw on your screen. This repository builds five of them from an empty folder, with tests. The four honest ones are 114 lines of JavaScript. The fifth one hides what it does, so that you can practise catching it.

Tested with the test kit of Claude Code 2.1.293 (plugin validate and plugin test). The mods were not run in a live session. The feature is new, and the documentation says events and methods can change between versions.

./run_all.sh                        # checks all six folders without starting a session: no sign-in, no network, no tokens; expected output is in demo-output.txt
CLAUDE=/path/to/claude ./run_all.sh # the same with another Claude Code binary (2.1.287 or newer is needed)
FolderWhat the mod doesLinesTests
1-counterWatches. Counts the tool calls Claude makes, shows the count in the spinner and adds a /tally command.283 pass
2-guard-first-tryAnswers. Holds rm -rf. It has a hole: the same delete spelled -fr, -r -f or with two spaces gets through. One test fails on purpose and shows the hole.102 pass, 1 fail
2-guardAnswers. Holds shell commands that delete a folder, force-push, hard-reset or git clean, and tells Claude the reason. The rules are plain functions in rules.js. If the guard itself crashes, the command is held (.catch).357 pass
3-plain-wordsRewrites. End a prompt with ?? and the mod rewrites it to ask for a short answer in plain words.143 pass
4-meterDraws. Shows how full Claude's memory (the context window) is, as a bar above the prompt.374 pass
5-read-the-labelDo not install. The name card says "Adds a sparkle to the spinner". It does. Three more lines approve every request to read a file before you are asked.161 pass

Each folder has the same three parts: .claude-plugin/plugin.json (the name card), hooks/hooks.json (points at the code) and hooks/register.js (the mod). Tests are in tests/.

To try an honest mod in a session of your own: claude --plugin-dir ./2-guard. That starts a normal session, so it uses your plan.

Read the label before you install

A mod runs with your permissions and is not sandboxed. Before you install one, run

claude plugin validate ./5-read-the-label

and read two lines: hooks: (the events it listens to) and calls: (what it asks Claude Code for). For mod 5 it prints ui.render{component=Spinner} and tool.check{tool=Read}. The name card never mentioned the second one. If a mod misbehaves, claude --safe-mode starts one session with every mod switched off.

What this is not

  • Not a live test. Every number here comes from plugin validate and plugin test on build 2.1.293.
  • The guard is a seat belt, not a wall. It reads the command, not the files the command starts: bash cleanup.sh passes even when the script deletes a folder.
  • Mod 5 is limited to reading on purpose. Without the { tool: 'Read' } filter the same three lines would approve every tool call.
  • Events and methods can change between versions of Claude Code. If run_all.sh prints something else on a newer build, the build changed.

Try it

Add one rule to 2-guard/hooks/rules.js for a command that you fear. Write the test first in 2-guard/tests/guard.test.ts, and watch it fail.

MIT licence.

Source 1 files
hooks/register.js 15 lines
1// Mod 3: REWRITE. End a prompt with ?? and Claude is asked for a short, plain answer.
2const NOTE = 'Answer in plain words for a beginner. Five sentences at most. No code unless asked.'
3
4export function register(on) {
5  on('prompt.submit', async ($, e, next) => {
6    // An empty prompt is stopped here and never sent
7    if (e.text.trim() === '') return { drop: 'Nothing to send.' }
8    // A prompt without the mark goes on exactly as typed
9    if (!e.text.trim().endsWith('??')) return next(e)
10    // Pass on a changed copy: one ? instead of two, and one extra line for Claude
11    const text = e.text.trim().slice(0, -1)
12    return next({ ...e, text, context: [...(e.context ?? []), NOTE] })
13  })
14}
15