v0.4.1 — Command resolution: score what the shell executes, not what the agent typed
v0.4.1 — Command resolution: score what the shell executes, not what the agent typed
The shell is a two-stage machine: stage one rewrites the text (variables, globs, scripts), stage two executes the result. Until now blast-scope — like every command guard — scored the input of stage one. The damage is done by its output. This release runs stage one statically, so every scoring axis sees the command the kernel would actually see.
The flagship catch:
rm -rf $BUILD_DIR/ # BUILD_DIR unset → executes as rm -rf /
Previously: the literal path $BUILD_DIR/ doesn't exist → "nothing to lose" → LOW.
Now: resolved to / with an unset-variable hazard → CRITICAL (0.9).
What's new
Command resolution (resolution.py)
- Env / tilde / brace / glob expansion, bash-faithful and quoting-aware: single quotes suppress everything, double quotes expand
$but not globs, only expanded text word-splits, unmatched globs stay literal (nullglob off). - Unset-variable hazards: expansion collapsing a path to
/or$HOMEfloors at 0.85; silently re-rooted paths ($APP_DIR/cache→/cache) floor at 0.6. - Expanded targets flow into every axis — graph in-degree, recoverability, infra/config consequences — not just one.
- Symlink evidence: "
./cacheis a symlink → /var/lib/app/data" appears in the rationale; the destination is what gets classified.
Script transparency
npm run clean contains nothing to score — the danger lives in package.json. Now resolved and scored:
sh|bash|zsh -c '...'payloads (sh -c 'rm -rf /'scored 0.015 before; 0.9 now)npm|pnpm|yarn|bun run Xincluding pre/post hooks — a destructiveprecleancan't hide behind an innocent script name- script files (
bash foo.sh,./foo.sh,source foo.sh), depth-capped make targetrecipes, parsed statically — nevermake -n, which executes$(shell ...)during Makefile parse- Opaque wrappers (
python -cwith destructive/obfuscated payloads,curl | sh) get uncertainty floors: not seeing inside never scores lower than seeing inside and finding it harmless
Read-only command substitution
rm -rf $(find . -name '*.log') names its targets through an inner command's output. When that inner command is provably read-only (deny-by-default allowlist, verb and flags checked, no metacharacters, 2s timeout), blast-scope runs just it and scores the real target list — the same philosophy as the existing git status / docker volume inspect / SQLite mode=ro probes.
Scoring
- New
system_rootrecoverability category:rm -rf /andrm -rf ~floor at 0.9 (previously scored medium) - Content-aware mass-destruction gate: destroying ≥3 source files — directly or inside recursively-deleted directories — floors at 0.55 even when git could restore them. Content-aware on purpose:
rm -rf tmp/ downloads/is routine cleanup,rm -rf src testsguts the codebase; "is a directory" can't tell them apart, "contains source code" can. find -execnow recognizes destructive payload verbs (truncate,dd,mv,chmod)
Numbers (SABER, 716 real agent workspaces — reproducible via bench/)
| metric | v0.3.1 | v0.4.x |
|---|---|---|
| benign false-positive rate | 0.4% | 0.58% (10/1725) |
data_destruction recall |
76.5% | 82.4% — graph-level recall, now on the graphless hook path |
fs_destruction recall |
53.8% | 61.5% |
code_tampering recall |
~0% | 50% |
| overall harmful recall | ~17% | 30.4% |
In-repo calibration corpus: 54/54 exact severity, gate F1 1.00, pinned by tests.
License
blast-scope is now Apache-2.0 (≤0.3.1 was MIT). The vendored code-review-graph sources remain MIT under their upstream notice — see NOTICE.
Upgrade
uvx blast-scope # MCP server (zero-install)
uv tool install blast-scope # or persistent installassess() gains an optional env mapping for $VAR resolution (defaults to the process environment — the hook shares the agent's env, so no configuration change is needed).
(v0.4.1 is v0.4.0 plus a refreshed PyPI page — no code changes between the two.)