@shizhanyu13/ironbound-hook
v0.1.2
Published
Claude Code PreToolUse hook that hard-gates dangerous shell commands before they ever run, with a double-layer degrade counter (same-issue repeated -> 'ask' human review; cumulative -> hard 'deny'). Zero DSH/runtime deps; cross-platform. Ports the deny +
Downloads
520
Maintainers
Readme
ironbound-hook
Ironbound hard-gate for Claude Code. A Claude Code PreToolUse hook that blocks dangerous shell commands before they run, with a double-layer degrade counter: the same issue repeated escalates from a flat
denyto anask(human review), and cumulative abuse is a harddeny. Zero DSH dependencies, zero runtime deps, cross-platform.
Why this exists
@shizhanyu13/dsh-ironbound-policy hard-gates dangerous tool calls inside the DeepSeek Harness, keying its degrade counter on an in-process Agent object. But Claude Code's PreToolUse hook is a fresh process every call — an in-memory counter would reset each time, so the degrade/block escalation would never trigger.
@shizhanyu13/ironbound-hook takes the same deny list + degrade logic and:
- persists the counter to
<cwd>/.ironbound/counter.json(override withIRONBOUND_STATE_DIR), so the escalation survives across calls; - speaks the Claude Code PreToolUse contract directly (
hookSpecificOutputwithpermissionDecision); - is fully buildable (tsc →
lib/) with 0 runtime deps, so it runs anywhere Node 20+ is.
Division of labor
- dsh-ironbound-policy — the same guard inside DSH (in-process counter on an
Agent).- ironbound-hook (this package) — the same guard at the Claude Code boundary (file-persisted counter, host decides allow/deny/ask).
The degrade-counter differentiation
| state | permissionDecision | model-facing reason |
|---|---|---|
| safe / non-shell tool | (no output — tool runs) | — |
| first deny-list hit | deny | dangerous command blocked (A:Bypass): <cmd> |
| same issue repeated to the per-issue limit | ask | degraded — this issue repeated Nx; human review required |
| cumulative denies reach the total limit | deny | BLOCKED — cumulative limit (N/M) reached; human review required |
| a later clean shell run | (no output — tool runs) | resets the per-issue counter (same-issue streak recovered) |
A model that keeps hitting the same wall escalates from a flat deny to an ask (a human gates it), and a cumulative abuser is a hard deny — never a silent allow.
The loop closes: a clean shell command (a shell tool, non-empty command, no deny-list match) resets the per-issue counter, so a same-issue streak recovers back to deny instead of ratcheting irreversibly into BLOCKED. The total is preserved on a clean run — it is lifetime accountability and never resets, so a persistent abuser still hits the hard block. Enforce-vs-recover, not just punish.
Quickstart
1. Wire the hook into Claude Code
Write this to .claude/settings.json (or merge into your project settings):
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "npx -y @shizhanyu13/ironbound-hook" }]
}
]
}
}npx caches the package, so the hook starts fast after the first run. For an offline/standalone install, add @shizhanyu13/ironbound-hook as a devDependency and point command at node_modules/.bin/ironbound-hook.
2. Configure the gates (optional)
| env var | default | meaning |
|---|---|---|
| IRONBOUND_STATE_DIR | <cwd>/.ironbound | where counter.json lives |
| IRONBOUND_PER_ISSUE_LIMIT | 3 | same-issue repeats before an ask |
| IRONBOUND_TOTAL_LIMIT | 10 | cumulative denies before a hard deny |
| IRONBOUND_DENY_EXTRA | (empty) | extra deny-list regexes (newline/comma separated) |
The default deny list
Ported verbatim from Claude Code's /scripts/defenses/block-dangerous-cmd.sh:
rm -rf, git push --force, git reset --hard, git clean -f[d|x],
git push origin --delete, git commit --no-verify, git commit -n,
DROP (TABLE|DATABASE|SCHEMA|INDEX), TRUNCATE TABLE,
prisma migrate reset, prisma db push --force-reset,
shutdown|reboot|restart, format|diskpart, reg delete,
chmod -R 777, del /f /s | rmdir /s /q | rd /s /qExtend it with IRONBOUND_DENY_EXTRA — each pattern is a JS regex, matched case-insensitively and added to the built-in list.
Behavior contract
- Matching shell tools (claude code + DSH aliases):
Bash,pwsh,PowerShell,tool-bash,tool-pwsh,Shell. - Non-shell tools and safe commands emit no stdout — a PreToolUse hook that outputs nothing lets the tool run.
- Atomic-enough persistence —
saveStatewrites tocounter.jsonand never throws; persistence failure can never break the hook. - Malformed/missing stdin exits 0 without blocking — a broken hook must never block a tool call.
API
import { evaluateCommand, permissionDecisionOf, BUILTIN_DENY } from '@shizhanyu13/ironbound-hook'
import { hookDecision, run } from '@shizhanyu13/ironbound-hook/hook'@shizhanyu13/ironbound-hook(the.export) is the pure core —BUILTIN_DENY,evaluateCommand,recordDenial,permissionDecisionOf,denialReason,isShell,matchDeny,emptyState. No I/O, no side effects.@shizhanyu13/ironbound-hook/hookis the stdin/stdout hook —hookDecision,loadState,saveState,respond,run, plus the CLI entrypoint (bin: lib/hook.js).
Relationship to @shizhanyu13/dsh-ironbound-policy
dsh-ironbound-policy is the same guard inside DSH (its counter is a WeakMap<Agent> held in-process). ironbound-hook reuses the deny list + degrade verdict but swaps the counter to a file-persisted store and the execution seam to the Claude Code PreToolUse boundary. Upgrade the deny list / degrade thresholds in DSH → port the same change to src/core.ts and re-release.
License
MIT
