This release closes commands that ran unguarded (writes hidden by IFS
splitting, by shell variables or by git rev-parse paths, redirections
written against the command word, upper-case program names, ssh_config
command options, OpenCode v2's shell tool, and Python, Deno, Bun and awk
process calls dcg did not read) and adds Amp, Muse Code and Factory Droid
integrations. It also fixes a long list of false positives reported against
0.15.3, most of them in quoted Python and Node heredocs, and makes a
configuration file that fails to load block commands instead of being
skipped.
A configuration file that fails to load now blocks every shell command
(#568). In 0.15.3 a user or system config.toml that did not parse, held
an invalid policy value or could not be read was skipped, and dcg ran with
its defaults, without that file's block rules. Now the hook denies every
shell command with a reason that names the file and points to
dcg doctor --strict, and dcg test exits 3 with a configuration_error.
DCG_FAIL_CLOSED and DCG_UNVERIFIED_DECISION cannot open it up again. A
DCG_CONFIG that names a missing file is handled the same way. A malformed
repository .dcg.toml that dcg found on its own is still not fatal. After
upgrading, run dcg test 'git status' once to confirm your configuration
loads (5b1d5bf).
OpenCode and Oh My Pi users: regenerate the plugin or bridge after
upgrading. OpenCode v2 calls its shell tool shell, which the plugin
generated by earlier versions skipped, so every v2 shell command ran
unguarded (#547). The Oh My Pi bridge treated a dcg exit without a readable
verdict as an allow (#564). Both fixes live in the generated files. Run
dcg install --opencode --force or dcg install --omp --force, or let
dcg update do it (it does when its installer detects the agent, unless you
pass --no-configure), then restart the agent (e31cc14, 450a289).
Claude Code users who set CLAUDE_CONFIG_DIR: run dcg install or
dcg doctor --fix after upgrading (#541). Earlier versions never read that
variable and put the hook in ~/.claude/settings.json, so the profile
Claude Code actually used may have no dcg hook, and nothing reported it
(65004aa, 4911e6f).
Custom pack authors: check your packs after upgrading. A rule that can
only match zero width now stops its pack from loading, safe patterns now
match the original command with its environment assignments and wrappers,
and packs.disabled now turns off packs loaded from custom_paths. See
Behaviour changes below (11b3f00, e0554cd, c490820).
Two heredoc checks are looser than in 0.15.3, on purpose. Quoted
interpreter heredocs are now judged in the interpreter's language, and a
substitution in an unquoted heredoc body is judged as the command inside it.
Some text that 0.15.3 refused because it could not verify it is now allowed.
See Behaviour changes and Known issues below (f9e8748, e4e0629).
Behaviour changes you may notice
- Quoted interpreter heredocs are judged in the interpreter's language
(#544, #560). 0.15.3 also read a body given topython3 -,node,ruby
orperlas shell, and refused what it could not prove harmless. When dcg
proves the interpreter reads the body as its program, that shell reading is
gone, so a backtick or$(…)in the body is no longer a substitution. This
loosens fail-closed behaviour. In a sample of the rows this change moved,
160 went from deny to allow, 9 from allow to indeterminate and 2 from deny
to indeterminate; those are counts from the sample, not totals. In a wider
synthetic comparison with 0.15.3, across about 40,000 heredoc forms, 847
that 0.15.3 denied are allowed here: 153 are 144 unquoted and 9 quoted
bodies whose 0.15.3 rule washeredoc.shell:launcher-unverified(the
unquoted ones are the next entry), 76 hold commands that are allowed when
typed plainly, and 618 cannot run what they contain, because Python rejects
the body or the text sits in a JavaScript template literal with no${}. A
pythonorpython2body with a backtick expression comes back
indeterminate, because Python 2 evaluates it. Of the forms 0.15.3 denied,
138 come backindeterminate: 136 with a python receiver and 2 Node
bodies. Dynamic execution in these bodies has a new check, with limits; see
Embedded code and heredocs (f9e8748). The CHANGELOG.md in the v0.16.0 tag
gives the pre-final (o3s) counts; these are the counts for the released
build. - A substitution in an unquoted heredoc body is judged as the command inside
it. Ingit commit -F - <<EOForcat <<EOF, a$(…)in the body really
runs. 0.15.3 denied every such body asheredoc.shell:launcher-unverified.
This release judges the inner command as if it were typed plainly, which
loosens fail-closed behaviour: a substitution whose command dcg cannot
resolve, or one that is harmless when typed plainly, is now allowed. The
same substitution ingit commit -m "$(…)"was already allowed in 0.15.3,
which explains the inconsistency and does not make the change safer. Still
denied: a destructive command dcg recognises in the substitution
(rm -rf /, a home or relative path,cd /etc && rm -rf *),sh -c,
python3 -c,eval,curl | sh, and any substitution in a body fed to
shorbash. Three of dcg's own tests still expect the old deny; see
Known issues (e4e0629). - Each command is also read without its redirections. The shell removes
a redirection from argv wherever it stands, but packs read it as a word.
Written against the command word or before it, a redirection took the
executable position and hid the command, sogit>/dev/null reset --hard
was allowed; written after an operand, it ended the command early. Pack
matching now also reads every command with its redirections blanked, under
POSIX, Cmd and PowerShell syntax, and a denial from either reading wins.
Where a command's input comes from (a heredoc fed topsql, for example)
is still read from the full command. In a replay of about 79,000 commands
this changed 58 verdicts, all from allow to deny; six were agent commands
whosegitcall took an unquoted variable and ended in a redirection,
which the dynamic-token rule could not see before (f8e60ac, a43e38e,
9b06d94, cfca5d4, 90648c4). - Shell payloads without a pack keyword are judged whole (#551). A
bash -c,sh -c, ssh or heredoc script went back through the evaluator
only when it contained a pack keyword, so$G -D mainwas denied while
bash -c '$G -D main'was allowed, andsh -c 'eval "$X"'got through
whileeval "$X"is denied asheredoc.posix:eval-dynamic. Such a script
is now evaluated once, whole, as its text is at top level. Two results
change beyond the issue: a keyword-less here-string to an unknown program
(mytool <<< "$a -d $b") now gets the Bash reading and is denied, and awk
printing intosh -c "eval $(cat)"is reported as
heredoc.posix:eval-dynamicinstead ofcore.git:reset-hard(57515f5). - An unquoted brace list voids the home-directory
mvexemptions. Bash
expands{a,b}beforemvruns, so the exemption checked a different
path than the one moved:mv ~/{..,a}/{..,b}/etc ~/Documents/x(which
moves/etc) andmv ~/{Documents,.ssh}/id_rsa ~/Documents/ywere
allowed. An unquoted{now sends the move to
mv-sensitive-source-root-home, for the Trash moves too. Ordinary brace
lists such asmv ~/Documents/{a,b}.txt ~/Documents/dir/are denied as a
result; spell the operands out. Quoted braces are literal and stay allowed
(b9ecd7d, 414d1d7). - Custom safe patterns match the original command (#598). A safe pattern
used to see the normalized command, with environment assignments, wrappers
and executable paths stripped, so an exemption for
/usr/bin/python3 -I /opt/tools/run_check.py --checkalso covered it with
PATH=/somewherein front, or underenvorsudo. Safe patterns now
match the original segment; destructive patterns still read the normalized
text, and custom destructive patterns read the original as well. A nested
command (bash -c '…') inherits a custom exemption only when a safe
pattern covers the whole carrier, and an earlier statement that can change
shell state (export PATH=…;) withholds a later segment's exemption. Put
the exact wrapper or assignment in the pattern when it is intended; see
docs/custom-packs.md. Built-in previews that dcg proves from the
command's arguments, such askubectl … --dry-run=clientand
helm uninstall … --dry-run, still hold behindsudo,envor an
assignment (e0554cd, 861727a). packs.disabledturns off packs loaded fromcustom_paths(#588).
Every loaded external pack was enabled regardless, so a disabled custom
pack kept denying commands, anddcg packs --enabled,dcg configand
dcg doctorcounted it. External packs now follow the built-in rule:
loading enables a pack,packs.disableddisables it by exact ID or by
category (a bare prefix does not), and an agent profile'sextra_packsand
disabled_packsapply on top.dcg test --with-packsstill re-enables one
for a single run (c490820).- A custom rule that can only match zero width is rejected (#558). A
pattern made only of lookarounds and anchors validated but never matched
any text, so a pack built on one guarded nothing. Such a pattern now fails
validation when the pack loads, with an error asking for a consuming match,
and the pack is not loaded. Patterns with any consuming part, including
nullable ones and ones containing literal punctuation, are unaffected
(11b3f00, a4f1fd0). - Escape-free tagged template literals are judged as shell. zx, Bun Shell
and execa run a$template as a shell command, so
$`echo $(git reset --hard)`through zx innode -ewas allowed. A
tagged template with no backslash in it is now read as a shell command
line. The cost, failing closed: a non-shell tag whose text looks
destructive as shell, such ashtml`<p>$(document).ready()</p>`, is
denied asredirect-truncate-dynamic-path. Templates with escapes
(String.rawwith escaped backticks) keep their earlier treatment
(fce0709). - A harmless Perl heredoc too large for the hook input is asked about. A
quoted heredoc of about 320 KB given toperl -, with no dynamic code in
it, makes the hook input larger thangeneral.max_hook_input_bytes(256 KiB
by default). With the default configuration, 0.15.3 allowed it with an audit
warning; this release answersask. The same body given topython3 -,
node -orruby -is still allowed; see Known issues.
Agent integrations
- Amp: new plugin,
dcg install --amp(#577). Amp runs tools without
asking.dcg install --ampwrites a dcg-owneddcg-guard.jsto
${XDG_CONFIG_HOME:-~/.config}/amp/plugins/(or.amp/plugins/with
--project), refreshing a stale dcg copy and refusing to overwrite a
user's file;dcg uninstall --ampremoves it. The plugin checks every
shell command a tool call carries, rejects the call on deny, opens Amp's
confirmation dialog on ask, and rejects when dcg gives no verdict unless
DCG_BRIDGE_CRASH_DECISION=allow.--agent ampselects[agents.amp],
anddcg doctorreports anamp_plugincheck. Reload Amp's plugins after
installing (9d964c1). - Muse Code: hook support,
dcg install --muse(#572).dcg install --museadds a matcher group for Muse'sshell,exec_commandandbash
tools to${XDG_CONFIG_HOME:-~/.config}/muse/settings.json, keeping
schema_version(adding1when missing) and every other key;
dcg uninstall --museremoves it anddcg doctorreports amuse_hook
check. The hook command carries--agent muse, which selects
[agents.muse]; Muse's Codex-shaped payload used to select
[agents.codex-cli]. An unreadable payload or a shell call without a
command is denied. The installers do not configure Muse yet, so run
dcg install --museonce and start a new session. For every agent, a tool
namedexec_commandis now a shell tool and itscmdargument is checked
next tocommand; Muse'sexec_commandcalls ran unchecked before
(54a755d). - Factory Droid:
Executehook,dcg install --droid(#617).
dcg install --droidregisters dcg for Droid'sExecutetool at user
scope, or with--projectin the project's.factorydirectory (the Git
root, or the current directory outside Git). It picks among the hooks and
settings files Factory documents and keeps other hooks, host options,
symlinks and permissions. The hook command carries--agent droid, which
selects[agents.droid].install.shandinstall.ps1detect Droid and
run this installer, and the uninstallers remove it.dcg doctorchecks the
Executematcher, the active file and the executable path. Commands that
Droid's Script mode runs through nestedExecutecalls are not verified;
seedocs/agents.md(efc1092). - OpenCode: the plugin guards v2's
shelltool (#547). OpenCode v2
renamed its shell tool frombashtoshell, and the plugin returned
early for any name butbash, so every v2 shell command ran unguarded. The
plugin now guardsbashandshell, checks any other tool that carries a
commandargument (with a warning on stderr), blocks a shell call whose
command is missing or empty, and fails loudly when OpenCode's v2 hook API
is missing. Regenerate the plugin, as described above (e31cc14). - Oh My Pi: a missing verdict follows the unverified policy (#563,
#564). The generated bridge allowed a command when dcg exited 0 or 2
without a readable allow verdict. Empty, malformed or unknown output is now
reported and handled byDCG_UNVERIFIED_DECISION. The bridge also gives
dcg 25 seconds instead of 30, so it answers before Oh My Pi's own 30-second
tool-call limit. Regenerate the bridge, as described above (450a289). - Antigravity: denials match its strict output contract (#573).
Antigravity CLI 1.4.0 parses hook output strictly and rejected dcg's
denials because of the extrasystemMessage,ruleIdandremediation
fields. A denial is now exactly{"decision":"deny","reason":…}on stdout
with exit code 0; the rule and the allow-once guidance stay inreason.
Warnings go to stderr with empty stdout, leaving the permission decision to
Antigravity (16b4225). - A command carried in several argument keys is checked in every key.
When a hook payload had more than one command-bearing key (Antigravity's
commandandCommandLine, or a stringifiedargsobject), a harmless or
unfinished value in one could hide a destructive command in another. Each
distinct key is now evaluated. Native Antigravity commands are also
recovered from malformed or oversized JSON (10bc819). - Claude Code:
CLAUDE_CONFIG_DIRis honoured (#541).dcg install,
dcg uninstall,dcg doctor, hook self-healing and both installers now
use$CLAUDE_CONFIG_DIR/settings.jsonwhen the variable is set and
non-empty (a leading~is expanded, a relative path is resolved from the
current directory). Doctor names the file it checked,--strictfails when
that profile has no protection, and--fixinstalls into it.
install --projectstill writes the repository's.claude/settings.json.
Grok's Claude compatibility layer still reads~/.claude/settings.json;
usedcg install --grokfor Grok (65004aa, f922413, 4911e6f). - Codex:
dcg doctorfollowsCODEX_HOME(#539). Doctor always read
~/.codex/hooks.jsonand~/.codex/config.toml, so with a custom
CODEX_HOMEit reported a registered hook as missing and--fixedited
the wrong file. Both files now come fromCODEX_HOMEwhen it is set, and
messages name the real path (5683b5d). - Installs and repairs keep your hook settings (#582, #345). Fields of
dcg's hook entry that dcg does not own, such astimeoutand
onFailure: "block", were dropped by the shell and PowerShell installers,
bydcg install --forcewhen they sat on a later duplicate entry, and by
the Grok repair, which overwrote the whole file. Every install and repair
path (Claude Code, Codex, Gemini, Posit Assistant, Grok, Reasonix, Crush,
Muse, Antigravity) now refreshes onlytype,commandandshell, merges
compatible fields from duplicate entries, and refuses conflicting ones
without writing. Settings with duplicate JSON keys are refused instead of
resolved silently. Writes are atomic and keep ownership, mode, symlinks
and, on Linux, ACLs and extended attributes. Repeated installs leave a
current file byte-identical (c64fc94, 2250ddb, 2074df7, 88a1ced,
27c9460, 05b2414, 53e4dc3). - MCP:
check_commandtakes adialect. The MCP server's
check_commandtool acceptsdialect(posix,powershell,cmdor
unknown) and echoes the dialect it used; an invalid value is a tool error
instead of being ignored. Without it the check keeps the conservative
multi-shellunknownreading (b49653a).
Commands that ran unguarded
- Writes hidden by
IFSfield splitting (#558, #565).
S=/tmp/a@/etc/shadow; IFS=@; echo x | tee $Swrites/etc/shadow, but
dcg read$Sas one harmless path. Destinations oftee,cp,mv,
dd,install,ln,sed -iandperl -ithat depend on a rebound
IFSare now split the way the shell splits them before the credential and
Git-internals rules run. That coversexport,printf -vand%n,
redirected or background assignments, brace-expanded exports, line
continuations, wrappers such asenv,command,niceandstdbuf,
tee.exespellings, and"${S:+$@}". A destination dcg cannot resolve is
denied under both rule families, and a grant for one family does not hide
the other (231ca34, 1850ae1, 9ce33b7, e2ea6b1, e77f026, 882eea6,
4de45ea, 191cad7). - Writes whose destination comes from a variable or
git rev-parse(#609,
#613).f=~/.ssh/authorized_keys; echo x | tee -a $fand
cp source "$(git rev-parse --git-path hooks)/pre-commit"were allowed.
dcg now follows literal values bound earlier in the same command and the
git rev-parsepath queries it recognises (--git-dir,
--git-common-dir,--absolute-git-dir,--git-path), including linked
worktrees,core.hooksPath,-Candbash -cbodies, and judges the
resulting path under the existing credential, Git-internals and redirect
rules. The query is never run. Paths such as~/.ssh/known_hostsstay
allowed (12036e9, 4850e2d, e4e0629). - A variable used as the command runs what it holds (#540).
X='git reset --hard'; $Xwas read as one quoted program name, so the Git
rule never saw it. Literal assignments now expand into argv the way the
shell splits them;"$X"stays one argument. Splitting, globbing or
interpreter flags dcg cannot predict require review, and rule grants and
source spans stay with the command that needs them (29fb1a2). - Program names in another case are judged like lower case. On macOS and
Windows, whose filesystems ignore case by default,GIT reset --hardruns
git, but the packs and the shell analysis compared lower-case names and
allowed it. Every command is now also read with recognised program names
folded to lower case (through quotes, backslashes, directories and a.exe
suffix), on every platform; arguments keep their case, and a heredoc
delimiter is never folded. Allowlist entries and grants still bind to the
command as written (5ce8e2b, 53199ed). - A
#inside a word no longer hides the rest of the line. dcg took a
#after a blank inside${…}, after a carriage return or form feed, or
right after a substitution's closing)for a comment, so in
echo ${x:- #}; rm -rf /the command after it went unchecked. A#now
starts a comment only where the shell starts one. Heredoc bodies also end
where the shell ends them: an unclosed quote,${or backquote in a body
line (echo it's) no longer swallows the commands after the terminator,
and an unquoted delimiter joined across a backslash-newline ends the body
as Bash does (786b5ae, 90648c4, d56aa46). - A quoted
;,|or&no longer hides later operands. Filesystem
rules that stay inside one command stopped at the separator byte even when
it was quoted, somv "a & b" /etc /tmp/x,find "a;b" / -deleteand
rsync -a --delete "a & b" /tmp/src/ /etc/were allowed while the same
commands with a quoted space were denied. Those rules now run again with
quoted and escaped separators masked; the extra pass can only add denials
(2350e9b). - ssh_config command options are judged.
-o ProxyCommand=…,
LocalCommand,KnownHostsCommandandRemoteCommandonssh,scpand
sftpcarry command lines that were never evaluated. They are now judged
like the remote command, in every spelling OpenSSH accepts; a value that is
only an expansion is denied as unverifiable (6bd0bed). - GNU tar helpers inside shell groups (#540). Helper discovery now
reachestarinvocations inside( … )and{ …; }with their original
context. A write or state change earlier in the group, such as a redirect
that overwrites a PostgreSQL checkpoint helper's input, stops the helper
from relying on stale local-file evidence (7c50fa7). xargs -I{}keeps the command it runs. The-Iplaceholder was
blanked as data, so inxargs -I{} chmod 777 {}the rules tookchmodfor
the placeholder value and lost the executed command, while the harmless
xargs -I{} echo chmod 777 {}was attributed tochmod. The placeholder
is no longer masked, andxargs -I{} unlink {}is judged like
xargs unlink(b91648a).- A safety regex that runs out of work is never an allow. When a rule's
backtracking budget ran out, warning or log policy, confidence scoring or
the rebase-recovery exemption could still let the command through. Such a
failure is now incomplete analysis: a proven denial elsewhere still denies,
and otherwise the command is reviewed or blocked under the unverified
policy (553263f). system.servicesblocks kernel module unloads. An agent "fixed" an
NVIDIA driver/library mismatch with
sudo modprobe -r nvidia_uvm nvidia_drm nvidia_modeset nvidiaover ssh;
the kernel oopsed in the framebuffer console teardown, and every new pty
andps auxhung until the host was rebooted. With the pack enabled, three
High rules denymodprobe -r/--remove(including clusters such as
-rf),rmmod, and an unquoted remote unload
(ssh host sudo modprobe -r x). The words as data
(grep -rn "modprobe -r",echo rmmod) are not matched (81fb9a3).system.permissions: path aliases,setfacland special bits. With
the pack enabled, recursivechmod,chownandchgrpof a protected
system or home path spelled with.,..or repeated/(/etc/.,
//etc,/usr/../etc) are denied like the plain path; a test run
against the previous parser found 21 such bypasses.setfaclis parsed in full, in both GNU
and BSD option order, so a later--testor--helpdoes not undo an
earlier protected recursive change and an ACL value is not taken for a
target. Symbolic setuid and setgid modes are recognised in any letter
order, an unresolved later argument keeps the alias and special-bit
denials, and a grant for one rule leaves the others in force (b1aa290,
e272047, 627a427, deae3ee).careful_company_running_windows.guardrails: dcg's own state is harder
to weaken (#610). With the pack enabled,dcg-policy-self-weakeningnow
also catches shell writes to the allow-once and pending-exception stores
(redirects,tee,cp/mv/install,sed -i,truncate,dd),
including configured store paths and literalcdchanges; reads and
unrelated writes stay allowed. Global options written before the
subcommand no longer hidedcg uninstallor an allowlist grant, and an
--agentvalue namedtestorexplainno longer makes a mutating dcg
command look like self-inspection (025d06a, 07c53c9, a2b42bd).
Embedded code and heredocs
- Dynamic execution in a quoted interpreter heredoc is read in that
language. With the shell reading gone, a Node, Python, Ruby or Perl
program that ran decoded or computed source reached no check at all.eval,
exec,compilewithexec,Functionandnew Functionincluding
tagged templates,instance_evaland their kin now deny as
heredoc.<language>:dynamic_sourcewhen dcg cannot read their source. A
string literal is judged as source one level deep, soeval("1 + 1")stays
allowed and a literal that runs a shell pipeline does not. That includes an
execof a literal that importssubprocessunder another name, which
0.15.3 allowed. In Python, JavaScript, TypeScript and Ruby each constant
string inside the literal is also judged as a command. That second reading
does not reach Perl literals, or strings with an interpolated field:{x}in
an f-string,${x}in a template,#{x}in Ruby. A string split across+
is judged a piece at a time. A name bound once to a
literal at the top level
counts as that literal, unless the body could rebind it unseen:globals(),
locals(),vars(),setattr, a star import, a Rubybinding, JavaScript
withor a write throughglobalThis, or the name inside a string. A
reading that stops at a limit, an unfinished parse or an unproven Perl string
boundary comes backindeterminate, not allowed. So does a quoted heredoc to
an interpreter with no terminator line, when a candidate word is present. PHP
and Go have no reader, so those bodies keep
their 0.15.3 shell reading ofeval. In the synthetic comparison above, this
denies again 181 forms that run and that the change to quoted heredocs would
otherwise have allowed. It scans what it can reach and no more: it runs only
on quoted heredocs dcg has proved go to the interpreter, not on unquoted
heredocs and not on inlinenode -eorpython3 -c. Of 657 synthetic forms
that run and that 0.15.3 also allowed, 431 stay allowed (63ca20f). - A computed command given to a language's own process call is not read.
This limit is the same in 0.15.3, and the check above does not answer it. A
literal dangerous command in these forms denies in both releases; a command
built at run time is allowed in both. In Ruby: backticks,%x,system,
exec,spawn,IO.popen,Open3andopenwith a pipe. In Perl:
backticks,qx,readpipe,system,execandopenwith a pipe. In PHP:
backticks,shell_exec,system,passthru,exec,popenand
proc_open. In Python:os.systemandsubprocesswithshell=True. In
Node:child_process. Also allowed in both releases:eval.bind()with no
arguments; a form the shell reading misses in a body given todeno run -,
such as(globalThis as any).eval(x), because dcg has not proved that command
reads stdin as its program and so does not read it as TypeScript; Python
evalandexecspelled with compatibility characters, reached through an
import alias, or taken from__builtins__.__dict__orvars(builtins);
Pythonrun = builtins.eval, an escaped name in anexecliteral, and a
write throughf_globalsunder a computed name; JavaScript
const run = globalThis.eval,globalThis.eval.call,
Reflect.construct(Function, ...),.constructortaken as a value, and a
\uescape in a property name; Ruby::Kernel.eval; PHPcreate_function;
Perls///eeanddo $file. An aliased call has no end to chase, so dcg
scans what it can reach. - Inline programs are read as the whole shell word (#570). A
-c/-e
program was judged by its first quoted piece only, so a call after an
embedded quote, as in
python3 -c 'import os; os.system('\''git reset --hard'\'')', or the same
program double-quoted with escapes, was missed. The program word is now
decoded in full, quote concatenation included, forbash -c,sh -c,
Python, Node, Ruby, Perl, awk and osascript; a word that is not static
stays partial and follows the unverified policy. A launcher inside another
command's quoted operand (tar -I 'sh -c "…"') is read from the decoded
operand (3df4240, ec28682, 091bddc, 2d97d80). - Deno and Bun process APIs and JavaScript template literals (#570).
new Deno.Command("git", {args: [...]}),Deno.run,Bun.spawnand
Bun.spawnSyncwere allowed while thechild_processtwin of each was
denied, and a template literal handed to an exec sink
(execSync(`git reset --hard`)) was read as a fully dynamic value.
They now get the same checks aschild_process; an argv dcg cannot read
(a spread, a computed key) is unverified, as it is forspawnSync
(cadddd6, 5501d88). - awk commands are judged after awk's escapes. The command in
system("echo x\ngit reset --hard"), or with\073for;, read as one
harmlessecho. Commands awk runs (system(),| "…","…" | getline)
are now decoded with octal,\xand control escapes first, and a pipe
target whose-cstring runs its stdin (print … | "sh -c \"cat | sh\"")
is recognised as a shell (2d97d80). - Strings that reach a shell from Python or JavaScript are checked
(#544). Literal substitutions ($(…), backticks) carried through
variables or opaque calls, and a static string passed to a literal Python
exec/evalor JavaScriptevalprogram that hands it to a shell sink,
now go to the command evaluator with their own rule and allowlist identity,
including escaped and quote-concatenated-c/-espellings. Unreferenced
strings, printed text, static argv and option values stay data; hoisted
JavaScript declarations and ambiguous names withdraw the proof, and
incomplete analysis cannot approve a command (467a3be, f9e8748,
b91648a). - Inline programs stay checked inside compound commands (#544). A
literal Python or JavaScript program carried through a variable is still
inspected next to another statement, a pipeline, a redirection, an
environment assignment or supported trailing operands. Another shell
consumer cannot claim the documentation exemption for it, and a later Node
option that replaces the program cannot claim the earlier argument. Static
argument arrays stay data (f9e8748). - Python
-csource supplied by an expansion is reviewed (#538). When an
unresolved shell expansion supplies the wholepython3 -cprogram, it now
gets the inline-launcher denial. Python option ownership and outer quoting
are followed, so literal or escaped dollar signs, statically resolved
source and-ctext passed as script arguments keep their treatment
(f9e8748). - Python
subprocessargv passed by name (#602).
args = ['rm', '-rf', '/home/user']followed bysubprocess.run(args)was
allowed, although the same list written inline is denied. When a sink's
whole argv is a bare name, each list or tuple literal bound to that name is
now read in its place, and pack rules see the command
(cmd = ['dd', 'if=/dev/zero', 'of=/dev/sda']). Every literal binding
counts, which errs toward denial;cmd = ['git', 'status']and
sys.argv[1:]stay allowed (efbd488). - More ways to reach
shutil.rmtreeare caught (#549). The temp-path
exemption accepted the temp root itself, soshutil.rmtree('/tmp')(also
/tmp/.,/private/tmpand/tmp/build/../..spellings) passed as
cleanup; only real descendants are exempt now. Also caught:
from shutil import rmtree as wipe,getattr(shutil, 'rmtree')(…),
shutil.__dict__['rmtree'],rmtree.__call__, a default argument
def f(remove=shutil.rmtree), and atempfile.mkdtemp()result after
tempfile,osoros.pathwas rebound or patched. Literal temp
descendants stay allowed, now including adjacent literals such as
'/tmp/' 'build'(00a1c8b, ecf3fc0, dec8bcb, 9ca6dde, 869dcb3,
9930825, f339fae). - Scripts a heredoc writes and a program then runs get language checks
(#540, #583). A quoted heredoc written byteeto a literal file and run
by a supported interpreter is now checked like one written bycat. The
written Python or Node script stays checked when the interpreter gets flags
or script arguments, and when the run is chained on the heredoc line:
cat > a.py <<'EOF' && python3 a.pywithshutil.rmtree('/home/user')in
the body was allowed and is now denied. Append mode, dynamic paths and
extra consumers keep conservative analysis (7c50fa7, a2b42bd, 1b2da8c). - A heredoc is inert interpreter source only when the interpreter reads
its program from it (#560). Withpython3 -c '…' <<'PY'or
python3 script.py <<'PY', the program can read the body and run it as
shell (os.system(sys.stdin.read())), so such a body keeps its shell
checks (a42c284).
False positives fixed
- No longer blocked: the four replayed commands 0.15.3 denied. The
replay of real agent commands (see Known issues) found four that 0.15.3
blocked and 0.16.0 allows. Each is a false positive fixed since 0.15.3,
and a destructive neighbour of each is still denied by both versions:- A dynamic
-Cpath followed by a literal subcommand other than
branch, such asgit -C $dir apply $S/x.patch, was denied as
core.git:branch-dynamic-token.git -C $dir branch -D $nameand
git -C $dir reset --hardstay denied (#597, 5c06006). - An append to a dynamic path, such as
cargo test 2>>$S/test.error
>>"$LOG", was read as a truncating redirect and denied as
core.filesystem:redirect-truncate-dynamic-path.2>$S/test.errstays
denied (#575, f6fdf44). - A
python3 - <<'EOF'body whose string literals hold Rust code, with
an apostrophe in a comment and acollect::<Vec<_>>()call, was read
with the shell's quoting once any pack keyword appeared in it, and the
>(was denied asheredoc.posix:process-substitution. The same body
given tobash, and a real>(rm -rf …), stay denied (#560, #591,
db83e37, 9b55141). - An
umountwith no-fwhose mount path contains-nfs, seen as the
quoted argument ofdcg explain, was denied assystem.disk:umount-force.
umount -fstays denied (#548, f6fdf44, 644aa60).
- A dynamic
- The quoted command given to
dcg explainordcg teststays data with
2>&1. dcg only analyses that argument and never runs it. 0.15.3
already alloweddcg explain "sudo umount -f /mnt/x", but adding2>&1,
alone or before a pipe, made it judge the argument as the command being
run, and deny it. It is now allowed with the redirect too; the same
command run directly, or piped intosh, stays denied. unlink,mkfs,mkswapandwipefsjudge the program that runs
(#550, #600). These rules matched the tool's name anywhere on the line,
soob sync-unlink,foo --unlink,find . -name unlink,
which mkfs.vfat mkfs.ext4,man mkswap wipefsand
ls /sbin/mkfs.vfatwere refused. They now judge the program a segment
runs, quoted or path-prefixed, throughsudo,env,timeout,
find -exec,xargs,flock,doas,parallelandbusybox, and
throughssh,toybox,docker,podman,nerdctl,kubectl,oc,
lxcandincus. Launchers whose grammar dcg does not model (watch,
strace,chroot, …) expose every operand (3e78579, e7d9de8, 644aa60,
2b72587).- Docker and Podman force removal needs a real
-f(#548).
docker rm probe-xf,docker image rm cve-followup/web:1.2and
docker network rm probe-net && grep -f patterns.txt file.txtwere read
as force removals. The flag must now be an option word of thatdockeror
podmancommand;docker rm c1 -f,-vf,--forceand Bash's$'-f'
still match.umount -fis scoped the same way (f6fdf44, 644aa60). - Home-directory and Trash
mvwith;,|or&in a quoted name
(#552). Any quoted operand containing one of these characters defeated
the exemptions, somv "/Users/u/Documents/a & b.txt" ~/.Trash/was
denied while the same name with a space was allowed. Quoted separators are
now file-name bytes there; unquoted ones, backslash escapes, home roots,
dotfiles and..are checked as before (45277d4, 414d1d7). - Assignment-only commands bind every name (#574).
B=/tmp/b T=/tmp/t
stored/tmp/b T=/tmp/tas the value ofBand lostT, so later
redirects into$Twere denied. Each word is now its own binding, and
readchanges only the names it reads (S=/tmp/x; read x; cat a > "$S/o"keeps the proof forS) (f6fdf44). rm -rfof a literal temp path insidesh -c(#576).
sh -c 'rm -rf /tmp/probe'andbash -c 'rm -rf /var/tmp/probe'were
denied ascore.filesystem:rm-rf-root-homealthough the same text is
allowed at top level and througheval,sudoandssh. A single-quoted
script that is one literalrmof/tmpor/var/tmpdescendants now
gets the same exemption; temp roots, traversal, globs, expansions, extra
commands and outer redirects stay denied (bb3a7d1, 418cb2d, 0e40cc0).- Temporary directories over ssh and under
TMPDIR(#554, #590).
ssh h 'T=$(mktemp -d); echo x > $T/s'is judged like the same script run
locally. Paths spelled literally under theTMPDIRdcg started with
(macOS's/var/folders/…/T/, for example) satisfy
redirect-truncate-dynamic-paththe way/tmppaths do;$TMPDIRitself
is still not trusted, and home, system, credential and symlinked roots stay
denied (0e40cc0, a2b42bd). git -C $dirfollowed by a read-only subcommand (#597).
git -C $r log main $b(anddiff,rev-list,merge-base,cherry)
was denied ascore.git:branch-dynamic-token, on the theory that$r
could split intodir branchand$binto-D. A literal subcommand
after the option value now decides;git -C $r branch $b, and a splitting
path followed by a flag, an expansion or an unknown word, stay denied
(5c06006).- Commit-message files copied with
scporsshstay data (#543). A
laterscporsshreference made a quoted heredoc written to a file look
executable, so ordinary prose beginning withevalreached the eval
scanner. The whole literal write, commit and transfer workflow is now
proved first, renamed copies included, and literal numeric ports
(-p 2222,-P 2222) are accepted. Unknown consumers, expansions,
pipelines into a shell, custom transports, and transfers onto executables,
protected files or Git hooks keep their checks (1e1a0df, f9e8748). - Quoted
flowctlreceipt text stays data (#538, #540). Complete static
flowctl gate checkandgate receiptoperands can hold quoted shell
punctuation without triggering destructive-command rules, including when
FLOWCTLis a variable. Live expansions, incomplete words, unknown
options, redirects and later commands keep their checks (29fb1a2,
f9e8748). - Python and Node heredocs that only build and print text (#544, #586).
Backticks in strings of a quotedpython3 -ornode -heredoc were put
back into the shell scan, so documentation edit scripts matched Git
branch-deletion rules. When dcg proves the program is straight-line code
that builds strings, prints them and reads or writes files through
open/fs, those literals are masked from the shell view. If it opens no
file at all, every literal is data, so text that merely spells a command
(acurlupload line in #586) no longer matches its pack. Where the
printed text goes (a pipe into a shell, a substitution, a redirect into a
profile or cron file) is judged as before (c255094, 78a965b). - Quoted interpreter heredocs are not parsed as Bash (#560, #591). A
python3 - <<'EOF'ornode - <<'EOF'body containing an apostrophe and a
backtick,<(or>(, regex literals, tuples or many shell-looking
strings could be read with the outer shell's quoting or as substitutions.
When dcg proves the interpreter reads the body as its program, the body is
kept out of outer-shell syntax, and its strings count as shell commands
only with execution evidence. That now also holds aftercd dir &&, with
Python run-mode flags or program arguments, and with commands after the
heredoc when its output reaches only the terminal. A quotedcatbody
written under~no longer counts as input to a followingpython3 -
heredoc (#591). Output piped to a shell, scripts written and then run, and
destructive strings behind a stray quote stay denied (efd67d2, db83e37,
11ca106, 1a13a62, 9b55141). - Quoted heredoc delimiters written in pieces (#544).
<<D\OCand
<<D'OC'now mark the whole delimiter as quoted in both the extractor and
the bundled Bash parser, so the body is literal text and the commands after
its terminator stay visible. Unsupported delimiter syntax cannot pick a
shorter terminator (467a3be). - A quoted body written to a file holds no live commands (#604). In
cat > f <<'SH', a body line such asX=$(pwd)followed by
trap x EXITwas read as a deferred command and refused as incomplete
analysis, and apython3 - <<'PY'heredoc inside the body was judged as a
program about to run. Both are now data. The same body piped to a shell,
run as a file, sourced or written to~/.bashrckeeps its checks
(f4b71bc). cdbefore a written script, and a consumer on the heredoc line
(#583).cd client && cat > f <<'EOF' … EOFfollowed bynode f, and
cat > f <<'EOF' && node f, read every JavaScript arrow in the body as a
truncating redirect. A literalcdwith one plain directory, and exactly
one&& <interpreter> <same file>on the heredoc line, now keep the proof
that the body is the program that runs; a shell consumer,||, pipes and
longer chains do not (4f83afa, 1b2da8c).- Python
tarargv built from module constants (#562).
subprocess.Popen(['tar', '-cf', '-', '-C', src, name])withsrcand
namebound once at module level to a string orPath('…')was denied as
core.filesystem:tar-exec-unverified. In straight-line programs such
constants now resolve to their literal value, for archive programs only; a
constant that is itself a helper option still denies, and shell or
interpreter code arguments are never resolved. A replay of 44,471 commands
changed exactly the two reported verdicts (35a6b33). - Python call text inside strings and comments (#601). An edit script
that quotessubprocess.run([…in a search or replacement string, or
mentions it in a comment, was refused as an unverified exec-sink call.
Comments and plain string literals are now read as text unless the program
can run a string (exec,eval,compile,subprocess,importlib,
…); destructive literal argv in a string still blocks (66e108c). - A variable bound to a literal interpreter path (#608).
PY=/usr/bin/python3; $PY -c '…'was denied as an obfuscated inline
launcher, while the literal command was judged by its program. The bound
interpreter is now read like the literal one: a harmless program is
allowed, a program that runsshutil.rmtreeis still denied, and an unset
variable, a binding that carries its own-c, or a command-substitution
binding stays an unverified launcher. The same binding inside a
bash -cor ssh script is not resolved yet; see Known issues (5f6c411).
Custom packs
- Custom rules see executable substitutions inside prose arguments
(#614). Custom rules and their keyword selection can now match active
backticks and$()inside GitHub CLI bodies, Git messages, Beads Rust
descriptions and other registered data arguments, after dcg has checked
the inner command. Inert prose, single-quoted or escaped markers and
checked producer output stay masked, and source spans, safe-pattern
authorization, rule allowlists and built-in checks are unchanged. A fully
quoted--message=valuestays data for the Git rules (f5a5f7f). - External packs are selected by the program a command runs (#592). An
external pack runs only when one of its keywords is present, and the gate
read the command text, so a program reached through a variable, a
substitution, a glob, a brace expansion,$"…"quoting or quotes inside a
launcher's operand (xargs my''tool) skipped the pack entirely. Selection
now also resolves each segment's program through wrappers and launchers,
with quotes and escapes removed, and evaluates every enabled external pack
when a segment's program cannot be known. Thetime,timeoutandchrt
parsers no longer take an option word for the program or the duration
(time /opt/bin/tool,timeout -vk 5 10 prog), which had made dcg resolve
the wrong program. In a proven POSIX command, a word written with quotes
inside it (zap''all) is matched as the word the shell passes (bc01a99,
3d1bfe2, b33f3ef). - Keyword-less external packs apply inside nested shell bodies. An
external pack with no keywords was not enforced in an executable nested
body such asbash -c '…'; it now is, while inert data stays masked
(553263f).
Allow-once and allowlists
- Exact allow-once grants for quoted interpreter commands can be redeemed
(#599). A grant for'/usr/bin/python3' -c 'print(1)'was never used:
the obfuscated-launcher check denied the command before the grant lookup
ran. Such grants are now looked up first, also when the command sits inside
an alias or a trap. Explicit block overrides still win, a grant matches
only its original bytes in its directory scope, and it is used once. Of 16
exact-grant cases, 0.15.3 honoured 6; this release honours all 16
(deff5c0, 07c53c9). - Stored grants and pending codes are validated when read (#610).
Records with an unsupported schema, a malformed or future timestamp, or a
lifetime longer than 24 hours are skipped and later pruned without
disabling valid neighbours, and a grant is expired at its expiry
timestamp. Scope uses the request's working directory, resolved so..
and symlinks cannot escape it. A project grant must name an existing
repository root, cannot cover a filesystem root, and stops at nested
repositories, submodules and worktrees; an unknown request directory
cannot borrow the hook process's own (82423ef, 025d06a). - Grants match the request as it was sent. Whole-command allowlist
entries, allow overrides and allow-once grants are compared with the
original request through child extraction, detection rewrites, case
folding and policy replays, so a command dcg derived from another cannot
match a grant meant for the original. Rule grants still cover only their
own findings (c4d3ad3). - Interpreter callbacks keep their own rule ownership (#544). A command
in a callback, an assignment or an argument's side effect could inherit the
already-checked status of the enclosing execution call, so an allowlist
entry for that outer sink also covered it. Such commands now keep their own
rule and need their own grant; static argument arrays and option values
stay data (29fb1a2).
Windows and PowerShell
- Python heredocs no longer switch on the Windows packs on Linux and macOS
(#569). A regex such asr'[^\n]{0,40}(x|y)'or a word such as
install-lin a quotedpython3 - <<'PY'body looked like PowerShell, so
the hook activatedwindows.*and denied the program as
windows.filesystem:windows-filesystem-semantic-unverified, depending on
the body's length;dcg testdisagreed. On a non-Windows host, a quoted
heredoc that a non-shell interpreter provably reads as its program no
longer activates those packs, unless the command namespwsh,
powershellor a.exeprogram. As a result
os.system("echo & rd /s /q C:\Users")in such a heredoc is allowed there,
because/bin/shhas nord. Whenwindows.filesystemis enabled and a
command exceeds its statement bound, the answer is now indeterminate
instead of a misattributed denial (39f6857). - PowerShell: chained dcg diagnostics are allowed, and an alias cannot
borrow dcg's self-inspection mask (#584).
& 'C:/example/dcg.exe' test --help; & 'C:/example/dcg.exe' config --format json
was denied because the mask left the call operator&behind; it is now
masked with the call. A command that can rebind thedcgname
(Set-Alias dcg Remove-Item, functions,iex,Import-Module, also when
split with Unicode quotes) gets no mask, so
Set-Alias dcg Remove-Item; dcg test -Recurse -Force <dir>, which was
allowed, is denied. Dot-sourced script blocks (. { … }) are now judged
as running their body (a79ef24, 76a7df2). dcg testanddcg explainuse the Windows packs for Windows dialects
(#589). On Linux and macOS,dcg test --dialect cmdand
dcg explain --dialect psused only the host's packs and could report an
allow that the live Windows hook denies. Explicit Cmd and PowerShell
dialects, and recognised Windows syntax in the unknown dialect, now enable
the default Windows packs; configured exclusions and per-run overrides
still apply (cb74688).- Git Bash
cmd //cis read ascmd /c(#561). MSYS2 rewrites a
leading/cas a path, so Git Bash users writecmd //corcmd //k.
From a POSIX shell these, with preceding host switches, are now read as
/cand/kand the payload is judged as Cmd; native Cmd and PowerShell
are unchanged, and unknown doubled switches still fail closed (0555a06). - No native ARM64 Windows build in this release. v0.16.0 publishes no
dcg-aarch64-pc-windows-msvc.zip. ARM64 Windows gets the x64 build under
emulation until a release with a native ARM64 run:install.ps1, which
dcg updatealso runs on Windows, falls back to the x64 zip with a
warning. v0.15.3 had a native ARM64 zip.
CLI
dcg doctorwarns about unrestricted allowlist grants (#545). An
active whole-rule grant with no working-directory restriction and no
expiry now produces a warning naming the rule, the layer and file, the
reason and any recorded author or timestamp; destructive filesystem and
Git grants are marked high risk. Path globs that match every directory
(**/,**/*,**/**, or an equivalent root path) count as
unrestricted. Project, user and system layers are covered, and warnings
still show next to parse errors. Doctor changes nothing, including under
--fix, and the JSON v1 output and warning-only exit status are unchanged
(d7c2e70, 2c7c015).- A fallback denial names the rule that owns the text. When
embedded-code analysis cannot finish, dcg scans the whole command
conservatively. A destructive command next to such a program was then
reported as incomplete embedded-code analysis, and the suggested remedy was
to waive that backstop. The command is now also checked by the packs
alone, and their denial (core.git:reset-hard, for example) is the
reported verdict; otherwise the fallback denial stands. Either way the
command is denied (a6f82fe). - Timeout reasons say where the time went (#571). An indeterminate
answer now ends with the stage and phase that ran out, the elapsed wall
time and the process CPU time, for example
(stage: evaluation, phase: regex_compile; elapsed 1152ms, process CPU 91ms).
Low CPU against high wall time means the host did not schedule dcg; CPU
close to wall time means the input was slow to analyse (cd3d3c2). - Copy-safe hints in denials. The suggested
dcg explain -- …command
is printed only when every argument is a plain literal, so copying it
cannot run an expansion or change quoting. Other commands are pointed to
dcg test --stdin --explain, with a bounded JSON preview; the warning
footer follows the same rule (ad98f9f). - The Trash advice says which moves it covers (#605).
rm-rf-root-homeandmv-sensitive-source-root-homesuggested
mv <path> ~/.Trash/for any path, but the Trash exemption admits only
sources under a home or temp directory and relative paths, so following the
advice for/opt/<x>led to a second denial. Both explanations now name
the sources it covers. No rule changed (b4aaf0b). DCG_PROFILE=1prints hook phase timings. Set on a single hook
invocation, it appends oneDCG_PROFILEJSON line to stderr with
per-phase wall times and call counts, without command text. See
docs/performance-issue-555.md(fb60af1).
Performance
- Read-only commands with inert redirections skip pack work (#571).
ls 2>/dev/nullselectedcore.filesystemandsystem.diskand compiled
both, although no rule in either can match a redirection to the null
device; on a busy host that pushed read-only commands past the hook
deadline. Pack selection now ignores redirections to or from/dev/null
and descriptor duplication when every redirection on the line is of that
kind; the selected packs still match the original text. Median CPU per
hook run:ls 2>/dev/null36.3 to 9.6 ms,ls 2>&123.8 to 9.6 ms,
cargo build --release 2>&1 | tail -524.8 to 10.1 ms. A replay of 44,471
commands gave identical verdicts (cd3d3c2). - Large heredocs cost less to check (#555). The hook no longer repeats
work on a bigpython3 - <<'EOF'ornode - <<'EOF'program: ast-grep
rules compile lazily per language and each AST is walked once, stdin
provenance is not rescanned for every reader, and completed heredoc
extractions and documentation-mask analyses are reused within a command.
Analysis limits for interpreter heredocs now scale with the program, so
benign 600 to 1,000-line Python and JavaScript heredocs finish within
them, and the fork-bomb rule no longer exhausts its search on long programs
full of function calls. The 256 KiB source cap still bounds the work
(fb60af1, ca1b3dc, 50c3580, eff12d3, b6f4a49, 1475a05). - The guardrails preset has a slower tail than in 0.15.3. With the
careful_company guardrails preset, p99 hook latency rose about 12% (143 →
161 ms) and p95 about 9%. Median and total fell (median 54.5 → 48.8 ms).
The default packs are faster at every percentile; the slower tail is long
commands that are allowed anyway, mostly multi-line heredoc bodies.
Command history and logs
- The history size cap is enforced against what SQLite committed. The
cap was checked against the main database file's length, which misses
pages still in the write-ahead log and growth by another connection; in
one probe a 1 MiB cap was accepted while the database held about 2 MiB.
Writers now check the page limit SQLite enforces inside every write
transaction, and opening the database no longer forces a truncating
checkpoint (418918d, 8d1e50d, 7d48fed). - History writes neither repeat nor silently lose rows (#433). After a
failed batch fell back to row-by-row inserts, recovery could insert the
already-committed rows again, and an insert inside an unfinished
transaction could be counted as written. Recovery now resumes after the
committed rows and rolls back before reopening. The writer's queue is
bounded at 4,096 entries and 16 MiB of pending text, and shutdown is never
blocked by it (98459f3, 70e203d, e234e92). - Full log redaction covers every persisted field (#585). With
[logging.redaction] mode = "full", free-form reasons, caller labels,
audit paths, and allow-once command hashes and short codes still reached
the log files and could reveal command content. They are now replaced
([REDACTED],Diagnostic details redacted.), while decisions, rule IDs,
timestamps and timings stay. Allow-once audit events, management actions
and the legacy[general] log_filefollow the same policy (97e5364,
d474653).
Installers
- The one-line installer runs on stock macOS again, and says when it needs
bash (#587). A change made after 0.15.3 putcasestatements inside a
command substitution, which macOS's/bin/bash3.2 cannot parse, so
curl … | bashstopped before installing anything. The snippet is now
read without a command substitution, and the block written to~/.bashrc
or~/.zshrcis byte-identical, so existing installs are not rewritten.
install.shanduninstall.shpiped to a shell other than bash (dash,
zsh, ksh and others) now exit with the bash one-liner to use instead of
failing onset -o pipefail, andinstall.shleaves POSIX mode when bash
runs it assh(8eca632, f9374e5). - The installer reads the latest version from one-line API responses.
When GitHub's API returned minified JSON,install.shtook the release
body for the version. It now extractstag_nameand requires a
version-shaped tag (37874bd). - v0.16.0 ships Minisign signatures only (key 69B3955C8D2E62A8); there are
no.sigstore.jsonbundles this release. The installers verify Minisign
when theminisigntool is on PATH (--require-minisignor
-RequireMinisignmakes it required) and skip the missing bundle with a
warning. The SHA-256 check stays mandatory.
Dependencies
- Security and routine updates.
rustls0.23.46 (RUSTSEC-2026-0346 to
0349),rustls-webpki0.103.16 (RUSTSEC-2026-0382),webpki-rootsand
webpki-root-certs1.0.11 (RUSTSEC-2026-0381, RUSTSEC-2026-0383),
ctutils0.4.7 (RUSTSEC-2026-0385) andpulldown-cmark0.13.6
(RUSTSEC-2026-0414);cargo auditnow reports only the unmaintained
unsafe-libyamlwarning, throughserde_yaml. Routine bumps:tokio
1.54.1,serde_json1.0.154,toml1.1.8,toml_edit0.25.17,libc
0.2.191,self_update1.3.1,schemars1.2.3,sha20.11.1,smallvec
1.16.4 andwinresource0.1.34.ast-grep-corestays at 0.45.3 to match
the vendored language adapter (bab32ea, c0d206d, 82df731, 9c90d05,
607e927, f655ead, 9d76299, cdf17b4).
Known issues
- Replay of real agent commands. 8,000 recent shell commands from real
agent sessions were replayed throughdcg testagainst 0.15.3 and this
release on an idle host. 0.16.0 blocks 11 that 0.15.3 allowed (0.14%) and
allows 4 that 0.15.3 blocked, all four false positives fixed since 0.15.3
(listed under False positives fixed); the candidates held back on
2026-10-14 and 2026-10-23 newly blocked 66 and 63 of 8,000. Of the 11, two
are intended (a PowerShell script block read from stdin over ssh, and
-EncodedCommandtaken from a variable), one is arguable (a>with a
dynamic target inside an sshbash -cloop), one is an ssh script with an
unterminated backtick that the remote shell cannot parse either, and seven
are false positives, listed below. On a heavily loaded host, slow
evaluations can also come backindeterminate, which blocks. - A Python heredoc that runs source it read at run time. A body that
passes text read from a file toexec, such asexec(open(path).read())
or a slice of it, is denied asheredoc.python:dynamic_source. One of the
8,000 replayed commands did this, to reuse a helper script, and it was
harmless. dcg cannot tell that from an attack: the source is not in the
command, so there is nothing to read. - An
evalof a string built at run time, in a body dcg does not read. In
a quoted Perl, Python, JavaScript or Ruby heredoc of more than 10,000 lines
(heredoc.max_body_lines), and in a quoted Python or Ruby heredoc past the
tenth heredoc in one command, dcg does not read the body, and such a call
comes backindeterminate, wherever it is in the body: a client with a
review prompt asks, any other client blocks, and
general.unverified_decision = "deny"denies. For a body of more than
10,000 lines, with the default configuration, 0.15.3 denied such a call in
Perl and JavaScript only because of a shell-word match on anevalthat
starts a shell command, at the start of a line or after a;, which also
blocked harmless code such aseval("1 + 2"). In Python and Ruby it asked
only because its 1000 ms clock ran out on such a body. Past
heredoc.max_body_bytessuch a call is still denied in all four languages,
and past the tenth heredoc in Perl and JavaScript. In a body dcg reads, such
a call is denied asheredoc.<language>:dynamic_source. - More than 128
evalcalls on literals in one quoted interpreter heredoc
come backindeterminate. A Perl, Python, JavaScript or Ruby body with
more than 128 calls such aseval "1"is more than dcg reads, so it asks or
blocks. 0.15.3 denied these in Perl, JavaScript and Ruby, and allowed them
in Python. A call whose source dcg cannot read still denies, wherever it is
in a body dcg reads. Past a body limit, where dcg does not read the body, it
comes backindeterminate. - The word
copyin a script, with the opt-in guardrails pack. With
careful_company_running_windows.guardrailson, a harmless script that uses
the wordcopy, such ascopy.deepcopyin a Python heredoc, can come back
indeterminate. The pack'sagent-hook-config-overwriterule reaches its
regex work limit, and dcg treats an unfinished check as unverified, so a
client with a review prompt asks and any other client blocks. Two of the
8,000 replayed commands did this, at 1.9 and 2.5 KB, both with that pack on.
The default packs are not affected. The rule is unchanged from 0.15.3; since
025d06a the pack reads anything that might write, so this rule now reaches
these commands. To get past it, approve the prompt, run the script from a
file, or allow the exact command withdcg allowlist add-command. No
allow-once code is offered for anindeterminate, and raising
hook_timeout_ms, which the message suggests, does not help: this is a work
limit, not a clock. - A long heredoc of one repeated harmless line can come back
indeterminate. On a body of about 60 KB, such as 7,875 lines of
"nope";given toperl -, the default fork-bomb rule can use up its regex
work limit. dcg treats an unfinished check as unverified: a client with a
review prompt asks, any other client blocks, and
general.unverified_decision = "deny"denies. 0.15.3 counted an exhausted
search as no match and was slower on these bodies: it asked from about 30 to
40 KB, when its 1000 ms clock ran out. The limit is unchanged from 0.15.3.
The message suggests raisinghook_timeout_ms, which does not help: this is
a work limit, not a clock (553263f, 1475a05). - A quoted interpreter heredoc near the 64 KiB command limit takes longer to
check. A body of about 60 KB takes about 0.3 to 0.5 s on one test host,
against about 0.04 s in 0.15.3: 0.3 s for 6,000 lines ofeval "1";given
toperl -, and 0.5 s for 5,500 lines ofeval("1");given tonode -.
0.16.0 reads a quoted Perl, JavaScript or Ruby body as its own language
(63ca20f, #544, #560) and runs its full pattern pass over it. 0.15.3 read
the body as shell and stopped at its first match, which is why it denied
harmless bodies such as 100 lines ofeval "1";. A command over 64 KiB is
not evaluated by either version: it asks or blocks at once. - Hook input over
max_hook_input_bytesis allowed, as in earlier
versions. Hook input larger thangeneral.max_hook_input_bytes(256 KiB
by default) is allowed with an audit warning unless dcg finds a command in
it that it denies or asks about. A quoted Python, JavaScript or Ruby heredoc
of 320 KB that callsevalorexecon a string dcg cannot read makes the
hook input that large, so it is allowed this way wherever the call is, by
0.15.3 as well; with the limit raised, the same command asks.
general.fail_closed = true(orDCG_FAIL_CLOSED=1) denies such input. - A heredoc with no terminator beside one that runs unproven code. A
closedpython3 <<'A'heredoc whose body isexec(x), followed in the same
command by an interpreter heredoc that never ends, comes back
indeterminate; 0.15.3 allowed it. A client with a review prompt asks, any
other client blocks, andgeneral.unverified_decision = "deny"denies it
everywhere. The message says dcg could not finish "within 1000ms" and
suggests raisinghook_timeout_ms, but no clock ran out (the check takes
about 70 ms) and raising it does not help: the cause is the
unterminated_interpreter_stdinnamed in brackets
(bd-indeterminate-reason-wording-jcpy). - A harmless
perl -pe,perl -neorruby -pefilter on a pipe (#611,
#624).tail -n 3 x | perl -pe 's/foo/bar/' | cut -c1-130is denied as
heredoc.posix:pipeline-consumer, in 0.15.3 as well. The rule cannot simply
skip inline code, because an/eflag makes a substitution executable.
With no pipe,perl -pe '…' fileis allowed. - An ssh script loses the home-directory
mvexemption (#620).
ssh h 'mv -n /home/u/work/x /home/u/work/x.old'is denied as
core.filesystem:mv-sensitive-source-root-home, while the samemvtyped
locally is allowed. 0.15.3 did not evaluate ssh scripts without a pack
keyword (#551);bash -c '…'already gave this denial in 0.15.3. - A quoted
>in an argument of an unresolved command word
(#621).$G grep -n "-> Result<Performance\|Performance {" srcis
denied ascore.filesystem:redirect-truncate-dynamic-path. WithG=git
bound earlier in the same command, or withgitwritten out, it is
allowed. - A literal interpreter binding inside
bash -cor ssh (#608).
bash -c 'PY=/usr/bin/python3; $PY -c "x = 1"'and the same script sent
throughssh h '…'are still denied as
heredoc.posix:inline-launcher-unverified. 0.15.3 allowed them only
because it did not evaluate such scripts without a pack keyword (#551).
The top-level form is fixed in this release. - Shell punctuation inside quoted Python heredoc literals (#544, #560).
Three replayed Python heredocs whose string literals contain backticks or
other shell syntax are still denied. A\"outside a string in a quoted
Python or Node heredoc comes backindeterminatein about 50 ms; Python
could not run such a body either. while IFS= readwith an unquoted path variable (#566). One replayed
loop is denied ascore.filesystem:credential-file-write.- An inline-code flag behind a variable command word (#622).
G=git; $G log -p -1 | tail -5is denied as
heredoc.posix:inline-launcher-unverified, in 0.15.3 as well:-pis
read as an interpreter's inline-code flag even though the binding names
git. - Tests that fail at this tag (#623). On a machine with no dcg
configuration, nine tests in dcg's own suite fail. Three expect the deny
that the unquoted heredoc change in Behaviour changes removed, so in these
a command runs that the test says must stay denied:
cross_pack_true_positive_controls_still_deny,
executing_receivers_and_expanding_bodies_stay_deniedand
unquoted_delimiter_with_command_substitution_stays_denied. That conflict
between e4e0629 and these tests is open. A fourth,
literal_command_queries_and_quoted_wrapper_operands_remain_harmless, is
an over-block new in this release: acommand -pv teewith a path in a
variable split onIFSis denied ascredential-file-write(4850e2d). The
other five are over-blocks or a different rule name. Four of those decide
exactly as 0.15.3 does:
repro_313_powershell_read_only_false_positives—
escaped_dcg_options_keep_agent_values_and_inspection_arguments_as_data,
escaped_dcg_syntax_cannot_hide_guardrail_changesand
native_agent_argument_boundaries_are_dialect_specific(with
--dialect unknown, a PowerShell backtick in a dcg command is read as a
POSIX substitution and denied asheredoc.posix:substitution-unverified),
andcli_e2e—
custom_pack_loading_tests::exact_command_grants_do_not_reappear_after_detection_rewrites
(upper-case PowerShellGIT RESET --HARDis denied as
core.git:git-alias-semantic-unverifiedrather thancore.git:reset-hard).
The fifth is new in this release:
global_options_preserve_inspection_and_argument_dataexpects
dcg --agent='dcg --robot allow-once abc --yes' allowlist listto be
allowed, as 0.15.3 did. With the opt-in
careful_company_running_windows.guardrailspack it is now denied as
dcg-policy-self-weakening.
repro_313_powershell_read_only_false_positives::hook_protocol_keeps_guardrails_active_with_global_options
also fails when a system-wide/etc/dcg/config.tomladds a custom pack.
Separately,installed_bridge_applies_unverified_posture_when_dcg_cannot_start
inrepro_504_omp_bridge_missing_cwdfails in about one run in twenty with
Text file busy, on this release's code before and after its last changes.
The test copies the dcg binary and runs the copy while sibling tests fork,
and the failure comes before any dcg code runs.