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

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.
| Plugin | What 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-builder | A 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. |
briefings | ai-, tech-, geo-, cyber-, econ-, space-briefing, and morning-briefing, which runs them all into one digest. | |
decks | marp-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-tools | cleanup, deps-audit, docgen, explain, find-bug, justify, package-scout, quick-commit, review-pr, scaffold, summarize-changes. | |
skill-tools | skill-writer, skill-refactor, skill-guide, agent-retro. |
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.
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.
.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.shhooks/register.ts 53 lines1import 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}
53hooks/logic.ts 21 lines1export 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