Skip to content

Add blast-radius skill - #1776

Open
kishormorol wants to merge 3 commits into
anthropics:mainfrom
kishormorol:add-blast-radius-skill
Open

kishormorol wants to merge 3 commits into
anthropics:mainfrom
kishormorol:add-blast-radius-skill

Conversation

@kishormorol

@kishormorol kishormorol commented Sep 17, 2026 •

Copy link
Copy Markdown

What

blast-radius — a checklist for the moment before a bulk or destructive write:
archiving users, revoking access, deleting rows, mailing a batch.

It covers the gap between a query being right about rows and a bulk operation being
right about the world. The core move is to classify every affected row into
act / exclude / unknown before touching any of them, where unknown is never act,
then check the act bucket for people the rule was never meant to catch.

Why

Every example in the skill is a real near-miss from running an internal platform, not an
invented scenario:

  • "Archive everyone with no account on our chat platform" would have archived the
    research director and a physician hired three weeks earlier. That signal measured
    never joined, not left — of 505 active members, exactly one had ever left.
  • "Revoke the 98 stale permission grants" would have cut off the service account and
    the team leads. Classified, only 18 were genuinely stale.
  • One batch email would have gone to someone who already held an offer, rotating their
    token and invalidating the accept link already in their inbox
    — their status field
    said otherwise.
  • "78 of 80 file weekly reports, so they're working" — 3,037 of 3,392 reports were
    auto-generated drafts.

None of these were caught by being careful. They were caught by splitting a count into
named rows before trusting it.

Contents

skills/blast-radius/
  SKILL.md                    instructions (113 lines)
  LICENSE.txt                 Apache-2.0
  scripts/blast-radius.sh     optional PreToolUse hook

scripts/blast-radius.sh is opt-in and does nothing on its own — hooks load from
settings, not from the skill folder, so SKILL.md carries the settings.json wiring. It
exists because of the trigger problem: a skill about not trusting a query only helps if
it loads, and the agent that would blindly run the query is the one that will not think
to load it. An unfiltered write returns permissionDecision: "ask"; a filtered bulk
write returns the checklist as additionalContext; read-only commands and anything
already flagged --dry-run stay silent. It fails open — missing jq, unrecognised
input or any error exits 0 silently.

The skill is useful without it; drop the script if you would rather skills/ stay
instructions-only.

Notes

  • Layout matches the per-skill convention in skills/ — SKILL.md + LICENSE.txt, with
    aux files under scripts/ as eight other skills do.
  • python3 skills/skill-creator/scripts/quick_validate.py skills/blast-radius → valid.
  • Hook smoke battery: 19/19, covering unbounded writes, filtered bulk writes, read-only
    fast paths, multiple leading VAR=value assignments, # inside string literals,
    malformed input and output shape.
  • Also published standalone at https://github.com/kishormorol/blast-radius

Happy to rename, trim, or restructure to fit house style.

A checklist for the moment before a bulk or destructive write — archiving users,
revoking access, deleting rows, mailing a batch. It covers the gap between a query being
right about rows and a bulk operation being right about the world.

Its core move is to classify every affected row into act / exclude / unknown before
touching any of them, where unknown is never act, and to check the act bucket for people
the rule was never meant to catch.

Every example is a real near-miss from running an internal platform: archiving on "no
account on our chat platform" would have caught the research director, because that
signal measured joining rather than working; revoking 98 apparently-stale permission
grants would have cut off the service account and the team leads, since only 18 were
genuinely stale; and one batch email would have rotated a token and invalidated an
accept link the recipient already held.

Apache-2.0, matching the repo's per-skill LICENSE.txt convention.
… hook

The six ways a signal lies now open the skill rather than sitting inside step one, and
each is named so it travels without the skill loaded: absence is not departure, ghost
activity, the field changed meaning, status lags the artifact, clustered timestamps,
loose matching invents members.

Adds a PreToolUse hook, because a skill about not trusting a query only helps if it
loads — and the agent that would blindly run the query is the one that will not think to
load it. The hook fires on the command instead: ask for writes with no filter at all,
checklist as context for filtered bulk writes and writes inside loops, silent for dry
runs and read-only commands. Fails open on any error.
@98zc5g5jyw-arch

Copy link
Copy Markdown

Thanks — reviewed at head 6fcf410. I extracted the PR tree, byte-verified it against the git blobs, ran the official validator, and put the hook through a real smoke battery. Detailed notes below.

Spec + repo conventions

  • ✅ agentskills validate → Valid skill. name matches the directory, description is 342/1024 chars, license: Apache-2.0 with a bundled LICENSE.txt, SKILL.md is 77 lines (spec recommends < 500).
  • ✅ Folder shape matches sibling convention (e.g. discernment-nudge: SKILL.md + LICENSE.txt). The standalone kishormorol/blast-radius repo is in sync — its SKILL.md and hooks/blast-radius.sh are byte-identical (git object hashes) to this PR's blobs.

Hook smoke battery — 19 cases against the extracted tree (byte-identical to 6fcf410), fed PreToolUse JSON on stdin:

  • Correct warnings (8): deleteMany({}) → permissionDecision: "ask"; filtered updateMany(...), DELETE FROM ... WHERE, forEach(...).delete(, find ... -delete, git push --force → additionalContext; DELETE FROM users; → ask; deleteMany({}) # cleanup → ask.
  • Correct silences (7): read-only grep ...; --dry-run-flagged command; FOO=1 rg ...; empty {}; non-JSON input; PATH without jq; harmless text. Fail-open holds, as documented.
  • Output shape (2): both decision and context outputs parse as valid JSON with hookEventName: "PreToolUse".
  • Two remaining probes are precision edges (below), both non-blocking.

Precision edges in the matcher (suggestions only — both fail in the safe direction):

  1. Multiple leading env assignments lose the read-only fast path. FOO=1 BAR=2 rg -n 'deleteMany' . → warned, while FOO=1 rg ... stays quiet. The strip regex can't re-anchor ^ for the second assignment (first token resolves to BAR=2). Repeating the strip until clean, or a small awk, fixes it.
  2. #-stripping is quote-unaware. node -e "const c=('#fff'); db.users.deleteMany({})" → silent; the scan is truncated at # — which also contradicts the script's own note about not stripping string literals. Stripping only full-line comments (or dropping the strip) avoids the blind spot.

For author / maintainers

  • The PR description is now behind the head commit: it says "No supporting scripts — it is instructions only" and lists "SKILL.md + LICENSE.txt", but the head adds a third file, hooks/blast-radius.sh. Worth updating before merge.
  • hooks/ has no precedent under skills/ in this repo (existing aux dirs: scripts/, shared/, examples/, language dirs). The spec permits extra files, but nothing loads this hook as-shipped — Claude Code hooks are wired via settings, not by dropping a file into a skill folder. A sentence on intended installation would help reviewers; or hold the hook until a repo convention exists.
  • Minor housekeeping: LICENSE.txt is the stock Apache-2.0 text with the appendix placeholder Copyright [yyyy] [name of copyright owner] unfilled; the most common in-repo copy (13 skills) differs only at line 190 (Copyright 2026 Anthropic, PBC.) plus a trailing newline. Flagging in case the merge process normalizes licenses.

The skill content itself reads well — the signal-failure taxonomy and act/exclude/unknown framing are genuinely useful, and it is all instructions. No blocking issues from my side once the description/hook questions above are settled.

…two matcher edges

Review feedback on anthropics#1776.

Move hooks/blast-radius.sh to scripts/blast-radius.sh. `hooks/` had no precedent
under skills/ — eight skills already use scripts/ — and the name implied the file
loads itself, which it does not.

Document that. SKILL.md gains an "Installing the guard as a hook" section with the
settings.json wiring, what each class of command produces, and the fail-open
behaviour, so nobody has to infer the install path from the folder name.

Fix two precision edges in the matcher, both of which failed safe but were wrong:

- Only the first leading VAR=value assignment was stripped, because a sed '^'
  anchor cannot re-match after its own substitution. `FOO=1 BAR=2 rg ...` resolved
  to `BAR=2` and lost the read-only fast path. Scan the fields instead and take the
  first that is not an assignment.
- Stripping from '#' to end of line was quote-unaware, so a '#' inside a string
  literal truncated the scan: `node -e "const c=('#fff'); db.users.deleteMany({})"`
  went silent. It also contradicted the note directly above it about not stripping
  string literals. Drop whole-line comments only.

Normalise LICENSE.txt to the copy the other thirteen skills carry, filling the
`Copyright [yyyy] [name of copyright owner]` appendix placeholder.

Smoke battery: 19/19, including both edges above. quick_validate.py reports valid.

Co-Authored-By: Kishor Morol <[email protected]>
@kishormorol

Copy link
Copy Markdown
Author

Thanks for the detailed pass — all five points addressed in 6d06779.

Hook moved to scripts/ and SKILL.md now carries the settings.json wiring, so the install path isn't left to inference. Both matcher edges fixed: the env-assignment strip is now an awk field scan (a ^ anchor can't re-match after its own substitution, which was the bug), and #-stripping is limited to whole-line comments — you were right that it contradicted the note directly above it. LICENSE normalized to the 13-skill copy. PR description rewritten to match the actual tree.

Re-ran your battery plus the two edges: 19/19.

kishormorol added a commit to kishormorol/blast-radius that referenced this pull request Sep 17, 2026
Keeps this repo in sync with anthropics/skills#1776, where review asked for all
three changes.

Move hooks/blast-radius.sh to scripts/blast-radius.sh. `hooks/` implied the file
loads itself, which it does not — hooks are wired through settings — and upstream
already uses scripts/ for skill aux files. README install snippets updated.

SKILL.md gains an "Installing the guard as a hook" section, so the skill carries the
settings.json wiring itself rather than relying on this README.

Fix two precision edges in the matcher, both of which failed safe but were wrong:

- Only the first leading VAR=value assignment was stripped, because a sed '^' anchor
  cannot re-match after its own substitution. `FOO=1 BAR=2 rg ...` resolved to
  `BAR=2` and lost the read-only fast path. Scan the fields instead and take the
  first that is not an assignment.
- Stripping from '#' to end of line was quote-unaware, so a '#' inside a string
  literal truncated the scan: `node -e "const c=('#fff'); db.users.deleteMany({})"`
  went silent. It also contradicted the note directly above it about not stripping
  string literals. Drop whole-line comments only.

Smoke battery: 19/19, including both edges above.

Co-Authored-By: Kishor Morol <[email protected]>
kishormorol added a commit to kishormorol/skills that referenced this pull request Sep 17, 2026
Keeps blast-radius installable from this fork's marketplace while
anthropics#1776 is still open upstream. The PR branch is
untouched.

Co-Authored-By: Kishor Morol <[email protected]>
@98zc5g5jyw-arch

Copy link
Copy Markdown

Re-verified at head 6d06779 (full 6d067792552b6aa5cd891d7b3d5d16b10bed493a) — all five points addressed. Re-ran the battery plus both edges against the extracted tree (hook byte-verified against the git blob; 21/21 cases, quick_validate.py → Skill is valid!).

✅ Env-assignment strip: FOO=1 BAR=2 rg -n 'deleteMany' . → silent with the awk field scan, while FOO=1 psql -c "DELETE FROM users;" still returns ask — no over-suppression.
✅ # handling: node -e "const c=('#fff'); db.users.deleteMany({})" now returns ask (was silent); rg "a # b" f stays silent and whole-line comment stripping behaves.
✅ PR description now matches the tree: scripts/blast-radius.sh listed, 113 lines (actual 113), scripts/ convention accurate (8 other skills).
✅ hooks/ → scripts/ plus the settings.json wiring section in SKILL.md; no skills/*/hooks/ remains in the head.
✅ LICENSE.txt is byte-identical to the canonical copy (blob 4f881c52d1f72f4cfb720e339e2d35c3058d01a9, shared by 14 files), Copyright 2026 Anthropic, PBC. filled.

Fail-open holds (empty {}, malformed JSON, no jq in PATH → exit 0, silent) and every output parses as JSON. No open items from my side — thanks @kishormorol.

@kishormorol

Copy link
Copy Markdown
Author

Thanks for the thorough re-verification — really appreciate you running the full battery against the extracted tree. Leaving this open for maintainer review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants