SLOPSHOPPER

guard

Holds shell commands that delete folders or overwrite work

newguard
v0.1.0NOASSERTIONupdated 2026-10-08vukrosic/claude-code-mods-from-scratch/2-guard
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · guard
› 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(rm -rf build && git push --force origin main) ⎿ Denied by guard: Held by the guard mod: this command deletes a whole folder with no way back. Ask the user ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
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 2 files
hooks/register.js 18 lines
1// Mod 2: ANSWER. Hold shell commands that delete folders or overwrite work.
2import { why } from './rules.js'
3
4async function guard($, e, next) {
5  const reason = why(e.command)
6  // No next(e): the command never runs, and Claude reads this sentence instead
7  if (reason) return { deny: 'Held by the guard mod: this command ' + reason + '. Ask the user first.' }
8  return next(e)
9}
10
11export function register(on) {
12  // If the guard itself breaks, hold the command instead of letting it through
13  on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
14    if (next.called) return next(e)
15    return { deny: 'The guard mod failed, so this command was not run: ' + next.error.kind }
16  })
17}
18
hooks/rules.js 19 lines
1// The guard's rules: a test for the command, and the reason Claude is given.
2export const RULES = [
3  [(c) => /\brm\b/.test(c) && flag(c, 'r') && flag(c, 'f'), 'deletes a whole folder with no way back'],
4  [(c) => /\bgit\s+push\b/.test(c) && /--force\b|\s-f\b/.test(c), 'overwrites the history on the server'],
5  [(c) => /\bgit\s+reset\s+--hard\b/.test(c), 'throws away work that was never saved'],
6  [(c) => /\bgit\s+clean\b/.test(c) && flag(c, 'f'), 'deletes files git does not know'],
7]
8
9// true when a one-letter flag is somewhere in the command: -rf, -fr, -r -f, -Rf
10function flag(command, letter) {
11  return command.split(/\s+/).some((w) => /^-[a-zA-Z]+$/.test(w) && w.toLowerCase().includes(letter))
12}
13
14// The reason a command is risky, or null when it is fine
15export function why(command) {
16  for (const [risky, reason] of RULES) if (risky(command)) return reason
17  return null
18}
19