bash + jq only for hooks. No Node.js. No Python. No external APIs. Python stdlib is OK for non-time-critical scripts (report generation).
Before submitting a PR, verify:
- Never use
flock— macOS doesn't have it. Use atomicmkdirfor locks. - Never use
$CLAUDE_SESSION_IDfor cache keys — doesn't reset after /clear. - Never use
jq -son growing files — slurps entire file into RAM. Safe on bounded inputs (tail -n 20output). - Every hook has
trap 'exit 0' ERR INT TERM— hooks must never break Claude. - 64KB stdout limit — write large output to tmpfiles under
/tmp/emu-*. - Validate JSON before parsing with
jq empty. - Block URL-encoded path traversal — decode
%2e%2ebefore checking. - Rotation at 10MB not 1MB.
- Use
jq -n --argfor JSON construction — never printf/sed chains.
shellcheck -xpasses on all.shfiles- Use
printf "%s"overechofor variable content - Use
localfor function variables - Quote all variable expansions (
"$var"not$var) - Use
[[ ]]over[ ]for conditionals - Use
$(command)over backticks - Keep hook scripts under 200 lines
- One responsibility per hook script
bash tests/run-all.shEach test pipes mock JSON to hooks via stdin and verifies exit codes and output. Tests must clean up all temp files and state files after running.
Each plugin owns exactly one hook lifecycle phase:
PreToolUse → token-saver (compress, dedup, delta)
PostToolUse → context-guard (drift detect, token estimation, aging alerts)
PreCompact → state-keeper (checkpoint, restore)
No plugin depends on another. All plugins write to their own state/metrics.jsonl.
Create the following structure:
plugins/<name>/
├── .claude-plugin/plugin.json # name, description, version, author, license, keywords, skills, agents
├── skills/<skill>/SKILL.md # allowed-tools frontmatter, instructions
├── agents/<agent>.md # model, context: fork, allowed-tools, instructions
├── commands/<command>.md # slash commands
├── hooks/
│ ├── hooks.json # lifecycle bindings
│ └── <hook-point>/<script>.sh # hook scripts
├── state/.gitkeep # runtime state
└── README.md # plugin-level documentation
Register the plugin in .claude-plugin/marketplace.json.
{
"name": "emu-<plugin-name>",
"description": "<one-line description>",
"version": "2.0.0",
"author": { "name": "Emu" },
"license": "MIT",
"keywords": ["<relevant>", "<keywords>"],
"commands": ["./commands/<cmd>.md"],
"skills": ["./skills/<skill>/"],
"agents": "./agents/"
}- Create the script in
plugins/<plugin>/hooks/<hook-point>/<name>.sh - Add the binding in
plugins/<plugin>/hooks/hooks.json - Start with:
#!/usr/bin/env bash trap 'exit 0' ERR INT TERM set -uo pipefail
- Source shared utilities:
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$(dirname "$0")/../.." && pwd)}" SHARED_DIR="${PLUGIN_ROOT}/../../shared" source "${SHARED_DIR}/constants.sh" source "${SHARED_DIR}/sanitize.sh" source "${SHARED_DIR}/metrics.sh"
- Read stdin JSON, validate, extract fields
- Always exit 0 on errors
- Add a test in
tests/<plugin>/test-<name>.sh
- Create
plugins/<plugin>/skills/<skill>/SKILL.md - Add
allowed-toolsfrontmatter:--- name: <skill-name> description: > When to trigger. What it does. allowed-tools: - Read - Bash ---
- Register in
plugin.jsonunderskills
- Create
plugins/<plugin>/agents/<agent>.md - Required frontmatter:
--- name: emu-<agent> model: haiku context: fork allowed-tools: - Read - Grep - Bash ---
- Agent must be self-contained — it runs in a forked context
shared/ contains utilities sourced by all hooks:
| File | Contents |
|---|---|
constants.sh |
Version, file names, size limits, thresholds |
metrics.sh |
acquire_lock(), release_lock(), log_metric() |
sanitize.sh |
sanitize_path(), validate_json(), sanitize_for_log() |
scripts/report-gen.sh |
Session report generator (text + optional PDF) |
scripts/learnings.sh |
Bayesian Strategy Accumulation — session learning persistence |
Hooks resolve shared code via:
SHARED_DIR="${PLUGIN_ROOT}/../../shared"shellcheck -xpasses on all.shfilestests/run-all.shexits 0- No banned patterns (
flock,jq -son unbounded files,$CLAUDE_SESSION_IDas cache key) - Every SKILL.md has
allowed-toolsfrontmatter - Every agent has
model,context: fork, andallowed-tools - New hooks have corresponding tests