Skip to content

v0.4.1 — Command resolution: score what the shell executes, not what the agent typed

Choose a tag to compare

@Atharva-Jayappa Atharva-Jayappa released this 05 Jul 23:59
· 29 commits to master since this release

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 $HOME floors 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: "./cache is 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 X including pre/post hooks — a destructive preclean can't hide behind an innocent script name
  • script files (bash foo.sh, ./foo.sh, source foo.sh), depth-capped
  • make target recipes, parsed statically — never make -n, which executes $(shell ...) during Makefile parse
  • Opaque wrappers (python -c with 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_root recoverability category: rm -rf / and rm -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 tests guts the codebase; "is a directory" can't tell them apart, "contains source code" can.
  • find -exec now 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 install

assess() 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.)