agent-infra-fixer
skill-miner classifies clusters as infra or task. Infra clusters come here.
No eval, no skill draft - reproduce, fix, verify, record.
Process
- Reproduce. Re-run the failing component on a triggering input. For hooks: pipe a realistic tool-input JSON into the hook script and check exit code AND which stream the message lands on.
- Fix the environment (hook, wrapper, permission config), minimal diff.
- Verify with a trigger/benign pair: the fix must fire on the bad input and stay silent on good input.
- Record in
$FORGE_ROOT/ledger.jsonlasdecision: "infra-fix"so the next mining run can confirm the cluster count dropped.
Known failure class: stdout-block hooks
PreToolUse hooks that print block reasons to stdout instead of stderr
surface to the agent as hook error: No stderr output - the block works but the
agent never sees WHY, retries, and the error count explodes. Exit code must be 2
with the reason on stderr.
Detector/fixer: for every exit 2 in a *.sh hook, the echo / printf / cat <<EOF
lines of the same block get >&2; --exit2 also turns a message + exit 1 (which does
not block at all) into exit 2. Dry run prints a diff; --apply writes .bak-stderr
backups. Idempotent.
python3 scripts/fix-hooks.py --exit2 # ~/.claude/hooks, diff only
python3 scripts/fix-hooks.py --hooks-dir /root/.claude/hooks --exit2 --apply
Then verify each fixed hook with a must-block input: exit 2 AND non-empty stderr. 2026-09-16: this class was the #1 mined cluster on both test machines (117 errors in 40% of one machine's sessions, 180 in 22 sessions on the other). The generic fixer reproduces the six hand-made fixes byte for byte.
Known failure class: over-broad guard patterns
A guard that blocks legitimate work is as costly as a missing guard: the agent loses a turn,
invents a workaround, and learns nothing. Substring patterns are the usual cause:
rm -rf / matches every rm -rf /tmp/x, truncate matches "truncated", curl.*|.*sh
matches | head, and heavy-task patterns match pgrep/du arguments, code strings and
commands already wrapped in ssh <server> '...'.
Replay every past block through the current hooks, and prove the guards still hold:
python3 scripts/replay-hook-blocks.py --must-block scripts/must-block.txt [--dump DIR]
now passes: the block no longer fires (fixed hook, or confirm it was a false positive).still blocked: inspect each reason; keep the true positives, narrow the rest.must-block cases missedmust be 0. Verify guards negatively: loosening a pattern untilrm -rf ~passes is worse than any false positive.
Fix pattern: match what is EXECUTED, not text. Anchor destructive targets (/, ~,
$HOME, * as the whole argument), allow --force-with-lease, parse command position
(skip heredoc bodies and quoted ssh payloads) for heavy-task rules.
2026-09-17 on one real machine: 135 blocks in 26 sessions, 129 were false positives; after the
rewrite 6 remain blocked (all true positives) and 0 must-block cases were missed.
Reference implementations: examples/block-destructive.sh and examples/block-heavy-local.sh
(set DEV_SERVER); both parse in python3 and read the hook JSON from stdin.