SLOPSHOPPER

tpl-guard

Blocks edits to protected files (.env, lockfiles) and toasts when a shell command fails.

newguardcommandtoast
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · tpl-guard
› fix the failing auth test and add an audit log call ╭──────────────────╮ │ tpl-guard │ ⏺ Read(src/auth.ts) │ failed: bun test │ ⎿ 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 › /tpl-guard ⎿ tpl-guard: tpl-guard is on. ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

plugin-marketplace

Béla's Claude Code plugins: mods (function-hook plugins that draw panes, bands and status lines), the skill that builds them, and personal skills grouped by theme.

PluginWhat it is
mods/mods opens a keyboard-driven pane that lists your mods, shows what each one does, and switches it on/off now or at startup.
mod-builderA skill that builds new mods to a fixed bar: four tested templates, a mandatory `/<mod> on\off, a bare /<mod>` that toggles what it opened, keyboard-first panes, and a validate + type-check + test gate.
briefingsai-, tech-, geo-, cyber-, econ-, space-briefing, and morning-briefing, which runs them all into one digest.
decksmarp-deck (Marp slides on a tech topic), feature-deck (researches a release and hands it to marp-deck), obsidian-canvas. Decks go to $MARP_DECKS_DIR, else ~/Documents/Presentations.
dev-toolscleanup, deps-audit, docgen, explain, find-bug, justify, package-scout, quick-commit, review-pr, scaffold, summarize-changes.
skill-toolsskill-writer, skill-refactor, skill-guide, agent-retro.

Install

At the prompt of a terminal Claude Code session:

/plugin install mods --marketplace B314-N03/plugin-marketplace
/plugin install mod-builder --marketplace B314-N03/plugin-marketplace
/plugin install briefings --marketplace B314-N03/plugin-marketplace
/plugin install decks --marketplace B314-N03/plugin-marketplace
/plugin install dev-tools --marketplace B314-N03/plugin-marketplace
/plugin install skill-tools --marketplace B314-N03/plugin-marketplace

A plugin's skills are called by their namespaced name, e.g. /briefings:ai-briefing.

Answer y to add the marketplace the first time, then choose the user scope. Get updates with claude plugin update <name>, then /reload-plugins.

Developing here

Add your clone as a folder marketplace instead. Installed plugins are then read straight from the folder, so an edit reaches a session after /reload-plugins, with no version bump:

claude plugin marketplace add ~/Coding/plugin-marketplace
claude plugin install mods@b314
claude plugin install mod-builder@b314
# and briefings@b314, decks@b314, dev-tools@b314, skill-tools@b314

Gate a plugin before committing (the types file path is the one the built-in plugin-authoring skill names when it loads):

bash plugins/mod-builder/skills/mod-builder/scripts/check.sh plugins/mods <types-file>

It must end with == PASS. claude plugin validate . checks the marketplace file.

Layout

.claude-plugin/marketplace.json   the plugin list
plugins/mods/                     hooks module, pure logic, tests, types contract
plugins/mod-builder/skills/mod-builder/
  SKILL.md                        the build procedure
  reference/conventions.md        the rules (off switch, keyboard, tests)
  templates/                      status-line, band-pane, tool-hook, poller
  scripts/                        new-mod.sh, check.sh, promote.sh
Source 2 files
hooks/register.ts 53 lines
1import type { Register } from 'claude-code'
2
3import { parseToggle, protectedReason, shortCommand } from './logic'
4
5const NAME = 'tpl-guard'
6const OFF_KEY = 'isOff'
7
8// A guard decides from `e` and module values alone: its `.catch` may run as a
9// re-entry, where `$` calls reject. session.start re-hydrates this on reload.
10let isOff = false
11
12const judge = (path: string) => (isOff ? null : protectedReason(path))
13
14export const register: Register = on => {
15  on('session.start', async ($, e, next) => {
16    await $.command.register({
17      name: 'tpl-guard',
18      description: `Protected-file guard. /${NAME} off disables it, /${NAME} on enables it again`,
19      argumentHint: '[on|off]',
20    })
21    isOff = (await $.store.get(OFF_KEY)) === true
22    return next(e)
23  })
24
25  // Guards: judge before next, refuse when the hook itself fails (fail closed).
26  on('tool.call', { tool: 'Edit' }, ($, e, next) => {
27    const reason = judge(e.file_path)
28    return reason ? { deny: `${NAME}: ${reason}` } : next(e)
29  }).catch(($, e, next) => (next.called ? next(e) : { deny: `${NAME}: the guard failed, so the edit was refused` }))
30
31  on('tool.call', { tool: 'Write' }, ($, e, next) => {
32    const reason = judge(e.file_path)
33    return reason ? { deny: `${NAME}: ${reason}` } : next(e)
34  }).catch(($, e, next) => (next.called ? next(e) : { deny: `${NAME}: the guard failed, so the write was refused` }))
35
36  // Observer: acts on the result and never blocks (a throw just skips it).
37  on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
38    const ran = await next(e)
39    if (!isOff && ran.deny === undefined && ran.isError === true) $.ui.toast(`failed: ${shortCommand(e.command)}`)
40    return ran
41  })
42
43  on('command.run', { command: 'tpl-guard' }, async ($, e) => {
44    const toggle = parseToggle(e.args)
45    if (toggle === 'invalid') return { text: `Use /${NAME}, /${NAME} on or /${NAME} off.` }
46    // A guard never switches off by a bare command: dropping a safety rule takes an explicit `off`.
47    if (toggle === 'show') return { text: `${NAME} is ${isOff ? 'off' : 'on'}.` }
48    isOff = toggle === 'off'
49    await $.store.set(OFF_KEY, isOff)
50    return { text: isOff ? `${NAME} off (also in new sessions). /${NAME} on to re-enable.` : `${NAME} on.` }
51  })
52}
53
hooks/logic.ts 21 lines
1export const PROTECTED: readonly RegExp[] = [
2  /(^|\/)\.env(\.|$)/,
3  /(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$/,
4]
5
6export const protectedReason = (path: string) =>
7  PROTECTED.some(pattern => pattern.test(path)) ? `${path} is protected: edit it by hand or turn the guard off` : null
8
9export const shortCommand = (command: string, max = 40) => {
10  const line = command.trim().split('\n')[0] ?? ''
11  return line.length > max ? `${line.slice(0, max - 1)}…` : line
12}
13
14export type Toggle = 'on' | 'off' | 'show' | 'invalid'
15
16export const parseToggle = (args: string): Toggle => {
17  const arg = args.trim().toLowerCase()
18  if (arg === '') return 'show'
19  return arg === 'on' || arg === 'off' ? arg : 'invalid'
20}
21