A correctness and hardening release from a full issue-triage pass. It closes a
class of fail-closed bypasses in the #261 init-idiom warn path, restores four
compose volume/force detections that global flags or combined short-flag
clusters could slip past, and removes several false positives that were blocking
ordinary safe workflows. No change to the evaluation hot path's asymptotics; a
profiling campaign confirmed dcg already runs 35–90× inside its hook budget and
produced a committed negative-evidence ledger rather than speculative tuning.
Security
eval/sourceinit-idiom WARN can no longer be used to smuggle a destructive command (#261). The curated init-idiom downgrade (which letseval "$(ssh-agent -s)"andsource <(kubectl completion bash)warn instead of hard-deny) was found across four adversarial fresh-eyes rounds to let a destructive command run with only a warning by attaching to, or hiding behind, a curated idiom. The warn is now locked to an allowlist-shaped box: it applies only at command-substitution depth 0 (a curated idiom reached through a nested substitution or heredoc recursion hard-denies at the consumption point), only when the whole command is a single POSIX segment (a trailing; rm -rf ~or any multi-segment/nested form keeps the hard denial), and only when the bytes before theeval/sourceword are statically inert (no(), backtick,<,>,|,&,;, or newline) — closing the leading-assignment, command-substitution, and process-substitution holes that a denylist kept missing.- Compose volume/force destruction is caught past global flags (#276).
containers.compose:down-volumes(anddown-rmi-all,rm-volumes,rm-force) required the subcommand to immediately followcompose, so the most ordinary dangerous form —docker compose -f docker-compose.prod.yml down -v, which destroys named volumes (database data, uploads) — was allowed. Each pattern now skips global options and their values with a bounded, option-only walker before the subcommand, without letting a service name or a-f down.ymlfilename value masquerade as thedown/rmsubcommand. - Combined short-flag clusters are caught (fresh-eyes finding). The
volume/force guards required a standalone single-flag form, so pflag-style
combined clusters slipped through:
docker compose down -vt 5anddocker compose rm -fsvremove volumes but were allowed when onlycontainers.composewas enabled. The guards now match adown/rmshort-flag cluster containing the target letter, enumerated to each subcommand's real short flags (keeping matching linear — no lookbehind, no ReDoS — and not firing on av/finside a long option like--verbose). - Execution-frontend strip bail no longer weakens live hooks (#260). When
an
env/nice/timeout-style frontend strip fails on dynamic options, the latergitwords now stay scan-required, so a live PreToolUse hook is never weaker thandcg teston the same command.
Fixed
- Fixed
xargs/sh -ctemplates that only consume record fields as data are no longer denied (#272). Positional-parameter masking now walks the template with quote- and escape-awareness, masking bare$N/${N}records while still refusing$@/$*aggregates,${!indirect},BASH_ARGV/ARGC, and any${N…}form with expansion modifiers. Two quote-scanner false negatives were fixed alongside it: an apostrophe inside a double-quoted word can no longer disable subsequent$Nmasking, and ANSI-C$'…'strings are skipped atomically so their escaped quotes cannot corrupt quote-state tracking and hide a$0. - Safe variable redirects into a same-command
$(mktemp)are allowed (#275). A synthetic scratch stand-in proves baremktemp/mktemp -dtargets (-d/-qonly; not-p/-t/-uor explicit templates), and a literal suffix after the closing quote folds into the proven target so"$D"/out.logworks — while traversal and non-literal continuations still deny. - Dynamic
git branchhazards get their own remediation (#274). An unresolvable dynamic word that may expand into a delete/force flag now attributes tocore.git:branch-dynamic-tokenwith a suggestion to quote the name or add--, distinct from provenbranch-force-delete. - PowerShell expression statements are no longer misread as dynamic
gitmutators (#273). A statement beginning with a quote, variable, array, or hashtable literal (with no$( )/@( )subexpression) cannot invoke its first token asgit, so it is not treated as a dynamic branch mutation. - Printed
heredoc.<family>rule ids can now be allowlisted (#162/#261). Embedded-code AST denials and#261unverifiable-sink rules attach synthetic ids likeheredoc.posix:eval-dynamic; pack-id validation now accepts exactlyheredoc.<family>, so thedcg allowlist add …command dcg itself prints actually validates (bareheredocstays invalid). - Cross-OS hook paths are diagnosed, not silently missed (#264). The CLI now
names a
C:\…\dcg.exehook path on Unix (and the reverse), so a cc-switch–style cross-OSsettings.jsonrematerialization is reported instead of a confusing "hook not found".
Internal
- Added a measurement-only
release-perfcargo profile (frame pointers + line tables forsamply) and committed the cold-start profiling artifacts: baseline fingerprint, per-phase differential attribution, a ranked hotspot table, and a negative-evidence ledger. The disciplined outcome: no isomorphism-safe optimization clears the Score ≥ 2.0 bar at acceptable false-negative risk on a security matcher, so the evaluation path is unchanged.
Install: curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/v0.9.4/install.sh | bash
All 6 platform archives are signed with minisign (key 69B3955C8D2E62A8) and cosign, with SLSA provenance and a SHA256SUMS manifest.