OPT-IN BLOCKING. Sibling of
rate-limiter(advisory). Whenstate/rate-policy.jsonsetsenabled: true, this PreToolUse hook blocks (exit 2) any tool call once the active(session, skill)token bucket is empty. Default disabled — out of the box this shield does nothing.
rate-limiter is observability (advisory, always exits 0, prints to
stderr when a bucket empties). It conforms to
../vis/packages/core/conduct/hooks.md "Hooks inform, they don't decide". Operators
who want enforcement, not just signal, install this plugin in
addition to (or instead of) the limiter.
Splitting them keeps:
- The advisory contract clean for users who don't want surprises.
- The blocking contract opt-in and reversible by editing one JSON file.
- The override of
hooks.mdlocalized to one plugin (this one), which CLAUDE.md explicitly permits: "When a module conflicts with a plugin-local instruction, the plugin wins — but log the override."
rate-shield shares state with rate-limiter whenever both are
installed:
- If
../rate-limiter/state/buckets.jsonexists, the shield reads and writes that file (so a single decrement counts against the same bucket the advisory limiter sees). - Otherwise the shield falls back to its own
state/buckets.json(created lazily on first run). - If the sibling file is corrupted/unreadable, the shield's fail-safe
path returns
exit 0(graceful degrade).
This means the per-skill overrides you set in rate-policy.json (this
plugin) drive the decrement semantics, while the sibling's
buckets.json is the state surface.
This plugin overrides the project-wide rule in
../vis/packages/core/conduct/hooks.md that hooks must be advisory-only. The override
is bounded:
- Off by default. Disabled
state/rate-policy.jsonships out of the box;enabled:falsemeans the hook is a silent no-op. - Fail-safe. Any error in the shield itself (malformed policy,
parse failure, IO error, missing python) results in
exit 0. Operator must fix the config to re-enable enforcement. - Subagent-recursion guard.
$CLAUDE_SUBAGENTset → exit 0. - Pre-filtered hot path. Disabled-policy fast path is a single
grep+ early exit, no python startup.
# 1) Copy the example policy.
cp state/rate-policy.example.json state/rate-policy.json
# 2) Edit to enable; tune per_skill as needed.
# {
# "enabled": true,
# "default_capacity": 60,
# "default_refill_per_sec": 1.0,
# "per_skill": { "deep-research": { "capacity": 200, "refill_per_sec": 5.0 } }
# }
# 3) Restart Claude Code so the hook is registered.
# 4) Reverse anytime by setting enabled:false.- PreToolUse hook on all tools. Pre-filter: bails if policy file
absent OR
enabled:truenot present in the JSON. - shield-check.py:
- Loads
state/rate-policy.json. If disabled → exit 0. - Resolves
(session, skill)identity:session=$CLAUDE_SESSION_IDif set, else SHA-1 ofcwd:ppid(truncated to 16 chars).skill= closestskills/<name>/ancestor of cwd, elseungated.
- Looks up
(capacity, refill_per_sec)frompolicy.per_skill[skill]when set, elsepolicy.default_*. - Opens the active
buckets.json(sibling preferred, own fallback) under exclusive lock. - Refills:
tokens = min(capacity, tokens + dt * refill). - If
tokens >= 1→ consume, write-back, exit 0. Else → record empty, write-back, stderr advisory, exit 2.
- Loads
- Subagent-recursion guard (
$CLAUDE_SUBAGENT). - Fail-safe. Any unhandled exception → exit 0.
=== rate-shield (BLOCKED) ===
Bucket empty for skill=<skill> (capacity=<n>, refill=<r>/sec). Tool
call blocked to prevent runaway loop. To unblock: wait for refill,
raise the limit in state/rate-policy.json (per_skill override), or set
enabled:false to disable the shield.
plugins/rate-shield/
├── .claude-plugin/plugin.json
├── README.md (this file)
├── hooks/hooks.json (PreToolUse registration)
├── hooks/pretooluse.sh (recursion guard, fail-safe, propagates exit 2)
├── scripts/shield-check.py (token-bucket decrement, exit 2 on empty)
├── skills/shield-awareness/SKILL.md (interpretation + opt-in flow)
└── state/rate-policy.example.json (default-disabled template)
rate-limiter— advisory sibling (observability only); this shield reads itsstate/buckets.jsonwhen present.budget-watcher— post-hoc cost ceilings.../vis/packages/core/conduct/hooks.md— advisory-default rule (overridden here, per the override-note above).- F-013 (rate-limit enforcement) — closed by this plugin.