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 ascore.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'sMonitortool 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 writeBash|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
idas a plugin module and refuse it withoutserver(). 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
beforeShellExecutionbridge 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=allowrestores 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 thePreToolUsehooks in~/.claude/settings.jsonand renames Claude Code'sBashtool 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,unzipand7zextracting 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,$$, ormktemp -dwith a/tmptemplate (#536), satisfyredirect-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 few
rm/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_bytesandmax_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, anddcg 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 ascore.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-dunlesscore.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.