Added
-
macOS
diskutilcoverage insystem.disk(#305).diskutil eraseDisk,
eraseVolume,reformat,zeroDisk,randomDisk, andsecureErasedeny
asdiskutil-erase;partitionDisk,splitPartition,mergePartitions,
andresetFusionasdiskutil-partition; andapfs deleteContainer/deleteVolume/eraseVolume/deleteSnapshotas
diskutil-apfs-delete— all Critical, all case-insensitive because diskutil
accepts any verb casing. Read-only verbs (list,info,activity,
apfs list/listSnapshots) stay allowed, and a read-only verb cannot mask
a chained destructive verb on the same line. -
The canonical fork bomb is blocked (#302).
:(){ :|:& };:and
word-named variants deny ascore.filesystem:fork-bomb(Critical). The
regex uses backreferences to require the same identifier in all three
positions, so ordinary function definitions that pipe two different
commands do not match. Because the shape necessarily spans|,&, and
;, the rule joins the whole-command cross-segment pass, and the shell
function-definition operator()(pure syntax, invisible to span-based
keyword gating) is now a recognized quick-reject signal for the pack.
Differently shaped bombs (while true; do (x) & done) remain out of scope —
the regex family cannot enforce those without unbounded false positives. -
Proven timestamped sibling-backup
mvis allowed (#308). The exact
cross-harness installer shape —STAMP=$(date +%Y%m%d%H%M%S); BACKUP="<src>.backup-$STAMP"; mv "<src>" "$BACKUP"— is a reversible
sibling rename: the destination is proven to be the source plus a
digits-only suffix, and the two assignments must be the segments
immediately before themvso nothing can mutate them in between. Only
mv-dynamic-pathandmv-sensitive-source-root-homeare narrowed; any
deviation (a different substitution, non-sibling destination, mv options,
extra operands, an intervening segment, traversal, globs, unquoted
destination) keeps the fail-closed deny. -
database.bigquerypack — thebqCLI and GoogleSQL. 11 CLI rules and
21 GoogleSQL rules. Three BigQuery specifics drive them, and each one makes a
naive port of the PostgreSQL/Snowflake packs wrong: a dataset is aSCHEMA
in GoogleSQL, soDROP SCHEMAis the dataset-level catastrophe rather than a
namespace tidy-up (Critical); GoogleSQL requires aWHEREclause on
DELETE/UPDATE, soWHERE TRUEis the idiomatic full-table spelling and a
delete-without-whererule modelled on PostgreSQL would never fire; and time
travel (2–7 days) is the only undo, so--max_time_travel_hoursand
SET OPTIONS(expiration_timestamp)destroy the recovery path itself and are
destructive in their own right. BigQuery ML models get their own rule —
they are not covered by time travel and cost hours of training to rebuild.
CLI rules are scoped withexecutables = ["bq"]and abq-anchored regex,
becauseexecutablesis enforced in the evaluator rather than in
Pack::check. Opt-in like every otherdatabase.*pack; joins the
careful_company_running_windowspreset; recommended automatically when a
project depends ongoogle-cloud-bigquery,@google-cloud/bigquery,
pandas-gbq, orsqlalchemy-bigquery.Evaluator wiring makes the pack indirect, so its unscoped GoogleSQL rules
cannot claim the SQL inside another client's invocation:database.bigquery
sorts first within tier 7, so without itsnow sql -q "DROP TABLE ..."would
have been reported as a BigQuery rule. Regression cases pin both directions.Implemented independently from the analysis in closed PR #295, per the
project's no-outside-merges policy. -
dcg doctor --strict(#296).doctorexited0unconditionally, including
when it had just reported"ok": false— sodcg doctor || handle_failurewas
dead code and a provisioning run got a green signal from a guard doctor itself
had classified as broken.--strictmakes the exit status carry the verdict.
It is opt-in, matchingpack validate --strict; the default stays0for
anyone already calling doctor in a pipeline.
Changed
- The block message no longer echoes the command twice (#299). The command
appeared in both theTip: dcg explain "<cmd>"line and aCommand: <cmd>
line, makinglen(permissionDecisionReason) = 2*len(command) + 499exactly. A
hook decision lands in the agent's transcript and is replayed on every later
turn, so the second echo was paid for repeatedly while telling the reader
nothing the first did not — the agent just wrote that command. TheTip:copy
is kept because it is also actionable. No verdict changes. platform.githubrules now carry explanations and suggestions (#300). All
sixteen destructive rules were built with the 3-arg macro form, so every
denial rendered as "No additional explanation is available yet. See pack
documentation for details." Each rule now explains what is actually lost and
names the safer spelling — for the raw-API catch-all, that includes pointing at
the first-classgh issue edit --remove-parent/--remove-sub-issueverbs,
which dcg allows outright. A new pack test enforces this going forward.
Fixed
-
chown -R/chmod -R/setfacl -Ron bare/and/homenow deny
(#301). Two independent bugs insystem.permissions: the protected-path
regex tail(?:$|bin|...)\bcould never match a bare/(the\bafter
the end-anchor has no word character to bound), and/homewas missing
from the protected list entirely.chmod-777had masked the 777 case,
which is why the gap survived the obvious test./homeis scoped to the
home root or a whole single-user home (/home,/home/user— where
~/.sshlives), so a routinechmod -R /home/user/projecton a project
directory stays allowed whilechmod -R /home(which locks out every
account) is blocked. -
pnpm/npm/yarnpublish rules require subcommand position (#306).
pnpm run build; bun ./publish-snapshot.ts,pnpm run build --reporter "publish", andpnpm run build publishno longer deny:publishmust be
reachable through option tokens only, so argument data and later shell
segments are not publication. Because the pack regexes run on the sanitized
command — which has already stripped the quotes that distinguish
pnpm --reporter "publish"(a value) frompnpm --reporter publish— a
match is confirmed against the original command by a quoting-aware gate
(invokes_publish_subcommand): an unquotedpublishin subcommand position
is publication, a quoted one is data. Real forms (pnpm -r publish,
pnpm recursive publish,--filter <ws> publish,yarn workspace <ws> publish,yarn npm publish,pnpm.cmd) still deny, and an unquoted
option value namedpublishstays fail-closed, in every dialect. The
*-dry-runsafe patterns are segment-bounded so a dry-run in one segment
cannot mask a later one. -
Single-quoted
$/backtick/backslash inmvpaths are literal (#307).
mv './$ROOT' /tmp/xis data, not expansion:mv-dynamic-pathstands
down only when every dynamic marker in the command is inside a POSIX
single-quoted span. One active marker anywhere — double quotes, unquoted
variables, a quote-manipulating backslash — keeps the deny. -
dcg --robot testhonors the hook evaluation budget (#309). Robot mode
is an agent-integration boundary, so it now enforces the configured
timeout and answers with bounded{"decision":"indeterminate", "source":"analysis_budget"}JSON without requiring the human-facing
--enforce-budgetdiagnostic flag (which stays opt-in for interactive
dcg test). -
pwsh --version/--helpand read-only-cvariable expressions are
allowed (#304). pwsh accepts exactly two GNU-style spellings, both
print-and-exit; they no longer land in the unknown-host-option refusal.
And a-Commandpayload that is exactly one variable read with property
accesses ($PSVersionTable.PSVersion,$env:PATH) invokes nothing, so it
no longer trips the runtime-expansion refusal — invoking, indexing,
subexpressions, or any second statement stays fail-closed. (-Filewas
already fixed on main;SP=…; pwsh -c "…"likewise.) -
Backing up the agent's hook config is a read, not tampering (#313).
Copy-Item ~/.claude/settings.json <backup>no longer denies:
copy-family verbs moved out ofagent-hook-config-tamperinto a new
agent-hook-config-overwriterule that fires only when the config path is
the write side (-Destinationor positional destination). Deleting,
rewriting, moving, or renaming the live config still denies, as does
copying anything onto it. -
bash -cpayloads keep their own quote context (#288 follow-up).
bash -lc 'grep -n "rm -rf /" notes.md'was denied while the bare inner
command was correctly allowed: the match landed inside the inline payload,
whoseInlineCodeclassification dropped the payload's internal quoting.
A core-rule match inside a POSIX-shell inline payload is now re-classified
against the payload itself, so it resolves exactly like the bare inner
command — andbash -c 'rm -rf /'still denies, because the payload
classifies it as live code. -
A pathological
ghcommand line could fail OPEN. The shared option-prefix
inplatform.githubused\S+for an option's value, which also matches a
flag token — so ingh -a -b -c …each token could parse either as a new
option or as the previous one's value, giving exponentially many parses.
These patterns carry a lookahead and therefore run on the backtracking
engine, whereCompiledRegex::is_matchmaps a backtrack-limit error to
false. Adopted the unambiguous shape the database packs already use
([^-\s;&|][^\s;&|]*). -
dcg doctor's pretty renderer computed a wrong verdict, which now
matters because--strictderives the exit status from it — and pretty is
what runs in CI, since rich output is disabled without a TTY. A failed
config write printed an error without counting it, and the Grok
"NOT REGISTERED" branch incrementedfixedwithout ever incrementing
issues, corrupting theissues == 0 || (fix && fixed == issues)
arithmetic in both directions: masking a genuinely unfixed issue, and
reporting failure on a fully repaired machine. -
doctor --fixcould buy off a problem it could not fix. Creating the
default config counted afixedwith no matchingissues(a missing config
is a warning, not an issue), so that one success cancelled a genuinely
unfixed issue through thefixed == issuesequality: on a machine whose hook
was misconfigured and unwritable,dcg doctor --fix --strictreported "All
issues fixed!" and exited 0. Both renderers now count the issue each repair
resolves, and a test assertsfixed <= issues. -
doctor --strictgave different answers per--format. The Grok
registration check existed only in the pretty renderer, so a machine with
Grok present and unwired exited 1 withdcg doctor --strictand 0 with
--format json. The check now lives in the shared report (grok_hook) and a
test pins that both renderers agree. -
gh repo deleterecommended a command dcg itself blocks. Its first
suggested alternative wasgh repo archive, which the same pack denies, so
the agent bounced between two denials. The suggestions now lead with a
runnable command and say plainly that archiving is gated too. A pack test
asserts no rule's first suggestion is blocked by its own pack. -
gh-api-delete-repois no longer a misnamed catch-all (#300). The rule
matched anygh api ... DELETE, not repository deletion, while its name is
what surfaces asrule_idin the history DB,dcg stats,dcg suggest-allowlist, and allowlist entries. The catch-all is now
gh-api-delete-generic, andgh-api-delete-repomatches what its name says:
DELETE /repos/{owner}/{repo}. Both are still denied, so no command changes
verdict. Breaking for persisted state: an allowlist or[rules]entry for
platform.github:gh-api-delete-reponow permits only repository deletion
rather than every raw-API DELETE — a tightening — and history rows written
before this release keep the oldrule_id. -
False positives:
pwsh -File <script.ps1>(and its abbreviation-f) is
no longer denied as an unverifiable launcher envelope.-Filewas missing
from the PowerShell host-option table entirely, so it resolved toUnknown
and hit the fail-closed branch for unrecognized dash tokens — while the
positionalpwsh <script.ps1>form, which is the same operation, and
-Command, which is strictly more dangerous, were both allowed.-Fileis
now a first-class option that ends host-option parsing (later tokens are
script arguments, not host options).-File -still refuses, because a
script read from stdin is no more inspectable than-Command -. Routing it
through the shared option table also meanspwsh -f -is now recognized as
reading a script from stdin, which the previous exact-match-filecheck in
the pipeline analyzer missed.