Added
- First-party OpenCode support (#318).
dcg install --opencodewrites a
nativetool.execute.beforeplugin to
~/.config/opencode/plugins/dcg-guard.js(--projectfor
<repo>/.opencode/plugins/). The plugin routes every OpenCodebashtool
call through dcg's Claude-compatible hook protocol — spawning the absolute
dcg path embedded at install time withOPENCODE=1so the agent is
identified — and aborts the tool call by throwing on adeny(anask
verdict also fails closed; OpenCode has no operator-review state).
Infrastructure failures (dcg missing) fail open with a stderr notice. The
file carries adcg-opencode-pluginownership marker: the installer refuses
to overwrite a user-owned file of that name even with--force, and
uninstall.shdeletes only marker-carrying files.install.shconfigures
OpenCode automatically when detected,dcg doctorgains an
opencode_plugincheck (an error +--fixable when OpenCode is in use but
unguarded — there is no Claude-compat fallback), andAgent::OpenCodeis
detected from the plugin'sOPENCODE=1. New
docs/opencode-integration.md. Motivated by a real shared-environment
outage where a green doctor coexisted with an entirely unguarded OpenCode
install. dcg updatecan no longer silently replace a local build that is ahead of
its release (#320). Build provenance (git describe --tags --dirty, plus
an explicitDCG_RELEASE_BUILD=1marker exported by dist.yml and the DSR
runbook) is embedded at compile time and shown as aCommit:line in
dcg --version.dcg updatenow refuses — before any network or installer
work — when the installed binary is a local build ahead of its release tag,
or when the install is pinned via the newgeneral.update_pinconfig
(DCG_UPDATE_PINenv). The explicit escape hatch is
dcg update --replace-local-build. Pinned installs also suppress the
background "update available" nudge, and a new warning-only doctor check
(build_provenance) flags the unpinned-local-ahead state that is one
routine update away from silent coverage loss.- Suggestions dcg itself gates are now labeled, machine-checked, and
consistent (#316).PatternSuggestiongained agatedflag: a gated
suggestion is a less destructive form of the blocked operation that still
requires approval, and every renderer now says so explicitly ("dcg gates
this too — it needs explicit approval" in block output,[gated: ...]in
dcg packs/classify text, agatedfield in JSON) so an agent reading the
block message stops retrying suggestions dcg will deny. The 14 remaining
self-denied suggestions from the #316 sweep are either fixed or marked
gated: the MySQLTRUNCATE⇄DELETEmutual-referral loop is broken (backup
suggestion first, gated cross-references labeled), the kamal proxy rules
gainedkamal proxy restartas a runnable first alternative,
core.git:branch-dynamic-tokennow leads with the workflow fix that
actually works and clarifies that quoting protects a creation while a
literal-Dstays gated, and the docker/kubectl/postgres/bigquery/
guardrails/github rows carry accurate gated markers. Two registry-wide
tests enforce the invariant in both directions (every non-gated suggestion
is allowed by its own pack; every gated marker is real), replacing the
first-suggestion-only check. External YAML packs can declare
gated: trueper suggestion. - The fail-closed launcher-verifier family now carries stable, allowlistable
rule ids (#316, #304, #313). "Embedded shell launcher cannot be statically
verified" and "Inline interpreter launcher cannot be statically verified"
denials previously reported"rule_id": nullwithsource: "legacy_pattern"— nothing todcg allowlist add, nothing for
[policy.rules]. They are nowheredoc.shell:launcher-unverifiedand
heredoc.posix:inline-launcher-unverified: reviewable, allowlistable
(an allowlist grant skips only the fail-closed launcher check — the rest of
the command is still evaluated on its own merits), and policy-addressable
like the #261 family.
Fixed
-
Heredoc/launcher allowlist grants no longer override pack denials. A
grant for a fail-closed heredoc-family rule (e.g.
heredoc.shell:launcher-unverified) was converted into a whole-command
allow at the end of evaluation even when the pack pass had denied the
command — an unverifiable encoded-launcher segment chained withrm -rf /
was allowed in full under the grant. Both terminal conversion sites now
attribute the grant only to an ALLOW outcome; pack denials and indeterminate
verdicts pass through untouched, so a grant skips exactly the fail-closed
check it names and nothing else (bd-l9jf whole-command leg). -
Windows installer repairs a stale
$PROFILEhook-check (#282). The
earlier #282 fix corrected the startup-check block's detection text, but the
installer skipped any profile already containing the marker line — which was
identical across versions — so pre-fix installs kept warning
[dcg] Hook missing from ~/.claude/settings.jsonon every new terminal no
matter how often dcg was reinstalled or updated.Add-DcgProfileChecknow
rewrites the managed block in place when its content is stale (line-ending
differences don't count as stale), and best-effort repairs the other
PowerShell host'sprofile.ps1(Windows PowerShell 5.1 vs pwsh 7) without
ever creating one. -
Unix shell startup checks self-repair too.
install.shand
dcg setup --shell-checkhad the same marker-only idempotence trap on the
bash/zsh RC snippet: once the marker line existed, no re-run would ever
replace the block, pinning users to the first snippet version they received.
The Unix snippet has never changed, so nobody was bitten — this closes the
trap before the first time it does. Both injectors now rewrite a stale
managed region (marker line through the first column-0fi) in place; an
unrecognizable boundary falls back to appending a current block. -
The keyword pre-filter treats
_as a boundary, not a word character
(#323). Underscore was in the pre-filter's word class, so underscore-joined
names never admitted a pack:export DCG_DISABLE=1quick-rejected past the
guardrails pack's own self-weakening rule,terraform destroy -target=cloudflare_record.wwwpastdns.cloudflare, andWEBHOOK_SECRET/
SCW_SECRET_KEYpast every credential rule — the regexes were correct but
never ran (silent fail-open, invisible to tests written against hyphenated
decoys)._is now a boundary in the pre-filter only; pack regexes still
decide the verdict, so the change can only admit more commands to full
evaluation. Alphanumeric continuations (dcgx) still quick-reject. -
Dead-gated rules re-armed by fixing their packs' keyword lists (#323).
system.diskgainedumount(theumount-forcerule was unreachable — no
keyword in the list occurs inumount -f),database.redisgainedvalkey
andkeydb(the protocol-compatible client renames; every rule silently
stopped firing on those binaries), anddatabase.mysqlgainedmariadband
RESET MASTER(the renamed client, and the reset-master statement when it
reaches the shell without amysqltoken). -
A
Bash-labeled command that is unmistakably PowerShell now evaluates as
the fail-closed union of all dialects (#322). VS Code Agent Host
transforms PowerShell tool calls and putstool_name: "Bash"on the wire,
soRemove-Item -LiteralPath .\pipelines -Recurse -Forcewas evaluated
under the POSIX dialect — where a cmdlet is an inert unknown binary — and
executed. When any statement segment starts with an approved-verb
Verb-Nouncmdlet token, the hook now widens the dialect toUnknown,
which fails closed across every dialect. Explicitpowershell/pwsh/cmd
labels are never second-guessed, and hyphenated POSIX commands (apt-get,
docker-compose,start-stop-daemon) do not widen.
Two follow-up gaps closed (fresh-eyes review): (1) the widening only
fired onVerb-Nouncmdlet tokens, so the PowerShell/cmd aliases agents
emit most —rm -Recurse -Force .\pipelines,del /s /q C:\src,
rd /s C:\dir— still evaluated as POSIX and failed open. The hook now also
widens when a segment leads with a destructive alias (rm/ri/del/rd/
rmdir/erase) and carries a Windows-shell-only argument — a
single-dash PowerShell parameter word (-Recurse/-Force/-Path/…, a
≥3-char prefix that POSIXrmnever accepts) or a cmd switch (/s,/q).
A plain POSIXrm -rf ./buildhas neither and keeps the Posix dialect.
(2) The oversized-input fail-closed path (try_deny_oversized_input, taken
when a payload exceedsmax_command_bytes) resolved each scan window with an
unrefined dialect, so padding a mislabeled PowerShell payload past the
limit reopened the same hole. That path now applies the identical
refine_shell_dialectwidening per window. -
redirect-truncate-root-homeknows the macOS home spelling (#325).
The sensitive-path alternation carried/homebut not/Users, so `echo x/Users//.zshrc
— the absolute form tools actually hand agents — was allowed while> ~/.zshrc,> $HOME/.zshrc, and even the *less* certain> $D/.zshrcwere blocked./Usersnow sits in the shared alternation of every rule that uses it (redirect-truncate-root-home,mv-sensitive-source-root-home,find -delete,unlink,truncate,shred,tar --remove-files,dd of=, and the cp/ln/rsync copy-then-delete chains), matching the platform parity therm` rules
already had (#247). -
Write-safe character devices no longer deny as truncating redirects
(#324).> /dev/null,> /dev/zero,> /dev/full, and> /dev/tty
are carved out ofredirect-truncate-*: these are always character
devices, so opening them withO_TRUNCcannot destroy persistent data,
and each false block cost a human round-trip./dev/st0(tape) and
/dev/tty0//dev/ttysNNN(other terminals) stay blocked.
Correction (fresh-eyes review): the carve-out originally also covered
/dev/stdout,/dev/stderr, and/dev/fd/[0-2], on the false premise that
they too are character devices. They are symlinks to whatever fd 0/1/2
currently point at, which may be a regular file (afterexec > logfileor
an inherited redirect) — whereO_TRUNCtruncates that real file. Those
three are no longer carved out; the guard blocks them under its
zero-false-negatives posture. -
Quoted
>bytes inside an inline interpreter payload no longer read as
redirect syntax when the segment carries a real redirect (#317).sh -c "echo 'a => %s'" 2>&1(and the2>/dev/null/ literal/tmptarget /
python3 -c/node -evariants) allowed: theredirect-truncate-*match
offset is now re-derived against the payload's own quoting, extending the
6f1aa5a treatment from$/backtick to the redirect operator. A live>
inside the payload (bash -c "cat x > $T"), a dynamic or sensitive target
outside it, and multi-segment payloads all keep the fail-closed deny. -
PowerShell
2>$nullis the null device, not a dynamic path (#321).
Under a proven PowerShell dialect, a command whose every redirect target is
the read-only$nullautomatic variable (case-insensitive,${null}
included) no longer denies asredirect-truncate-dynamic-path.$nullFile,
$none, mixed targets, and POSIX/Unknown dialects — wherenullis an
ordinary assignable variable — stay denied. -
Grok Build's documented shell tool name is accepted (#319). Grok's hooks
guide names the shell toolrun_terminal_command; dcg only accepted the
abbreviatedrun_terminal_cmd, so the documented envelope was answered with
a "skip" — a silent fail-open on the exact path Grok uses. Both spellings now
classify as the Grok protocol and evaluate the command. -
redirect-truncate-*denials now carry redirect-specific suggestions
(#316/#317). The suggestion registry previously served the recursive-rm
set (ls -lapreview /rm -ri/ move-to-trash) for redirect rules — a non
sequitur on a redirect denial. The rules now suggest inspecting the resolved
target, appending instead of truncating, redirecting to a literal temp path,
and backing up first. Therm-rf-root-homeexplanation also no longer
recommendsrm -rf /path/to/specific/directory— a command dcg itself
denies — and points at the literal-temp and interactive (rm -ri) forms it
actually allows (#316).
Dependencies
- Applied dependabot #315 directly:
async-trait0.1.92,ast-grep-core/
ast-grep-language0.45.1,rusqlite0.40.2 (+libsqlite3-sys0.38.2).