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 lets
eval "$(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/rm
subcommand. - 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 5and
docker compose rm -fsvremove volumes but were allowed when only
containers.composewas enabled. The guards now match adown/rm
short-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
anenv/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.log
works — 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
git
mutators (#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 exactly
heredoc.<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 aC:\…\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.