github Dicklesworthstone/destructive_command_guard v0.15.3

5 hours ago

This release closes commands that ran unguarded (Claude Code's Monitor
tool, wrapper and remote-shell redirects, GNU tar helper programs, archive
extraction into credential or .git paths) and a long list of false
positives reported against 0.15.2.

Claude Code users: refresh the hook after upgrading. The hook matcher is
now Bash|PowerShell|Monitor. Run dcg install (or dcg doctor --fix);
hook self-healing does the same on its next run. Existing Bash or
Bash|PowerShell dcg entries are migrated in place, and unrelated hooks keep
their matchers.

Behaviour changes you may notice

  • A temporary-path variable proof stops at an unmodelled assignment.
    0.15.2 proved > $SP/out.txt safe from an earlier SP=/tmp/... even when
    a later assignment took its value from a command dcg does not model
    (N=$(python3 -c ...), N=$(basename "$BIN" | cut -c1-16)). Such
    redirects are now denied as core.filesystem:redirect-truncate-dynamic-path.
    Redirect before that assignment, or write the literal path. In a replay of
    6,000 real agent commands this changed three verdicts from allow to deny;
    the #536 fixes below changed one from deny to allow.
  • GNU tar helper programs are judged. --checkpoint-action=exec=,
    --to-command, --use-compress-program/-I, --info-script/-F,
    --new-volume-script and --rmt-command run code. Their payloads now go
    through the packs; a helper dcg cannot verify is denied as
    core.filesystem:tar-exec-unverified.
  • rm of a path inside .git names the real risk.
    rm .git/index.lock is still denied under core.filesystem:rm-protected-file,
    but the reason now says it deletes repository state, not a credential or
    login-startup file, and how to clear a stale index.lock safely. A command
    that also removes a credential keeps the credential reason (#533).
  • git branch -d is reported as core.git:branch-delete; see Git branch
    rules below (#509).

Agent integrations

  • Claude Code: Monitor scripts are judged (#529). Claude Code's
    Monitor tool runs its command as a POSIX shell script. The hook matcher
    did not include it, so those scripts never reached dcg. dcg install, the
    installers, dcg doctor --fix and hook self-healing now write
    Bash|PowerShell|Monitor.
  • Codex: exact-command review for denials (#537). A Codex denial now
    carries its allow-once code in permissionDecisionReason, keeping Codex's
    three-field denial JSON. The grant matches only the exact original request,
    is single-use, and cannot be consumed by a nested payload or an altered
    wrapper, substitution or whitespace.
  • pi: the recipe evaluates with --dialect posix (#532). Without it,
    pi's commands were read with zsh-only rules and some were denied falsely.
  • OpenCode: the plugin loads again (#516). OpenCode 1.3.4 and later read
    a default export that has an id as a plugin module and refuse it without
    server(). The plugin generated by 0.15.0 through 0.15.2 exported
    { id, setup } for OpenCode v2, so current OpenCode (1.18.33 in the report)
    refused the whole file at startup and ran every bash command unguarded. The
    default export now also carries server(), which returns the same hook map
    as the named DcgGuard export; v2 ignores the extra key. Verified against a
    real OpenCode 1.18.33 run. Regenerate the plugin
    (dcg install --opencode --force, or dcg update) and restart OpenCode;
    dcg doctor reports an old plugin as outdated.
  • Cursor: the bridge fails closed (#517). The beforeShellExecution
    bridge allowed every command it could not check. On Windows that was every
    command: cursor-agent sends a UTF-8 BOM, Windows PowerShell 5.1 decoded it
    with the OEM code page, the JSON parse failed, and the bridge allowed. Both
    bridges (PowerShell and the Unix Python one) now read stdin as bytes and
    drop a BOM, send dcg UTF-8, and ask dcg for an explicit allow, so an
    unreadable payload or a dcg that ran without a verdict (killed, timed out,
    non-zero exit, no answer) is denied. DCG_BRIDGE_CRASH_DECISION=allow
    restores fail-open for those; a dcg that cannot be started at all is still
    allowed, with a notice on stderr. Re-run the installer to regenerate the
    bridge.
  • Cursor: Claude Code hooks see Shell (#518). Cursor runs the
    PreToolUse hooks in ~/.claude/settings.json and renames Claude Code's
    Bash tool to Shell. dcg did not know the name and allowed every such
    command; it now judges them and answers in the Claude shape Cursor reads.
    The other supported agents' tool names were audited and needed no change.

Command history

  • dcg history analyze on an empty history now says there is nothing to
    analyze and how to enable [history], instead of green "no coverage gaps"
    checks and advice to disable packs, core first, drawn from zero commands.
    The JSON output gains has_data. With data, a pack that never matched is
    still listed but is no longer recommended for removal: a guard pack that
    stays quiet is working (#513).
  • History rows now record the machine's hostname; it was always NULL (#514).
  • The docs and dcg history analyze --help now state that exit_code is NULL
    on every row the hook writes, because the hook runs before the command and
    cannot know how it ended. NULL means unknown, not success (#515).

Commands that ran unguarded

  • A wrapper no longer swallows a redirect (#531, #521).
    nice -n > ~/.bashrc was allowed because the wrapper parser read > as
    the value of -n; it is now judged as the credential-file write it is. A
    wrapper option with no operand (nice -n >~/x) panicked; it is now
    rejected without a panic.
  • A redirect in an ssh script is judged for the remote host (#534). A
    literal script piped or fed to ssh or to an interactive container exec
    shell was judged against the local disk, so a local file's absence could
    prove a remote overwrite safe. Such scripts are now judged in the receiving
    environment's filesystem scope; an unknown producer or unbounded nested
    dispatch fails closed.
  • Local files are not evidence after a wrapper changes context. Input
    files opened after a recognised wrapper changes directory, user, home or
    environment (sudo -u, env -C, remote and container shells) were checked
    against the caller's files. Those inputs are now treated as unverified.
  • Archive extraction into credentials or .git. GNU/BSD tar, unzip
    and 7z extracting into an explicit credential or .git destination
    (including bundled C options such as tar xfC and unknown member
    lists) is denied; list, create and stdout modes are unaffected.
  • Complete chmod/chown/chgrp parsing in the permissions pack. The
    prefix-only exemption could hide a later recursive flag, a protected
    target, or a different operation whose option value contained safe-looking
    text. Every option and target is now checked.
  • Windows Git redirects keep their own rules (#492). Allowlisting
    redirect-truncate-dynamic-path, or one Git redirect, could conceal a
    separate Git or credential write spelled with Windows separators in the
    same command. Every truncate/append hit is now kept and judged under its
    own rule.
  • An executable whose argument count cannot be bounded now fails closed
    instead of having its arguments shifted.

False positives fixed

  • A JavaScript arrow function (=> !x) in a quoted heredoc is not read as a
    redirect (#519).
  • A Python heredoc containing a regex no longer switches macOS commands to
    Windows parsing (#520), and ^ in a python3 - heredoc no longer widens
    the dialect into a PowerShell scan (#523).
  • A quoted heredoc piped to python3, node, ruby or perl is analysed
    as that language, not as POSIX shell (#522).
  • A multi-line for loop with a quoted |^ in sed (#524), and a quoted
    heredoc appended to a file after sed read it (#525), are no longer denied
    as heredoc.shell:launcher-unverified.
  • A provable loop variable after a literal path prefix (#526), and provable
    /tmp values from an && assignment, a chained variable, $$, or
    mktemp -d with a /tmp template (#536), satisfy
    redirect-truncate-dynamic-path. Resolution is bounded (eight binding
    levels, 4096 expanded bytes, 256 segments) and still denies traversal,
    protected names and mutated bindings.
  • An embedded process call with a non-literal argument keeps its words in
    place; dropping it shifted the rest and produced false
    core.git:git-alias-semantic-unverified denials (#527).
  • --command "<string>" after a variable-path flowctl gate check/gate receipt is treated as receipt data, not an inline launcher. Only the
    fixed flowctl gate grammar is exempt; unknown executables with
    --command stay denied (#538).

Embedded code and heredocs

  • An inline-shell launcher quoted as prose inside another command's argument
    (tracker comment 1 "example: bash -c 'git reset --hard' is refused") is no
    longer judged as if it ran. A launcher that opens the quoted text, follows a
    separator or wrapper inside it, sits in a substitution, or is in a string
    that eval/ssh/watch run is still judged (#510).
  • awk printing into a shell that reads its stdin (print "git reset --hard" | "sh") now has the printed text judged as the command it is, including
    through a variable the program assigns one literal. A computed print into
    such a shell is denied as heredoc.posix:pipeline-consumer, like the
    shell-level awk '…' | sh (#511).
  • A single string given to a shell sink is now judged by every pack, not only
    the few rm/git payloads the per-language catalogue knows:
    perl -e 'system("git push --force origin main")' and system("find . -delete") are denied, as are the same strings through Python os.system,
    Ruby system, PHP system/shell_exec and Node child_process.execSync.
    Perl backtick commands go the same way, and qx is recognised with any
    delimiter (qx{…}, qx(…), qx[…], …), not only qx/…/. Payloads the
    catalogue already reports keep their heredoc.* rule ids (#512).

CLI

  • dcg config (text and --format json) now reports every effective
    [general] setting, including unverified_decision, update_pin,
    check_updates, max_hook_input_bytes, max_command_bytes and
    max_findings_per_command, with environment overrides applied (#530).
  • dcg doctor judges the Claude hook only on the keys dcg owns (type,
    command, shell). A hook entry with an extra key Claude Code supports,
    such as timeout, is no longer reported as misconfigured, and
    dcg install --force keeps it. An async hook is still an error, because
    Claude Code would not wait for its verdict (#528).

Git branch rules

  • git branch -d / --delete without force is now reported as
    core.git:branch-delete. -D, --delete --force, -df, -f, -M and
    -C stay on core.git:branch-force-delete. Both are still denied by
    default. Setting [policy.rules] "core.git:branch-delete" = "ask" lets an
    agent propose merged-branch cleanup for approval while forced deletion stays
    denied, which no configuration could express before (#509).
  • Existing configuration keeps its meaning: a [policy.rules] or allowlist
    entry for core.git:branch-force-delete still covers -d unless
    core.git:branch-delete has an entry of its own, which then wins. Tools that
    read ruleId from deny output or history will see the new id for -d.

Don't miss a new destructive_command_guard release

NewReleases is sending notifications on new releases.