Starter mod: logs each tool call and blocks Bash commands that contain rm -rf.

The smallest mod that does something. One tool.call hook: logs the tool name to the transcript and refuses Bash commands containing rm -rf. Its .catch refuses too if the hook throws, so the guard fails closed.
Copy this folder to start a new mod. Rename in .claude-plugin/plugin.json.
Try it for one session (Claude Code 2.1.287 or later):
claude --plugin-dir examples/hello-mod
Run its test, no session needed:
cd examples/hello-mod && claude plugin test
Prove it loads and the deny holds in a real headless run (two small model calls):
examples/hello-mod/smoke.sh examples/hello-mod hello-modhooks/register.ts 23 lines1import type { Register } from 'claude-code'
2
3/**
4 * Minimal mod. Copy this folder and change the hook body.
5 *
6 * Every hook is ($, e, next). Return without next to answer alone.
7 * Return next(e) to let the rest of the chain and the engine run.
8 */
9export const register: Register = (on) => {
10 on('tool.call', ($, e, next) => {
11 $.ui.log(`hello-mod: ${e.tool}`)
12
13 // Deny must happen before next(e). After next the tool already ran.
14 if (e.tool === 'Bash' && e.command.includes('rm -rf')) {
15 return { deny: 'hello-mod: rm -rf is blocked' }
16 }
17
18 return next(e)
19 })
20 // A guard that throws is skipped and the tool runs. This makes it fail closed.
21 .catch(($, e, next) => (next.called ? next(e) : { deny: 'hello-mod: guard failed' }))
22}
23