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.txtsafe from an earlierSP=/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 ascore.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-scriptand--rmt-commandrun code. Their payloads now go
through the packs; a helper dcg cannot verify is denied as
core.filesystem:tar-exec-unverified. rmof a path inside.gitnames the real risk.
rm .git/index.lockis still denied undercore.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 staleindex.locksafely. A command
that also removes a credential keeps the credential reason (#533).git branch -dis reported ascore.git:branch-delete; see Git branch
rules below (#509).
Agent integrations
- Claude Code:
Monitorscripts are judged (#529). Claude Code's
Monitortool runs itscommandas a POSIX shell script. The hook matcher
did not include it, so those scripts never reached dcg.dcg install, the
installers,dcg doctor --fixand hook self-healing now write
Bash|PowerShell|Monitor. - Codex: exact-command review for denials (#537). A Codex denial now
carries its allow-once code inpermissionDecisionReason, 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 anidas 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 carriesserver(), which returns the same hook map
as the namedDcgGuardexport; v2 ignores the extra key. Verified against a
real OpenCode 1.18.33 run. Regenerate the plugin
(dcg install --opencode --force, ordcg update) and restart OpenCode;
dcg doctorreports 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
PreToolUsehooks in~/.claude/settings.jsonand renames Claude Code's
Bashtool toShell. 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 analyzeon 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,corefirst, drawn from zero commands.
The JSON output gainshas_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 --helpnow state thatexit_codeis 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 > ~/.bashrcwas 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
sshscript is judged for the remote host (#534). A
literal script piped or fed tosshor 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/BSDtar,unzip
and7zextracting into an explicit credential or.gitdestination
(including bundledCoptions such astar xfCand unknown member
lists) is denied; list, create and stdout modes are unaffected. - Complete
chmod/chown/chgrpparsing 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 apython3 -heredoc no longer widens
the dialect into a PowerShell scan (#523). - A quoted heredoc piped to
python3,node,rubyorperlis analysed
as that language, not as POSIX shell (#522). - A multi-line
forloop with a quoted|^insed(#524), and a quoted
heredoc appended to a file aftersedread it (#525), are no longer denied
asheredoc.shell:launcher-unverified. - A provable loop variable after a literal path prefix (#526), and provable
/tmpvalues from an&&assignment, a chained variable,$$, or
mktemp -dwith a/tmptemplate (#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-unverifieddenials (#527). --command "<string>"after a variable-pathflowctl gate check/gate receiptis treated as receipt data, not an inline launcher. Only the
fixedflowctlgate grammar is exempt; unknown executables with
--commandstay 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 asheredoc.posix:pipeline-consumer, like the
shell-levelawk '…' | sh(#511). - A single string given to a shell sink is now judged by every pack, not only
the fewrm/gitpayloads the per-language catalogue knows:
perl -e 'system("git push --force origin main")'andsystem("find . -delete")are denied, as are the same strings through Pythonos.system,
Rubysystem, PHPsystem/shell_execand Nodechild_process.execSync.
Perl backtick commands go the same way, andqxis recognised with any
delimiter (qx{…},qx(…),qx[…], …), not onlyqx/…/. Payloads the
catalogue already reports keep theirheredoc.*rule ids (#512).
CLI
dcg config(text and--format json) now reports every effective
[general]setting, includingunverified_decision,update_pin,
check_updates,max_hook_input_bytes,max_command_bytesand
max_findings_per_command, with environment overrides applied (#530).dcg doctorjudges the Claude hook only on the keys dcg owns (type,
command,shell). A hook entry with an extra key Claude Code supports,
such astimeout, is no longer reported as misconfigured, and
dcg install --forcekeeps it. Anasynchook is still an error, because
Claude Code would not wait for its verdict (#528).
Git branch rules
git branch -d/--deletewithout force is now reported as
core.git:branch-delete.-D,--delete --force,-df,-f,-Mand
-Cstay oncore.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 forcore.git:branch-force-deletestill covers-dunless
core.git:branch-deletehas an entry of its own, which then wins. Tools that
readruleIdfrom deny output or history will see the new id for-d.