dcg (Destructive Command Guard)

A high-performance hook for AI coding agents that blocks destructive commands before they execute, protecting your work from accidental deletion across Claude Code, Codex CLI, Gemini CLI, Copilot CLI, VS Code Copilot Chat, Cursor, Hermes Agent, Grok (xAI), Posit Assistant, Oh My Pi, and related tools.
Supported: Claude Code, Codex CLI 0.125.0+, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat, Cursor IDE, Hermes Agent, Posit Assistant (Positron/RStudio extension, standalone server, and pa terminal client), Grok (xAI) (native ~/.grok/hooks/ plus Claude compatibility layer), Antigravity CLI (agy) (native ~/.gemini/config/hooks.json via dcg install --agy), OpenCode (native tool.execute.before plugin via dcg install --opencode — see docs/opencode-integration.md), Oh My Pi (omp) (native tool_call extension via dcg install --omp), Crush (native hooks.PreToolUse entry in crush.json via dcg install --crush — see docs/crush-integration.md), Reasonix (native hooks.PreToolUse entry in settings.json via dcg install --reasonix — see docs/reasonix-integration.md), Pi (via extension recipe), Aider (limited—git hooks only), Continue (detection only)
Quick Install
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/install.sh?$(date +%s)" | bash -s -- --easy-mode
Works on Linux, macOS, and Windows via WSL. Auto-detects your platform, downloads the right binary, and configures supported agent hooks including Claude Code, Codex CLI, Gemini CLI, GitHub Copilot CLI, VS Code Copilot Chat (through VS Code's Claude-hook compatibility), Cursor IDE, Hermes Agent, Posit Assistant, Oh My Pi, and Grok (xAI) (via dcg install --grok for a native ~/.grok/hooks/dcg.json, or via the Claude compatibility layer automatically picked up by Grok). For native Windows, use the PowerShell installer below.
Windows (native, PowerShell)
& ([scriptblock]::Create((irm "https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/install.ps1"))) -EasyMode -Verify
Installs native dcg.exe, verifies the mandatory SHA256 checksum, verifies the release's long-lived minisign signature when minisign is available, and verifies Sigstore/cosign provenance when both cosign and a trusted bundle are available. It adds dcg to your User PATH (-EasyMode), runs a self-test (-Verify), and configures detected agent hooks for Claude Code, Codex CLI, Gemini CLI, GitHub Copilot CLI, Cursor IDE, Hermes Agent, Posit Assistant, and Oh My Pi. Copilot is configured at the user level under %COPILOT_HOME%\hooks (or %USERPROFILE%.copilot\hooks) so every workspace is protected. On Windows the windows.filesystem and windows.system packs are on by default, so del /s, rd /s, Remove-Item -Recurse (with or without -Force), format, and vssadmin delete shadows are blocked out of the box. Pin a version with -Version vX.Y.Z; use -RequireMinisign to fail closed if the sidecar or verifier is unavailable.
TL;DR
The Problem: AI coding agents (Claude, Codex, Gemini, Copilot, etc.) occasionally run catastrophic commands like git reset --hard, rm -rf ./src, or DROP TABLE users—destroying hours of uncommitted work in seconds.
The Solution: dcg is a high-performance hook that intercepts destructive commands before they execute, blocking them with clear explanations and safer alternatives.
Why Use dcg?
| Feature | What It Does |
|---|---|
| Zero-Config Protection | Blocks dangerous git/filesystem commands out of the box |
| 50+ Security Packs | Databases, Kubernetes, Docker, AWS/GCP/Azure, Terraform, and more |
| Sub-Millisecond Latency | SIMD-accelerated filtering—you won't notice it's there |
| Heredoc/Inline Script Scanning | Catches python -c "os.remove(...)" and embedded shell scripts |
| Smart Context Detection | Won't block grep "rm -rf" (data) but will block rm -rf / (execution) |
| Rich Terminal Output | Human-readable denial panels, rule context, and suggestions on stderr |
| Agent-Safe Streams | Machine-readable hook output stays on stdout while rich UI stays on stderr |
| Native Codex Support | Codex CLI 0.125.0+ receives a minimal stdout JSON denial that current clients enforce reliably |
| Graceful Degradation | Plain output for CI, pipes, dumb terminals, and no-color environments |
| Scan Mode for CI | Pre-commit hooks and CI integration to catch dangerous commands in code review |
| Bounded Failure Policy | Analysis timeouts become explicit review/block outcomes; malformed raw hook envelopes remain auditable and configurable |
| Explain Mode | dcg explain "command" shows exactly why something is blocked |
Quick Example
# AI agent tries to run:
$ git reset --hard HEAD~5
# dcg intercepts and blocks:
════════════════════════════════════════════════════════════════
BLOCKED dcg
────────────────────────────────────────────────────────────────
Reason: git reset --hard destroys uncommitted changes
Command: git reset --hard HEAD~5
Tip: Consider using 'git stash' first to save your changes.
════════════════════════════════════════════════════════════════
Enable More Protection
# ~/.config/dcg/config.toml
[packs]
enabled = [
"database.postgresql", # Blocks DROP TABLE, TRUNCATE
"kubernetes.kubectl", # Blocks kubectl delete namespace
"cloud.aws", # Blocks aws ec2 terminate-instances
"containers.docker", # Blocks docker system prune
]
Agent-Specific Profiles
dcg automatically detects which AI coding agent is invoking it and can apply
agent-specific configuration. The trust_level field is an advisory label
recorded in JSON output and logs — it does not directly change rule evaluation.
Behavioral differences come from the other profile fields:
| Option | Effect |
|---|---|
disabled_packs |
Removes rule packs from evaluation |
extra_packs |
Adds rule packs to evaluation |
additional_allowlist |
Adds command patterns that bypass deny rules |
disabled_allowlist |
When true, ignores all allowlist entries |
# Trust Claude Code more — wider allowlist, fewer packs
[agents.claude-code]
trust_level = "high"
additional_allowlist = ["npm run build", "cargo test"]
disabled_packs = ["kubernetes"]
# Oh My Pi has its own canonical profile (distinct from legacy Pi)
[agents.omp]
trust_level = "medium"
extra_packs = ["strict_git"]
# Restrict unknown agents — extra rules, no allowlist bypass
[agents.unknown]
trust_level = "low"
extra_packs = ["strict_git", "database"] # real pack / category IDs (see `dcg packs`)
disabled_allowlist = true
extra_packs/disabled_packstake the same pack and category IDs as[packs] enabled/disabled— a category ID like"database"expands to everydatabase.*sub-pack. Use IDs listed bydcg packsor indocs/packs/README.md;"paranoid"is a graduation mode, not a pack, so enable the realstrict_gitpack for stricter git rules.
See docs/agents.md for full documentation on supported agents, trust levels, and configuration options.
Codex Support
dcg now treats Codex CLI as a first-class hook target, not just a Claude-shaped
compatibility path. The installer configures Codex CLI 0.125.0+ automatically
when it detects codex on PATH or an existing ~/.codex/ directory.
| Codex behavior | dcg handling |
|---|---|
| Hook config | Merges a PreToolUse Bash hook into ~/.codex/hooks.json |
| Denied command | Exits 0 with a minimal hookSpecificOutput denial on stdout; human warning stays on stderr |
| Allowed command | Exits 0 with empty stdout and stderr |
| Existing hooks | Preserves coexisting hooks, keeps dcg first for Bash, and refuses to overwrite malformed JSON |
| Validation | Covered by subprocess protocol tests plus an opt-in real Codex E2E harness |
Codex's hook input is intentionally close to Claude Code's, but Codex rejects
unknown fields in hook output. dcg detects Codex payloads from the non-empty
turn_id field and emits only Codex's documented denial fields so a blocked
command is reported as blocked rather than as a failed hook. See
docs/codex-integration.md for protocol details,
manual probes, and troubleshooting.
Origins & Authors
This project began as a Python script by Jeffrey Emanuel, who recognized that AI coding agents, while incredibly useful, occasionally run catastrophic commands that destroy hours of uncommitted work. The original implementation was a simple but effective hook that intercepted dangerous git and filesystem commands before execution.
- Jeffrey Emanuel - Original concept and Python implementation (source); substantially expanded the Rust version with the modular pack system (50+ security packs), heredoc/inline-script scanning, the three-tier architecture, context classification, allowlists, scan mode, and the dual regex engine
- Darin Gordon - Initial Rust port with performance optimizations
The initial Rust port by Darin maintained pattern compatibility with the original Python implementation while adding sub-millisecond execution through SIMD-accelerated filtering and lazy-compiled regex patterns. Jeffrey subsequently expanded the Rust codebase dramatically to add the features described above.
Escape Hatch / Bypass
If dcg is blocking something you genuinely need to run:
| Method | Scope | How |
|---|---|---|
| Env var bypass | Single command | DCG_BYPASS=1 |
| Allow-once code | Single command | Copy the short code from the block message, run dcg allow-once |
| Permanent allowlist | Rule or command | dcg allowlist add core.git:reset-hard -r "reason" |
| Remove the hook | All commands | Delete or comment out the dcg entry in ~/.claude/settings.json (or equivalent for your agent) |
DCG_BYPASS=1 disables all protection for that invocation. Use it sparingly and prefer allowlists for recurring needs.
Modular Pack System
dcg uses a modular "pack" system to organize destructive command patterns by category. Packs can be enabled or disabled in the configuration file.
Category IDs expand to their sub-packs. Listing a bare category in enabled
turns on every pack under it: enabled = ["database"] activates
database.postgresql, database.mysql, and the rest of that category. You can
still drop a single sub-pack with disabled = ["database.redis"]. The same
expansion applies to agent-profile extra_packs / disabled_packs. Always use
real pack or category IDs from dcg packs / docs/packs/README.md — a name like
"paranoid" is a graduation mode, not a pack.
- Full pack ID index:
docs/packs/README.md - Canonical descriptions + pattern counts:
dcg packs --verbose
Enabled by default (no config file)
With no config file present, dcg enables only the packs that guard against the most catastrophic, unrecoverable mistakes:
core.filesystem- Dangerous recursivermoperations and equivalent filesystem destruction outside literal temp subdirectories (always enabled; cannot be removed from evaluation)core.git- Destructive git commands that lose uncommitted work, rewrite history, or destroy stashes (always enabled; cannot be removed from evaluation)system.disk-mkfs,dd-to-device,fdisk,parted,mdadm,lvmremoval,wipefs(on by default; opt out withdisabled = ["system.disk"])
"Cannot be removed" is not the same as "cannot be relaxed." A core.* pack
always evaluates, so disabled = ["core.filesystem"] is ignored — but what
dcg does with a match is policy, and policy is yours.
Relaxing a critical rule takes a per-rule entry. A broad warn or log —
whether written as [policy.packs] or as [policy] default_mode — is silently
raised back to deny for any rule whose severity is critical, and dcg does
not report that it ignored the setting. Most of what core.filesystem and
core.git exist to stop is exactly that severity, so the broad form alone will
not do what it looks like it does:
# Relaxes only the high/medium rules. `rm -rf ~/work` still hard-denies,
# because core.filesystem:rm-rf-root-home is critical.
[policy.packs]
"core.filesystem" = "warn"
# Relaxes that one critical rule. This is the form that actually works.
[policy.rules]
"core.filesystem:rm-rf-root-home" = "warn"
warn lets the command run and records the decision; log does the same
silently; ask requests operator review where the hook protocol supports it.
Use dcg explain --format json '' and read mode to confirm which
mode a rule actually resolved to before relying on it. See
Configuration for the constraint and
Graduated Response for the severity ladder.
On Windows, two additional packs are on by default so a fresh install blocks the catastrophic native-Windows operations with no config:
windows.filesystem- cmddel /s,rd /s,format :and PowerShellRemove-Item -Recurse(with or without-Force; aliases included),Clear-Content,Clear-RecycleBin(default-on on Windows only; opt out withdisabled = ["windows.filesystem"]or["windows"])windows.system-vssadmin delete shadows/wmic shadowcopy delete(Volume Shadow Copy destruction),diskpart,Format-Volume,Clear-Disk,Remove-Partition,cipher /w,bcdedit /delete(default-on on Windows only; opt out withdisabled = ["windows.system"]or["windows"])
The broader windows.misc (reg delete, net user /delete, wsl --unregister, robocopy /MIR) and
windows.powershell (registry/provider deletes, Remove-LocalUser, Disable-ComputerRestore, Remove-VM)
packs are opt-in on every platform. On Unix the windows.* packs are registered but off by default; enable
them (e.g. to scan committed .ps1/.cmd scripts in CI) via [packs] enabled = ["windows"].
Every other pack — including database.postgresql and containers.docker — is
opt-in and is not active until a config file enables it. Running dcg init
writes a starter ~/.config/dcg/config.toml whose [packs] enabled list turns on
database.postgresql and containers.docker as common examples, but that is a
generated starter config, not the no-config default. Enable any pack below by adding
it to [packs] enabled — see Enable More Protection.
Storage Packs
storage.s3- Protects against destructive S3 operations like bucket removal, recursive deletes, and sync --delete.storage.gcs- Protects against destructive GCS operations like bucket removal, object deletion, and recursive deletes.storage.minio- Protects against destructive MinIO Client (mc) operations like bucket removal, object deletion, and admin operations.storage.azure_blob- Protects against destructive Azure Blob Storage operations like container deletion, blob deletion, and azcopy remove.
Remote Packs
remote.rsync- Protects against destructive rsync operations like --delete and its variants.remote.scp- Protects against destructive SCP operations like overwrites to system paths.remote.ssh- Protects against destructive SSH operations like remote command execution and key management.
Database Packs
database.postgresql- Protects against destructive PostgreSQL operations like DROP DATABASE, TRUNCATE, and dropdb.database.mysql- MySQL/MariaDB guard.database.mongodb- Protects against destructive MongoDB operations like dropDatabase, dropCollection, and remove without criteria.database.redis- Protects against destructive Redis operations like FLUSHALL, FLUSHDB, and mass key deletion.database.sqlite- Protects against destructive SQLite operations like DROP TABLE, DELETE without WHERE, and accidental data loss.database.snowflake- Protects modernsnow sqlinline queries, files, stdin, nested sources, destructive data operations, pipelines, warehouses, and account privileges.database.supabase- Protects against destructive Supabase CLI operations including database resets, migration rollbacks, function/secret/storage deletion, project removal, and infrastructure changes.database.databricks- Protects against destructive Databricks CLI operations like account workspace deletion, bundle destroy, recursive workspace/fs deletion, permanent cluster deletion, secret-scope removal, and arbitrary REST DELETE calls.database.bigquery- Protects thebqCLI and GoogleSQL against dataset drops (DROP SCHEMA), table overwrites, unfiltered DML (WHERE TRUEis GoogleSQL's full-table idiom), and settings that shorten the time-travel recovery window.
Container Packs
containers.docker- Protects against destructive Docker operations like system prune, volume prune, and force removal.containers.compose- Protects against destructive Docker Compose operations like down -v which removes volumes.containers.podman- Protects against destructive Podman operations like system prune, volume prune, and force removal.
Kubernetes Packs
kubernetes.kubectl- Protects against destructive kubectl operations like delete namespace, drain, and mass deletion.kubernetes.helm- Protects against destructive Helm operations like uninstall and rollback without dry-run.kubernetes.kustomize- Protects against destructive Kustomize operations when combined with kubectl delete or applied without review.
Cloud Provider Packs
cloud.aws- Protects against destructive AWS CLI operations like terminate-instances, delete-db-instance, and s3 rm --recursive.cloud.azure- Protects against destructive Azure CLI operations like vm delete, storage account delete, and resource group delete.cloud.gcp- Protects against destructive gcloud operations like instances delete, sql instances delete, and gsutil rm -r.
CDN Packs
cdn.cloudflare_workers- Protects against destructive Cloudflare Workers, KV, R2, and D1 operations via the Wrangler CLI.cdn.cloudfront- Protects against destructive AWS CloudFront operations like deleting distributions, cache policies, and functions.cdn.fastly- Protects against destructive Fastly CLI operations like service, domain, backend, and VCL deletion.
API Gateway Packs
apigateway.apigee- Protects against destructive Google Apigee CLI and apigeecli operations.apigateway.aws- Protects against destructive AWS API Gateway CLI operations for both REST APIs and HTTP APIs.apigateway.kong- Protects against destructive Kong Gateway CLI, deck CLI, and Admin API operations.
Infrastructure Packs
infrastructure.ansible- Protects against destructive Ansible operations like dangerous shell commands and unchecked playbook runs.infrastructure.atmos- Protects against destructive Atmos operations like terraform deploy (auto-approve), clean, destroy, state rm/taint, and helmfile destroy.infrastructure.pulumi- Protects against destructive Pulumi operations like destroy and up with -y (auto-approve).infrastructure.terraform- Protects against destructive Terraform operations like destroy, taint, and apply with -auto-approve.
System Packs
system.disk- Protects against destructive disk operations including dd to devices, mkfs, partition table modifications (fdisk/parted), RAID management (mdadm), btrfs filesystem operations, device-mapper (dmsetup), network block devices (nbd-client), and LVM commands (pvremove, vgremove, lvremove, lvreduce, pvmove).system.permissions- Protects against dangerous permission changes like chmod 777, recursive chmod/chown on system directories.system.services- Protects against dangerous service operations like stopping critical services and modifying init configuration.
CI/CD Packs
cicd.circleci- Protects against destructive CircleCI operations like deleting contexts, removing secrets, deleting orbs/namespaces, or removing pipelines.cicd.github_actions- Protects against destructive GitHub Actions operations like deleting secrets/variables or using gh api DELETE against /actions endpoints.cicd.gitlab_ci- Protects against destructive GitLab CI/CD operations like deleting variables, removing artifacts, and unregistering runners.cicd.jenkins- Protects against destructive Jenkins CLI/API operations like deleting jobs, nodes, credentials, or build history.
Secrets Management Packs
secrets.aws_secrets- Protects against destructive AWS Secrets Manager and SSM Parameter Store operations like delete-secret and delete-parameter.secret_disclosure- Exact opt-in protection against secret-manager commands that expose credential values through agent-visible output or agent-chosen files; injection commands such asinfisical run,op run, anddoppler runremain allowed. It is intentionally outside thesecrets.*category so existingenabled = ["secrets"]configurations do not change policy on upgrade.secrets.doppler- Protects against destructive Doppler CLI operations like deleting secrets, configs, environments, or projects.secrets.infisical- Protects against deleting Infisical secrets, folders, and dynamic-secret leases, plus resetting local Infisical configuration.secrets.onepassword- Protects against destructive 1Password CLI operations like deleting items, documents, users, groups, and vaults.secrets.vault- Protects against destructive Vault CLI operations like deleting secrets, disabling auth/secret engines, revoking leases/tokens, and deleting policies.
Provider packs preserve dcg's default destructive-operation scope: read commands remain allowed. Teams that also treat transcript disclosure as destructive can enable the separate policy explicitly:
[packs]
enabled = ["secrets.infisical", "secret_disclosure"]
With secret_disclosure enabled, value-emitting reads such as infisical secrets get, infisical export, op read, doppler secrets download, vault kv get, aws secretsmanager get-secret-value, aws secretsmanager batch-get-secret-value, and decrypted SSM reads are blocked. Metadata
inspection, CLI help, and direct process injection remain available.
The opt-in careful_company_running_windows preset also includes both new packs
as deliberate members of its pinned secret-store policy.
Platform Packs
platform.azure_devops- Protects against destructiveazure-devopsAzure CLI extension operations acrossaz devops,az repos,az pipelinesandaz boards: deleting team projects, repositories, refs, branch policies, pipelines, variable groups, wikis, teams, service connections and work items, removing users and group memberships, resetting permission ACLs, and issuing arbitrary state-changingaz devops invokeREST calls.az artifactsexposes no destructive command and carries no rule. Read-only verbs and ordinary development flow are untouched.platform.github- Protects against destructive GitHub CLI operations like changing repository visibility or deleting repositories, gists, releases, or SSH keys.platform.gitlab- Protects against destructive GitLab platform operations like deleting projects, releases, protected branches, and webhooks.platform.kamal- Protects against destructive Kamal 2.x operations that tear down the stack (kamal remove), delete accessory data directories (kamal accessory remove), drop proxy routing, take the app offline, or prune the images thatkamal rollbackrelies on.platform.modal- Protects against destructive Modal serverless platform operations like recursive volume removal, app stops with--force, and secret deletion.platform.railway- Protects against destructive Railway CLI and Public API operations that can delete projects, environments, services, functions, volumes, variables, or deployments.
DNS Packs
dns.cloudflare- Protects against destructive Cloudflare DNS operations like record deletion, zone deletion, and targeted Terraform destroy.dns.generic- Protects against destructive or risky DNS tooling usage (nsupdate deletes, zone transfers).dns.route53- Protects against destructive AWS Route53 DNS operations like hosted zone deletion and record set DELETE changes.
Email Packs
email.mailgun- Protects against destructive Mailgun API operations like domain deletion, route deletion, and mailing list removal.email.postmark- Protects against destructive Postmark API operations like server deletion, template deletion, and sender signature removal.email.sendgrid- Protects against destructive SendGrid API operations like template deletion, API key deletion, and domain authentication removal.email.ses- Protects against destructive AWS Simple Email Service operations like identity deletion, template deletion, and configuration set removal.
Feature Flag Packs
featureflags.flipt- Protects against destructive Flipt CLI and API operations.featureflags.launchdarkly- Protects against destructive LaunchDarkly CLI and API operations.featureflags.split- Protects against destructive Split.io CLI and API operations.featureflags.unleash- Protects against destructive Unleash CLI and API operations.
Load Balancer Packs
loadbalancer.elb- Protects against destructive AWS Elastic Load Balancing (ELB/ALB/NLB) operations like deleting load balancers, target groups, or deregistering targets from live traffic.loadbalancer.haproxy- Protects against destructive HAProxy load balancer operations like stopping the service or disabling backends via runtime API.loadbalancer.nginx- Protects against destructive nginx load balancer operations like stopping the service or deleting config files.loadbalancer.traefik- Protects against destructive Traefik load balancer operations like stopping containers, deleting config, or API deletions.
Messaging Packs
messaging.kafka- Protects against destructive Kafka CLI operations like deleting topics, removing consumer groups, resetting offsets, and deleting records.messaging.nats- Protects against destructive NATS/JetStream operations like deleting streams, consumers, key-value entries, objects, and accounts.messaging.rabbitmq- Protects against destructive RabbitMQ operations like deleting queues/exchanges, purging queues, deleting vhosts, and resetting cluster state.messaging.sqs_sns- Protects against destructive AWS SQS and SNS operations like deleting queues, purging messages, deleting topics, and removing subscriptions.
Monitoring Packs
monitoring.datadog- Protects against destructive Datadog CLI/API operations like deleting monitors and dashboards.monitoring.newrelic- Protects against destructive New Relic CLI/API operations like deleting entities or alerting resources.monitoring.pagerduty- Protects against destructive PagerDuty CLI/API operations like deleting services and schedules (which can break incident routing).monitoring.prometheus- Protects against destructive Prometheus/Grafana operations like deleting time series data or dashboards/datasources.monitoring.splunk- Protects against destructive Splunk CLI/API operations like index removal and REST API DELETE calls.
Payment Packs
payment.braintree- Protects against destructive Braintree/PayPal payment operations like deleting customers or cancelling subscriptions via API/SDK calls.payment.square- Protects against destructive Square CLI/API operations like deleting catalog objects or customers (which can break payment flows).payment.stripe- Protects against destructive Stripe CLI/API operations like deleting webhook endpoints and customers, or rotating API keys without coordination.
Search Engine Packs
search.algolia- Protects against destructive Algolia operations like deleting indices, clearing objects, removing rules/synonyms, and deleting API keys.search.elasticsearch- Protects against destructive Elasticsearch REST API operations like index deletion, delete-by-query, index close, and cluster setting changes.search.meilisearch- Protects against destructive Meilisearch REST API operations like index deletion, document deletion, delete-batch, and API key removal.search.opensearch- Protects against destructive OpenSearch REST API operations and AWS CLI domain deletions.
Backup Packs
backup.borg- Protects against destructive borg operations like delete, prune, compact, and recreate.backup.rclone- Protects against destructive rclone operations like sync, delete, purge, dedupe, and move.backup.restic- Protects against destructive restic operations like forgetting snapshots, pruning data, removing keys, and cache cleanup.backup.velero- Protects against destructive velero operations like deleting backups, schedules, and locations.
Windows Packs
Native-Windows (cmd.exe + PowerShell) destructive-command protection. windows.filesystem and
windows.system are default-on on Windows (off/opt-in on Unix); windows.misc and
windows.powershell are opt-in everywhere. All patterns are case-insensitive.
windows.filesystem- Recursive/forced filesystem destruction: cmddel /s,rd /s/rmdir /s,format :; PowerShellRemove-Item -Recurse(with or without-Force;-Forceonly broadens coverage to hidden/read-only items; aliasesrm/del/rd/riincluded),Clear-Content,Clear-RecycleBin. Whitelists PowerShell-WhatIfpreviews only on cmdlets/aliases that honor it, plus temp-dir deletes.windows.system- Catastrophic disk/system operations:vssadmin delete shadowsandwmic shadowcopy delete(Volume Shadow Copy destruction — a ransomware hallmark),diskpart,Format-Volume,Clear-Disk,Remove-Partition,Initialize-Disk/Reset-PhysicalDisk,cipher /w,bcdedit /delete.windows.misc- Registry/account/service/WSL/copy destruction:reg delete,net user|localgroup /delete,sc delete,schtasks /delete,wsl --unregister(destroys a WSL distro),robocopy /MIR(mirror-delete).windows.powershell- Destructive PowerShell cmdlets: registry/provider deletes (Remove-Item HKLM:\,Remove-ItemProperty,Remove-PSDrive),Remove-LocalUser/Remove-LocalGroup,Unregister-ScheduledTask,Disable-ComputerRestore, forcedStop-Computer/Restart-Computer,Remove-VM/Remove-AppxPackage.
Careful Company (Windows) Preset
Every other pack answers "will this command destroy something?". This preset also
answers "is this command sending our data somewhere, or switching off the
controls that watch it?" — the question that matters once an agent runs on a
Windows workstation with tool-permission prompts disabled. The same policy is
applied to statically inspectable commands submitted through either
PowerShell or cmd.exe, including Cmd's caret escaping, control prefixes,
nested cmd /c / call, and command chaining. It is opt-in on every
platform, and one line enables the whole posture:
[packs]
enabled = ["careful_company_running_windows"]
With this exact preset ID enabled, the hook evaluation deadline defaults to
3000 ms instead of the ordinary 1000 ms unless config or
DCG_HOOK_TIMEOUT_MS explicitly supplies another value. This changes only the
time available to reach the same fail-closed decision. Inspect the effective
value and source with dcg config --format json.
That turns on the six sub-packs below and the existing destruction coverage
the same posture needs: the current windows.*, database.* (including
Snowflake), storage.*, remote.*, backup.*, secrets.*, and cloud.*
packs. Membership is an explicit pinned list rather than a prefix rule, so a
future pack added to one of those reused categories does not silently join this
security posture — it has to be added deliberately. (A future
careful_company_running_windows.* sub-pack does join, through ordinary
category expansion.) Any member can be dropped individually with
disabled = ["remote.rsync"].
careful_company_running_windows.email- Sending mail from the workstation:Send-MailMessage,System.Net.Mail.SmtpClient, Outlook COM automation, Microsoft GraphsendMail, transactional mail-API send endpoints,aws ses send-email, SMTP CLI tools (blat,swaks,msmtp,git send-email,curl --mail-rcpt), and persistent forwarding rules (New-InboxRule -ForwardTo,Set-Mailbox -ForwardingSmtpAddress).careful_company_running_windows.chat- Chat and webhook destinations: Slack incoming webhooks and Web API writes, Teams connectors and Power Automate triggers, Discord, Telegram, Google Chat, Twilio, Zapier/IFTTT, PagerDuty, and request catchers such aswebhook.siteandinteract.sh.careful_company_running_windows.upload- HTTP file-upload primitives (-InFile,-Form,curl -T,-F field=@file,--data-binary @file,--post-file,WebClient.UploadFile,GetRequestStream,MultipartFormDataContent, BITS uploads), file-drop/paste services,gh gist create,certreq -Post, and request bodies built from file or clipboard contents.careful_company_running_windows.transfer- Outbound file transfer: scp/sftp/WinSCP to a remote destination, scripted FTP,tftp put, rsync and rclone to a remote, cloud-storage uploads (aws s3 cplocal→s3://,az storage blob upload, azcopy,gsutil cp→gs://, b2/s3cmd/mc/wrangler r2), peer-to-peer senders, WebDAV mounts, and copy LOLBins (esentutl /y,print /D:).careful_company_running_windows.tunnel- Channels that expose the workstation or bypass inspection: ngrok, cloudflared, devtunnel/code tunnel, localtunnel,tailscale funnel,ssh -R/-D, chisel/frp, ncat/netcat/socat, PowerShell raw sockets,netsh interface portproxy, DNS tunnels, and out-of-band callback domains.careful_company_running_windows.guardrails- Turning off the safety net: Defender (Set-MpPreference -Disable*/-ExclusionPath), the firewall, EDR and event-log services, BitLocker,Set-ExecutionPolicy Bypass, script-block logging, event-log clearing, dcg's ownDCG_BYPASS,dcg uninstall, allowlist grants (dcg allowlist add,dcg allow-once), runtime config overrides (DCG_DISABLE/DCG_PACKS/DCG_CONFIG), and the agent's hook config, plus unreviewed remote code (iwr | iex,powershell -EncodedCommand, mshta/regsvr32 remote payloads). Diagnosis stays open:dcg explain,dcg allowlist list, anddcg allowlist validateare whitelisted.
False positives are the design constraint. Rules require positive evidence of
egress — an attached file, a known egress host, a mutating method — so ordinary
GETs, -OutFile/curl -o downloads, and every package-manager install pass
through untouched (fetching from a known file-drop or paste host is the one
exception, and it warns rather than blocks). Requests whose destinations are all internal (loopback,
RFC1918, *.internal/*.corp/*.local, bare intranet hostnames) are
whitelisted, with the cloud metadata endpoints (169.254.169.254,
metadata.google.internal) deliberately excluded from that allowance. Searching
for a token (Select-String "Send-MailMessage" *.ps1) and dcg explain "" are never blocked. git push to a named remote is untouched, and
SMB copies to a corporate share are out of scope.
Genuinely ambiguous cases warn instead of blocking (Medium severity: the
command runs and the decision is recorded) — a POST with an inline body is a
GraphQL query as often as an exfiltration. Promote them when your posture calls
for it:
[policy.rules]
"careful_company_running_windows.upload:cli-http-mutating-request" = "deny"
"careful_company_running_windows.upload:ps-http-mutating-request" = "deny"
This preset carries one built-in trust boundary you should know about. While any
careful_company_running_windows.*pack is enabled, a command whose executable ishfdt(optionally path-qualified) is allowed without evaluating any pack at all — not just this preset's.hfdt rm -rf /datais permitted with the preset on and denied with it off. The exemption is structural rather than textual: it requireshfdtto be the actual executable of the whole command and refuses chains, redirection, and process substitution, sohfdt …; Invoke-RestMethod …andhfdt $(…)are evaluated normally. If you do not run that tool, this never fires; if you do, treat it as an explicit decision to trust it completely. Seedocs/careful-company-windows.md.
Other first-party internal tooling gets no such exemption and should be allowlisted, which keeps the grant narrow and recorded:
dcg allowlist add-command "mytool publish --to https://artifacts.corp.internal" \
-r "First-party internal publisher" --user
Other Packs
package_managers- Protects against dangerous package manager operations like publishing packages and removing critical system packages.strict_git- Stricter git protections: blocks all force pushes, rebases, and history rewriting operations.
Enable packs in ~/.config/dcg/config.toml:
[packs]
enabled = [
# Databases
"database.postgresql",
"database.redis",
"database.supabase",
# Containers and orchestration
"containers.docker",
"kubernetes", # Enables all kubernetes sub-packs
# Cloud providers
"cloud.aws",
"cloud.gcp",
# Secrets management
"secrets.aws_secrets",
"secrets.vault",
# CI/CD
"cicd.jenkins",
"cicd.gitlab_ci",
# Messaging
"messaging.kafka",
"messaging.sqs_sns",
# Search engines
"search.elasticsearch",
# Backup
"backup.restic",
# Platform
"platform.github",
"platform.railway",
# Monitoring
"monitoring.splunk",
]
Custom Packs
Create your own organization-specific security packs using YAML files. Custom packs let you define patterns for internal tools, deployment scripts, and proprietary systems without modifying dcg.
[packs]
custom_paths = [
"~/.config/dcg/packs/*.yaml", # User packs
".dcg/packs/*.yaml", # Project-local packs
]
For detailed pack authoring guide, schema reference, and examples, see docs/custom-packs.md.
Validate your pack before deployment:
dcg pack validate mypack.yaml
Heredoc scanning configuration:
[heredoc]
# Enable scanning for heredocs and inline scripts (python -c, bash -c, etc.).
enabled = true
# Extraction timeout budget (milliseconds).
timeout_ms = 50
# Resource limits for extracted bodies.
max_body_bytes = 1048576
max_body_lines = 10000
max_heredocs = 10
# Optional language filter (scan only these languages). Omit for "all".
# languages = ["python", "bash", "javascript", "typescript", "ruby", "perl", "go"]
# Bounded heredoc fallback (strict mode can block instead).
fallback_on_parse_error = true
fallback_on_timeout = true
CLI overrides for heredoc scanning:
--heredoc-scan/--no-heredoc-scan--heredoc-timeout--heredoc-languages
Heredoc documentation:
docs/adr-001-heredoc-scanning.md(architecture and rationale)docs/patterns.md(pattern authoring + inventory)docs/security.md(threat model and incident response)
Heredoc Three-Tier Architecture
Heredoc and inline script scanning uses a three-tier pipeline designed for performance and accuracy:
Command Input
│
▼
┌─────────────────┐
│ Tier 1: Trigger │ ─── No match ──► ALLOW (fast path, <100μs)
│ (RegexSet) │
└────────┬────────┘
│ Match
▼
┌─────────────────┐
│ Tier 2: Extract │ ─── Error/Timeout ──► FALLBACK SCAN or BLOCK (strict)
│ (<1ms) │
└────────┬────────┘
│ Success
▼
┌─────────────────┐
│ Tier 3: AST │ ─── No match ──► ALLOW
│ (<5ms) │ ─── Match ──► BLOCK
└─────────────────┘
Tier 1: Trigger Detection (<100μs)
Ultra-fast regex screening to detect heredoc indicators. Uses a compiled RegexSet for O(n) matching against all trigger patterns simultaneously:
static HEREDOC_TRIGGERS: LazyLock = LazyLock::new(|| {
RegexSet::new([
r"<<-?\s*(?:['\x22][^'\x22]*['\x22]|[\w.-]+)", // Heredocs
r"<<<", // Here-strings
r"\bpython[0-9.]*\b.*\s+-[A-Za-z]*[ce]", // python -c/-e
r"\bruby[0-9.]*\b.*\s+-[A-Za-z]*e", // ruby -e
r"\bnode(js)?[0-9.]*\b.*\s+-[A-Za-z]*[ep]", // node -e/-p
r"\b(sh|bash|zsh)\b.*\s+-[A-Za-z]*c", // bash -c
// ... more patterns
])
});
Commands without any trigger patterns skip directly to ALLOW—no further processing needed.
Tier 2: Content Extraction (<1ms)
For commands that trigger, extract the actual content to be evaluated:
- Heredocs:
cat <: hook-mode diagnostics; sends the evaluator's tracing events to stderr (DCG_LOG=debug, or atracingfilter such asdestructive_command_guard::heredoc=trace). Unset by default. DCG_QUIET=1: suppress non-error outputDCG_COLOR=auto|always|never: color modeDCG_NO_RICH=1: disable rich terminal formatting and use plain renderingDCG_NO_COLOR=1: disable colored output (same as NO_COLOR)DCG_LEGACY_OUTPUT=1: force plain output paths (same as--legacy-output)DCG_ROBOT=1: enable robot mode for JSON stdout and quiet stderrDCG_HIGH_CONTRAST=1: enable high-contrast output (ASCII borders + monochrome palette)DCG_FORMAT=text|json|sarif: default output format (command-specific — see Output Formats for which values each subcommand actually accepts; real SARIF isdcg scan-only)DCG_FAIL_CLOSED=1: block (deny) on hook input that cannot be parsed, instead of the default fail-open allow (opt-in; see Bounded Failure Policy)DCG_UNVERIFIED_DECISION=deny|ask: decision for commands dcg could not verify (evaluation timeout, or overmax_command_bytes);denysuits unattended sessions where nobody can answerask(see Bounded Failure Policy)DCG_BRIDGE_CRASH_DECISION=allow: let a command through when the OpenCode plugin or the Oh My Pi bridge started dcg but got no verdict from it (dcg crashed or was killed); the default blocks (see Bounded Failure Policy)DCG_BYPASS=1: bypass dcg entirely (escape hatch; use sparingly)DCG_CONFIG=/path/to/config.toml: use explicit config fileDCG_HEREDOC_ENABLED=true|false: enable/disable heredoc scanningDCG_HEREDOC_TIMEOUT=50: heredoc extraction timeout (milliseconds)DCG_HEREDOC_TIMEOUT_MS=50: heredoc extraction timeout (milliseconds)DCG_HEREDOC_LANGUAGES=python,bash: filter heredoc languagesDCG_AST_TIMEOUT_MS=: AST-matching budget for embedded code (default 20). It can only raise the compiled-in budget, never lower it: a smaller window pushes the matcher into its bounded fallback, which denies but without naming a rule, so shrinking it from the environment would degrade analysis rather than tighten it. Lower bounds belong toDCG_HOOK_TIMEOUT_MSandDCG_HEREDOC_TIMEOUT_MS, which are measured against real workDCG_POLICY_DEFAULT_MODE=deny|ask|warn|log: global default decision mode (askrequires native operator review and fails closed on unsupported clients)DCG_HOOK_TIMEOUT_MS=: explicit hook evaluation timeout (ordinary default: 1000; automaticcareful_company_running_windowspreset default: 3000)DCG_UPDATE_PIN=1: pin this install againstdcg update(#320) — the updater refuses before any network/installer work unless--replace-local-buildis passed, and the "update available" nudge is suppressed. Same asgeneral.update_pin = truein config.DCG_HISTORY_DB=/path/to/history.db: history database file (overrides[history] database_path;~is expanded). See Command History.DCG_HISTORY_DISABLED=1: never open the history database, even when[history] enabled = true.
Command History
Command history is opt-in ([history] enabled = true). When enabled, the
hook records every evaluated command (redacted per redaction_mode; the
default "pattern" replaces recognised credential shapes with placeholders and
truncates long quoted arguments) in a
SQLite database that dcg history, dcg stats, and dcg suggest-allowlist
read.
Where the database lives, highest priority first:
DCG_HISTORY_DBenvironment variable[history] database_pathin config (~expanded; relative paths resolve against the working directory)- An existing
history.dbbesideconfig.toml(~/.config/dcg/history.db) from a release before 0.15 — it keeps being used until you move it - The platform state directory:
$XDG_STATE_HOME/dcg/history.db, defaulting to~/.local/state/dcg/history.dbon Linux/macOS, and%LOCALAPPDATA%\dcg\history.dbon Windows
History is state, not configuration, so it no longer defaults into
~/.config/dcg; a sandbox that mounts the config directory read-only keeps
working. Directories dcg creates for the database are owner-only (0700).
dcg doctor prints the resolved path, which rule selected it, and whether the
hook can write there.
What a row can and cannot tell you:
hostnameis the machine that recorded the row, so databases copied off several machines can be merged and still attributed.exit_codeis always NULL on rows the hook writes. dcg runs before the command, so it never learns how the command ended. Read NULL as "unknown", not as "succeeded". History records dcg's decisions (allow, deny, warn, bypass); it cannot by itself show that an allowed command did damage.dcg history analyzeworks from those decisions. With no recorded commands it says so and makes no recommendations. A pack that never matched is listed but never recommended for removal: a guard pack that stays quiet is working.
Output Formats and DCG_FORMAT
--format (and the DCG_FORMAT env var, which seeds the default) is
command-specific: each subcommand accepts only its own set of values, and an
unrecognized value is a usage error (exit 2). DCG_FORMAT applies wherever a
command has a --format flag and is silently ignored by commands that don't.
| Command | Accepted --format values |
Notes |
|---|---|---|
dcg scan |
pretty, json, markdown, sarif |
Only command that emits real SARIF 2.1.0 |
dcg test |
pretty (alias text), json (aliases sarif, structured), toon |
|
dcg config |
pretty (alias text), json (alias sarif) |
|
dcg packs |
pretty (alias text), json (alias sarif) |
|
dcg explain |
pretty, json (alias sarif) |
|
dcg doctor |
pretty, json (alias sarif) |
|
dcg simulate |
pretty, json (alias sarif) |
|
dcg corpus |
json, pretty (alias sarif) |
|
dcg suggest-allowlist |
text, json (alias sarif) |
sarif is a JSON alias on every command except dcg scan. This is
deliberate so that setting DCG_FORMAT=sarif globally degrades gracefully —
dcg scan produces a real SARIF report while other commands fall back to their
structured JSON rather than erroring. If you need machine-readable output from a
non-scan command, prefer --format json (which is unambiguous); use dcg scan --format sarif for SARIF. --robot forces JSON regardless of --format.
Configuration Hierarchy
dcg supports layered configuration from multiple trusted sources, with higher-priority sources overriding lower ones:
- Environment Variables (DCG_* prefix) [HIGHEST PRIORITY]
- Explicit Config File (DCG_CONFIG env var)
- User Config (~/.config/dcg/config.toml)
- System Config (/etc/dcg/config.toml)
- Compiled Defaults [LOWEST PRIORITY]
An automatically discovered .dcg.toml is intentionally not a normal
precedence layer. A repository is untrusted when it is first cloned, so its
config may only add enforcement: enable built-in packs, add deny policy
entries, opt into general.fail_closed, enable
heredoc scanning, or turn off heredoc bounded fallbacks. Settings that grant
trust or reduce coverage — including allow overrides, pack disables, custom
pack paths, custom regex overrides (including block regexes), resource limits,
language filters, agent profiles, nested project overrides, and per-rule
target-path exemptions — are ignored during
automatic discovery.
Automatic project discovery reads only a direct regular file bound to the
handle it actually reads: O_NOFOLLOW plus descriptor identity on Unix
(including macOS), and a reparse-point-refusing open plus handle/path identity
on native Windows. A symlinked .dcg.toml is refused on every platform.
To deliberately trust the complete repository config for one invocation, select
it explicitly: DCG_CONFIG=.dcg.toml dcg .... An explicit file has the same
full authority as any other user-selected config.
Accessibility & Themes
dcg supports colorblind-safe palettes and high-contrast output. Colors are always paired with symbols/labels to avoid conveying meaning by color alone.
[output]
high_contrast = true # ASCII borders + black/white palette
[theme]
palette = "colorblind" # default | colorblind | high-contrast
use_unicode = true # false for ASCII-only
use_color = true # false for monochrome
Configuration File Locations:
| Level | Path | Use Case |
|---|---|---|
| System | /etc/dcg/config.toml |
Organization-wide defaults |
| User | ~/.config/dcg/config.toml |
Personal preferences |
| Project | .dcg.toml (repo root) |
Automatically discovered enforcement-only policy |
| Explicit | DCG_CONFIG=/path/to/file |
Testing or override |
The machine-wide system-config layer is accepted on Unix only after the file
and every directory in its direct path are root-owned and not group/world
writable. Native Windows currently ignores that implicit layer until native
ACL and reparse-point validation is implemented; use a user config or an
explicit DCG_CONFIG file there.
Merging Behavior:
Configuration layers are merged additively, with higher-priority sources overriding specific fields:
// Only fields explicitly set in higher-priority configs override
// Missing fields retain values from lower-priority sources
fn merge_layer(&mut self, other: ConfigLayer) {
if let Some(verbose) = other.general.verbose {
self.general.verbose = verbose; // Override if present
}
// Unset fields retain previous values
}
This means you can set organization defaults in /etc/dcg/config.toml, personal
preferences in ~/.config/dcg/config.toml, and repository-owned hardening in
.dcg.toml without letting a newly cloned repository weaken the user's guard.
Use DCG_CONFIG=.dcg.toml only after reviewing a project file that needs full
override authority.
Project-Specific Pack Configuration:
The [projects] section allows different pack configurations for different repositories:
[projects."/home/user/work/production-api"]
packs = { enabled = ["database.postgresql", "cloud.aws"], disabled = [] }
[projects."/home/user/personal/experiments"]
packs = { enabled = [], disabled = ["core.git"] } # More permissive for experiments
Bounded Failure Policy
dcg distinguishes an unreadable hook envelope from a command whose safety evaluation began but could not finish. It never treats elapsed analysis time or an oversized extracted command as proof that execution is safe.
| Scenario | Default behavior | Strict/configured behavior |
|---|---|---|
| Malformed or oversized raw hook JSON | Allow with an audit warning | general.fail_closed = true denies |
| Transient hook stdin I/O error | Allow with an audit warning | Always fail-open because the payload was not attacker-controlled |
Extracted command exceeds max_command_bytes |
Explicit indeterminate result | Review-capable clients receive ask (unverified_decision = "deny" turns this into a deny); other clients block |
| Absolute evaluation deadline expires | Explicit indeterminate result | Review-capable clients receive ask (unverified_decision = "deny" turns this into a deny); other clients block |
| Heredoc extraction/parse/AST failure | Run the bounded fallback scanner | fallback_on_parse_error = false or fallback_on_timeout = false blocks |
| OpenCode plugin or Oh My Pi bridge: dcg cannot be started (missing or not executable) | Allow with a visible diagnostic, so a broken install does not block every command | OMP: DCG_UNVERIFIED_DECISION=deny in its environment blocks; the config-file setting cannot apply because dcg never read it |
| OpenCode plugin or Oh My Pi bridge: dcg started but gave no verdict (killed by a signal or the bridge's timeout, an unexpected exit status; for OpenCode also exit 0 without its explicit allow line) | Block, with the reason and a visible diagnostic; a deny dcg wrote before dying still stands | DCG_BRIDGE_CRASH_DECISION=allow in the agent's environment lets such commands through; DCG_UNVERIFIED_DECISION=deny still blocks |
| A panic in the hook after its configuration is loaded and before a shell command is known to be allowed | The unverified-command response for the request's protocol (ask, or deny under unverified_decision = "deny") |
Always blocking |
Configurable Strictness:
Raw hook-envelope fail-open behavior and embedded-code fallback behavior are configured independently.
For heredoc/inline-script analysis specifically:
[heredoc]
fallback_on_parse_error = false # Block on heredoc parse errors
fallback_on_timeout = false # Block on heredoc timeouts
For the top-level hook input (the JSON dcg reads from stdin), enable fail-closed mode so that input which cannot be parsed at all is blocked instead of allowed:
[general]
fail_closed = true # Deny when the hook input itself is unparseable
or at runtime:
DCG_FAIL_CLOSED=1 # env var overrides the config value
For the two unverified outcomes (evaluation deadline expired, or command
over max_command_bytes), the default ask presumes a human is present to
answer. On unattended or autonomous sessions there is no such human, and
anything auto-answering prompts would approve exactly the commands dcg
declined to inspect. Opt those sessions into denial instead:
[general]
unverified_decision = "deny" # refuse what could not be inspected
or at runtime with DCG_UNVERIFIED_DECISION=deny. The denial reason is
actionable (shrink or split the command; raise hook_timeout_ms /
max_command_bytes after review), and ordinary verified commands are
unaffected. A repository .dcg.toml may set unverified_decision = "deny"
(tightening) but never relax an operator's deny back to ask.
A payload that itself declares "permission_mode": "bypassPermissions" or
"dontAsk" gets the deny posture automatically: Claude Code documents that a
hook deny holds in those modes, but not what a hook ask does there. Only an
explicit DCG_UNVERIFIED_DECISION=ask overrides this.
The default is fail-open (unparseable input is allowed) and is unchanged
unless you opt in. With fail-closed enabled, a genuinely unparseable hook
payload produces a deny (a permissionDecision: deny for Claude-style hooks; a
"decision":"deny" line plus a non-zero exit for dcg hook --batch).
Transient IO read errors still fail open even in this mode, since they are not
attacker-controlled malformed payloads.
Even under the fail-open default, an unparseable, oversized or non-UTF-8
payload is not allowed blind: dcg scans the raw text for a shell tool's
"command" value and evaluates it. A command that would be denied (or asked
about) gets that answer; only a payload with no evaluable shell command, or one
whose command is allowed, falls through to the fail-open allow. Unpaired UTF-16
surrogate escapes (\ud800), which JavaScript hosts can emit, are replaced
with U+FFFD before parsing rather than failing it.
A leading UTF-8 BOM (
EF BB BF) is stripped before parsing in all hook paths, so a BOM-prefixed but otherwise-valid command is correctly evaluated (and blocked if dangerous) rather than allowed through as "unparseable".
With strict mode enabled, dcg blocks malformed attacker-controlled hook input and reports why. Separately, when heredoc parsing cannot complete and fallback is enabled, dcg runs a lightweight bounded check over the original command:
static FALLBACK_PATTERNS: LazyLock = LazyLock::new(|| {
RegexSet::new([
r"shutil\.rmtree",
r"os\.remove",
r"fs\.rmSync",
r"\brm\s+-[a-zA-Z]*r[a-zA-Z]*f",
r"\bgit\s+reset\s+--hard\b",
// ... other critical patterns
])
});
This fallback is specific to embedded-code extraction. It is not used for a raw hook envelope that could not be parsed, and it does not turn a deadline or an oversized extracted command into an allow.
Absolute Evaluation Deadline:
To prevent any single command from blocking indefinitely, dcg enforces an
end-to-end evaluation deadline. The ordinary default is 1000ms; the
careful_company_running_windows preset defaults to 3000ms, and an
explicit general.hook_timeout_ms or DCG_HOOK_TIMEOUT_MS overrides either
default (values below 10ms are clamped to that safety minimum). Exhausting
that budget produces an explicit indeterminate result, which requests operator
review where the hook protocol supports it and otherwise blocks.
The deadline intentionally uses monotonic wall-clock time. A CPU-time budget
would stop advancing while dcg was descheduled or waiting on a bounded
operation, so it could not guarantee hook latency. On a heavily loaded host,
increase hook_timeout_ms and use dcg test --enforce-budget to exercise the
same evaluator-side budget outside a live hook.
Installation
Quick Install (Recommended)
The easiest way to install is using the install script, which downloads a prebuilt binary for your platform:
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/install.sh?$(date +%s)" | bash -s -- --easy-mode
Easy mode auto-detects your platform, downloads the right binary, verifies SHA256 checksums, configures all supported AI agent hooks and bridges (Claude Code, Codex CLI, Gemini CLI, GitHub Copilot CLI, Cursor IDE, Hermes Agent, Posit Assistant, Oh My Pi, OpenCode, Crush, Reasonix, Aider), and updates your PATH. For Codex CLI 0.125.0+, the installer merges a PreToolUse Bash hook into ~/.codex/hooks.json; invalid JSON or malformed existing Codex hook shapes are left unchanged and reported instead of being overwritten.
Homebrew
The upstream tap supports Apple Silicon and Intel macOS plus ARM64 and x86_64 Linux:
brew install dicklesworthstone/tap/dcg
dcg install
Homebrew installs only the dcg binary. The explicit dcg install step
configures hooks for the coding agents detected on your machine; the formula
does not mutate hook or configuration files during package installation.
If your Homebrew installation enforces tap trust, trust this formula before installing it:
brew trust --formula dicklesworthstone/tap/dcg
brew install dicklesworthstone/tap/dcg
dcg install
Manual install (no curl | bash)
Every release archive is signed, so you can verify it yourself and never run the installer script:
V=v0.15.0; T=aarch64-apple-darwin # or x86_64-unknown-linux-musl, etc.
curl -fLO "https://github.com/Dicklesworthstone/destructive_command_guard/releases/download/$V/dcg-$T.tar.xz"
curl -fLO "https://github.com/Dicklesworthstone/destructive_command_guard/releases/download/$V/dcg-$T.tar.xz.minisig"
minisign -Vm "dcg-$T.tar.xz" -P RWSoYi6NXJWzaRs1mJmOwwXrZfPWcq6MXnQlNMLBYKzlIQTLwuVQG6uO
tar -xJf "dcg-$T.tar.xz" && install -m 0755 dcg ~/.local/bin/dcg
dcg install # configure agent hooks
Each archive also has a .sha256 and a .sigstore.json bundle. Verify the
bundle with cosign verify-blob --new-bundle-format --key --bundle dcg-$T.tar.xz.sigstore.json dcg-$T.tar.xz, where `` is the key pinned
as COSIGN_RELEASE_PUBLIC_KEY in install.sh.
Other options:
Interactive mode (prompts for each step; prompts read your terminal via
/dev/tty, so they work even when the script is piped through bash. With no
terminal at all — e.g. CI — the installer proceeds with safe defaults and
prints each decision it makes):
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/install.sh?$(date +%s)" | bash
Install specific version:
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/install.sh?$(date +%s)" | bash -s -- --version v0.7.6
Install to /usr/local/bin (system-wide, requires sudo):
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/install.sh?$(date +%s)" | sudo bash -s -- --system
Build from source instead of downloading binary:
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/install.sh?$(date +%s)" | bash -s -- --from-source
Download/install only (skip agent hook configuration):
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/install.sh?$(date +%s)" | bash -s -- --no-configure
Note: If you have gum installed, the installer will use it for fancy terminal formatting.
The installer verifies each adjacent .minisig with the embedded release public
key when minisign is available. A present but invalid signature is always fatal;
--require-minisign also makes a missing sidecar or verifier fatal. The pinned key
ID for current releases is 69B3955C8D2E62A8; the retired
36B847D11BA5A0D0 key is accepted only when installing v0.6.7. Trusted Sigstore
cosign bundles are checked independently against either the pinned local-release
public key or the repository's GitHub Actions OIDC identity, and the SHA256
checksum remains mandatory. Cosign versions affected by CVE-2026-22703 are not
trusted. The installer falls back to building from source if no prebuilt is
available and removes the legacy Python predecessor (git_safety_guard.py) if
present.
Agent-specific notes
- Aider: No PreToolUse-style interception. The installer enables
git-commit-verify: truein~/.aider.conf.ymlso git hooks run. For full protection, install dcg as a git pre-commit hook. - Continue: No shell command interception hooks. The installer detects Continue but cannot auto-configure protection. Use a git pre-commit hook instead.
- Codex CLI: PreToolUse hooks via
~/.codex/hooks.json(stable in Codex 0.125.0+; thecodex_hooksfeature is on by default). dcg detects Codex from theturn_idstdin field and emits the minimal documentedhookSpecificOutputdeny JSON with exit code 0; dcg-only metadata is omitted so Codex's strict parser accepts the decision. The Unix installer andinstall.ps1both merge dcg's hook into the existing hooks object, detect an already-current dcg hook exactly, leave invalid JSON or malformed hook shapes untouched, and surface the failure reason in the install summary. After installation, open Codex's/hooksUI once to trust the hook.uninstall.shanduninstall.ps1remove only dcg-owned Codex hooks and preserve coexisting entries. See the Codex integration notes. Caveats: the model can still write scripts to disk to bypass hook-based blocking; and Codex'sPreToolUsehooks do not yet intercept everyunified_execshell path, so treat it as a guardrail rather than a complete enforcement boundary. - GitHub Copilot CLI: The installer writes a user-level hook to
${COPILOT_HOME:-~/.copilot}/hooks/dcg.json, protecting every workspace. The generatedpreToolUsehook covers both Unixbashand Windowspowershellpayloads and emits Copilot's exact top-level permission-decision JSON. - VS Code Copilot Chat: Current VS Code releases load
~/.claude/settings.jsonby default, so the Claude Code hook installed by dcg also protects Copilot Chat without a second bridge or duplicate hook. dcg recognizes VS Code's documentedrunTerminalCommandshell tool plus the observed compatibility namesrun_in_terminalandrunInTerminal, readstool_input.command, and returns VS Code's documentedhookSpecificOutputdeny. The newer Copilot Agent Host (and the Agents window built on it) sends a batched envelope instead —{"toolCalls": [{"name": "powershell", "args": "{\"command\": …}"}]}with JSON-encoded argument strings; dcg evaluates every shell entry in the batch independently and a single destructive entry denies the request (#252). Agent hooks are still a VS Code preview feature and can be disabled by organization policy; use Developer: Show Agent Debug Logs or the GitHub Copilot Chat Hooks output channel to confirm that the hook loaded. - Cursor IDE: Hooks are configured through
~/.cursor/hooks.jsonplus a generated bridge (dcg-pre-shell.ps1on Windows). The installer inserts dcg first inbeforeShellExecution, collapses duplicate dcg entries, and preserves coexisting Cursor hooks. The bridge blocks a command it cannot verify: a payload it cannot read, or a dcg that ran but gave no verdict (killed, timed out, non-zero exit, no answer). SetDCG_BRIDGE_CRASH_DECISION=allowto let those through instead; only a dcg that cannot be started at all is allowed, with a notice on stderr. Cursor also runs thePreToolUsehooks in~/.claude/settings.jsonfor itsShelltool, and dcg judges those payloads too. - Hermes Agent: NousResearch's Hermes Agent declares shell hooks in its
config.yamlunderhooks.pre_tool_call. Hermes resolves its data root fromHERMES_HOMEwhen set, else%LOCALAPPDATA%\hermeson native Windows and~/.hermeson Linux/macOS — both installers write the hook to that resolved path (install.ps1never writes to%USERPROFILE%\.hermesunlessHERMES_HOMEpoints there, since native Windows Hermes would never read it). The installer merges a singlematcher: "terminal"entry that invokes dcg directly — no wrapper script — because Hermes' input JSON (hook_event_name: "pre_tool_call",tool_name: "terminal",tool_input.command) deserializes straight into dcg's existingHookInput. Hermes explicitly documents that "non-zero exit codes... never abort the agent loop", so dcg switches to Hermes' JSON block protocol on output:{"decision":"block","reason":...}(plus the alternate{"action":"block","message":...}form for cross-version compatibility). The installer also setshooks_auto_accept: trueif not already set; Hermes silently drops un-allowlisted hooks in non-TTY runs (gateway/cron) without it.unconfigure_hermesinuninstall.shremoves only the dcg-owned entry and leaveshooks_auto_acceptalone (other Hermes hooks may rely on it). - Grok (xAI): Grok Build / Grok CLI auto-discovers every
*.jsonunder~/.grok/hooks/.dcg install --grokwrites a self-contained~/.grok/hooks/dcg.jsonwith aPreToolUse/matcher: "Bash"entry — Grok internally aliases Claude-style"Bash"to its ownrun_terminal_cmdtool, so a single rule covers every shell command. dcg detects Grok at runtime from the camelCase wire shape (hookEventName: "pre_tool_use",toolName: "run_terminal_cmd") or from theGROK_SESSION_ID/GROK_HOOK_EVENT/GROK_WORKSPACE_ROOTenvironment variables, and switches its output to Grok's JSON contract:{"decision":"deny","reason":...}(note"deny", not Hermes'"block"). Grok also picks up dcg automatically through its~/.claude/settings.jsoncompatibility layer, so existing Claude Code users get protection with no additional install step. Add--projectto write/.grok/hooks/dcg.jsonfor a per-repo install (Grok requires/hooks-trustthe first time it opens a repo with hooks). - Antigravity CLI (
agy): Google Antigravity'sagyCLI ships a Claude-Code-compatible hooks system.dcg install --agymerges aPreToolUse/matcher: "Bash"entry into~/.gemini/config/hooks.json(the canonical path;agymigrates the legacy~/.gemini/antigravity-cli/hooks.jsonhere and symlinks the old path to it).agyruns the hook before itsrun_commandshell tool; dcg detectsagyat runtime from the distinctive nestedtoolCallenvelope ({"toolCall":{"name":"run_command","args":{"CommandLine":"…"}},"conversationId":…,"stepIdx":…}) — the shell command is read fromtoolCall.args.CommandLine— or from theANTIGRAVITY_CONVERSATION_IDenvironment variable /agyparent-process name. dcg switches its output toagy's JSON contract:{"decision":"block","reason":…}with exit code 0 (verified:agyhonors both"block"and"deny"and aborts the tool; a non-zero exit code is only logged and does NOT reliably block, so dcg always emits exit 0 + JSON). Add--projectto write/.gemini/config/hooks.jsonfor a per-repo install. Restartagy(start a new session) after installing. - Posit Assistant: Posit Assistant reads Claude-Code-compatible lifecycle hooks from
~/.posit/assistant/settings.json(global) and/.posit/assistant/settings.json(project). The installer merges onePreToolUseentry into the global file, so a single install covers the Positron/RStudio extension, the standalone server, and thepaterminal client across every workspace. No protocol work was needed on dcg's side: thePreToolUsestdin is the snake_case Claude shape (tool_name,tool_input.command,tool_use_id,permission_mode), exit code 2 blocks with stderr shown as the reason, andhookSpecificOutput.permissionDecision(allow/deny/ask) is read on exit 0 — dcg's existing Claude-compatible response answers all of it. Three details differ from the Claude Code entry: the matcher is lowercase"bash|powershell"(a simple matcher string is an exact match — or a|/,-separated list of exact matches — against the tool name, so a copied Claude"Bash"matcher would never fire; listing both names covers a Windows PowerShell host with one entry); only documented handler fields are written (type,command,timeout), so there is noshellfield — the command path is quoted instead, since shell-form hooks run throughcmd.exeon Windows; andtimeoutis in seconds. dcg identifies the agent at runtime fromPA_PROJECT_DIR, which the hook contract sets in the hook subprocess (also used to keep apowershelltool name from being answered with Codex's minimal deny shape). Existing matcher groups are left structurally intact rather than consolidated — hook config is additive, so a user'smatcher: "bash,edit"group keeps working untouched — andunconfigure_posit_assistantinuninstall.shremoves only dcg-owned entries and never deletes the settings file, since unrelated settings live there too. Note: Posit's hooks documentation is not public yet; this contract was verified empirically and is pinned by tests insrc/hook.rs. - OpenCode: First-party plugin support (#318).
dcg install --opencodewrites a nativetool.execute.beforeplugin to~/.config/opencode/plugins/dcg-guard.js(add--projectfor/.opencode/plugins/dcg-guard.js). The plugin routes every OpenCodebashtool call through dcg's Claude-compatible hook protocol — spawning the absolute dcg binary path embedded at install time withOPENCODE=1in the environment — and aborts the tool call by throwing when dcg denies (anaskverdict also fails closed, since OpenCode has no operator-review state). The plugin asks dcg for an explicit allow line, so a dcg that ran but gave no verdict (killed, a non-zero exit, or exit 0 with nothing on stdout) blocks the command unlessDCG_BRIDGE_CRASH_DECISION=allow; only a dcg that cannot be started at all (missing) fails open, with a stderr notice. The file carries adcg-opencode-pluginownership marker: the installer refuses to overwrite a user-owned file of the same name, and the uninstaller deletes only marker-carrying files.install.shconfigures it automatically when OpenCode is detected;dcg doctorreports anopencode_plugincheck (error +--fixable when OpenCode is in use but unguarded, since there is no Claude-compat fallback). Restart OpenCode after installing. See docs/opencode-integration.md. An earlier community plugin by aspiers pioneered this approach. - Oh My Pi (
omp): First-class native extension support.dcg install --ompwrites a marker-owned ExtensionAPI module to the active OMP user profile (normally~/.omp/agent/extensions/dcg-guard.ts); add--projectfor/.omp/extensions/dcg-guard.ts. OMP's native project-extension discovery is cwd-only: it does not require Git and does not walk ancestors, so run the project install from the same directory where you launch OMP. The extension interceptsbashthrough OMP's pre-executiontool_callevent and sends the raw command to the embedded absolute dcg pathname asdcg --robot test --stdin --agent ompwith the dialect that matches OMP's selected backend. The private bridge pins--format json, so ambientDCG_FORMATcannot redirect or invalidate its compact protocol while remaining available to supported environment-conditioned policy. The install-time pathname is authoritative against ambientDCG_BINredirection, but it does not attest a hash, inode/file ID, signature, or immutable executable object: Bun resolves the pathname for each guarded call, and replacing bytes at that pathname changes what a later callback executes.dcg doctorcompares the marker-owned extension with source generated for the doctor process's pathname at inspection time; it does not attest executable bytes or an extension already loaded by a running OMP session. Rebind deliberately with/desired/path/dcg install --omp --force(add--projectfor project scope), then restart OMP; protect the binary, extension, and their parent directories from writers not trusted to control OMP execution. Ordinary and managed-async calls use OMP's embedded Brush shell and therefore pass--dialect posix, including on native Windows; an eligible localpty: truecall instead maps OMP's configured external shell toposix,cmd, orps.PI_NO_PTY=1keeps the embedded POSIX route. The bridge returns{ block: true, reason }for dcg deny/ask/indeterminate results. No shell is used to spawn dcg. A dcg that cannot be started is reported and fails open; a dcg that started and then died without a blocking verdict (a signal, including the bridge's own timeout kill, or an exit status dcg never uses for a verdict) is reported and blocks unlessDCG_BRIDGE_CRASH_DECISION=allow; dcg evaluation failures and local-PTY shell-resolution failures remain blocking. Bun enforces a 30-second parent-sideSIGKILLbackstop on the direct dcg child, and the bridge immediately arms an independent 30.5-second observation watchdog after successful spawn. Direct-child exit or exit-observation rejection switches to a 250-millisecond pipe-drain grace because a descendant can inherit stdout/stderr after the direct child is gone; expiry cancels the local readers, while an exit-observation fault or hard deadline also attempts one direct-childSIGKILL. All watchdogs are cleared after observation, late exit settlement/rejection remains consumed, and there is no retry or replacement process. A complete deny/ask/indeterminate frame or stdout overflow retained before cancellation remains absorbing; status, stream, kill, and deadline faults remain visible. These generous ceilings are separate from dcg's ordinary configurable 1-second/3-second evaluation budgets, but deliberately cap an explicit evaluator budget longer than 30 seconds on the OMP bridge. After observation the bridge reads Bun'ssignalCode, so an ordinary numeric exit 137 remains distinguishable fromSIGKILLand signal diagnostics name the exact signal. dcg's blocking exit 1 cannot be erased by a signal/status observation fault, and other abnormal exit statuses remain visible even when a deny-like verdict is authoritative. Residual process limit: an in-process timer cannot preempt a synchronousBun.spawnor JavaScript event-loop stall, and Bun's kill targets the direct child rather than proving process-group/descendant termination; local reader cancellation bounds a standards-compliant callback but does not claim surviving descendants were reaped. The canonical agent/profile key isomp(aliasoh-my-pi), deliberately distinct from legacy Pi. With[history] enabled = true, robot-boundary decisions are persisted withagent_type = "omp"; ordinary humandcg testdiagnostics remain outside command history. Named profiles followOMP_PROFILEoverPI_PROFILE;PI_CONFIG_DIRselects a config directory name relative to the user's home (drive-qualified values are rejected on Windows), andPI_CODING_AGENT_DIRremains supported for the default profile. Both platform installers auto-configure detected OMP installations,dcg doctorreports theomp_extensioncheck, and uninstallers remove only files carrying thedcg-omp-extensionmarker. Restartompafter installing. Known ACP limitation: OMP routes a foreground non-PTY call through the configured external shell when an ACP client advertises terminal support, but its public ExtensionAPI exposes neither that terminal capability nor the selected backend (and both ACP and JSON-RPC reportmode: "rpc"). The bridge therefore keeps non-PTY RPC analysis POSIX instead of guessing and importing Cmd/PowerShell false positives; ACP-terminal calls do not yet have exact Cmd/PowerShell-specific coverage until OMP exposes that routing state.- OMP deadline and signal detail: The 30.5-second observation limit is one monotonic absolute deadline, not a fresh allowance after exit. A post-exit or rejected-exit drain is
min(250 ms, remaining absolute budget); a hard-budget-clamped drain retains hard-deadline provenance and never kills again. A successful direct-childkill("SIGKILL")request is also distinct from an observed signal: diagnostics name SIGKILL only when Bun's latersignalCoderead actually exposes it. Standards-compliant Web Stream cancellation closes pending reads even if its underlying cancel algorithm rejects; dcg consumes that rejection while retaining already observed blocking frames or overflow. A non-standard synchronous cancel fault that also leaves its pending read unsettled remains outside the JavaScript boundary.
- OMP deadline and signal detail: The 30.5-second observation limit is one monotonic absolute deadline, not a fresh allowance after exit. A post-exit or rejected-exit drain is
- Crush: First-class hook support (#388). Crush runs Claude-Code-style
PreToolUsehooks declared as flat{name, matcher, command, timeout}entries in thehooksobject of itscrush.json.dcg install --crushmerges amatcher: "^bash$"entry into~/.config/crush/crush.json— resolved exactly as Crush does, honoring theCRUSH_GLOBAL_CONFIGdirectory override andXDG_CONFIG_HOME, and~/.configon Windows too — preserving every other key and hook (add--projectfor the repo root'scrush.json;dcg uninstall --crushremoves the entry). Crush pipes{"event":"PreToolUse","tool_name":"bash","tool_input":{"command":…}}to dcg's stdin; dcg recognizes the envelope without an--agentflag and answers with Crush's own{"decision":"deny","reason":…}on exit 0. Before this, that payload was routed to the Copilot arm and answered with a flatpermissionDecisionCrush does not read, so a block silently became "no opinion". dcg never answers"allow"(in Crush that pre-approves the call and skips the user's permission prompt); allowed commands stay silent, warnings travel ascontext, and review requests fail closed. Crush setsCRUSH=1for hook subprocesses, which is what dcg's agent detection keys on.install.sh/install.ps1configure it automatically when Crush is detected, anddcg doctorreports acrush_hookcheck. See docs/crush-integration.md. - Reasonix: First-class hook support (#358). Reasonix runs
PreToolUsehooks declared in itssettings.json.dcg install --reasonixmerges a{"match":"bash|pwsh","command":…,"timeout":5000}entry into/settings.json($REASONIX_HOME, else~/.reasonix, or%APPDATA%\reasonixon Windows) and keeps every other key and hook. Add--projectto write.reasonix/settings.jsonat the repo root instead;dcg uninstall --reasonixremoves the entry. Reasonix pipes{"event":"PreToolUse","toolName":…,"toolArgs":{"command":…}}to dcg and reads only the exit status. dcg blocks with exit 2 and puts the reason on stderr; a warning exits 1, which Reasonix shows without blocking. Reasonix has no "ask", so review requests block. Before this change dcg answered that payload as if it came from Copilot, with a JSON deny on exit 0, so Reasonix ran the command anyway. On Windows, a PowerShell command can arrive labeledbash, so dcg judges the command text rather than the label, as it does for Codex.install.sh/install.ps1configure the hook when Reasonix is detected, the uninstallers remove it, anddcg doctorreports areasonix_hookcheck. See docs/reasonix-integration.md. - Pi: Not auto-configured. Pi intercepts shell commands through user-authored TypeScript extensions (
pi.on("tool_call", …), auto-loaded from~/.pi/agent/extensions/*.tsor/.pi/extensions/*.ts). A ready-to-usedcg-guard.tsextension that routes eachbashcommand throughdcg --robot test(exit 1 = deny) and blocks with the dcg reason is documented in docs/pi-integration.md.
Recommended: After installing, run
dcg setupto add a shell startup check that warns you if the dcg hook is ever silently removed from~/.claude/settings.json.
From source (Rust 1.95+; pinned nightly recommended)
The locked dependency graph requires Rust 1.95 or newer. Release builds use the
repository's known-good nightly-2026-08-25 pin; the included
rust-toolchain.toml selects it automatically inside a checkout.
# Install the release toolchain if you don't have it
rustup toolchain install nightly-2026-08-25
# Install the tagged source reproducibly
cargo +nightly-2026-08-25 install --locked --git https://github.com/Dicklesworthstone/destructive_command_guard --tag v0.7.6 destructive_command_guard
Manual build
git clone https://github.com/Dicklesworthstone/destructive_command_guard
cd destructive_command_guard
# rust-toolchain.toml automatically selects the pinned release nightly
cargo build --release
cp target/release/dcg ~/.local/bin/
Updating
Run the built-in updater to re-run the installer for your platform:
dcg update
Optional flags mirror the installer scripts (examples):
dcg update --version v0.7.6
dcg update --system
dcg update --verify
dcg update --verify --no-configure # binary only; preserve existing hook wiring
You can always re-run install.sh / install.ps1 directly if preferred.
Local builds, pinning, and update refusal (#320)
For most tools, being overwritten by the official release is the right
outcome. For a guard it is not necessarily: a locally built binary may carry
coverage the published release does not have yet, and replacing it silently
downgrades protection. dcg therefore embeds build provenance at compile
time (git describe --tags --dirty, shown as a Commit: line in
dcg --version, plus the full commit object id shown as Git SHA:; release
pipelines additionally set an explicit DCG_RELEASE_BUILD=1 marker) and uses
it three ways:
dcg updaterefuses early — 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. The explicit escape hatch isdcg update --replace-local-build.- An opt-in pin:
general.update_pin = true(orDCG_UPDATE_PIN=1) makes the refusal unconditional and also suppresses the background "update available" nudge, so dcg stops advertising an action it will then refuse. - A doctor check (
build_provenance, warning-only): flags an unpinned local build ahead of its release tag — precisely the state that is one routinedcg updateaway from silent loss — and recommends the pin.
Builds without git metadata (e.g. cargo install from a registry tarball)
have unknown provenance; only the pin applies to them.
Prebuilt Binaries
Prebuilt binaries are available for:
- Linux x86_64, statically linked with musl (
x86_64-unknown-linux-musl) - Linux ARM64 (
aarch64-unknown-linux-gnu) - macOS Intel (
x86_64-apple-darwin) - macOS Apple Silicon (
aarch64-apple-darwin) - Windows x64 (
x86_64-pc-windows-msvc) - Windows ARM64 (
aarch64-pc-windows-msvc)
Download from GitHub Releases and verify the SHA256 checksum.
Starting with v0.7.5, each manually published artifact has an adjacent
.minisig, verifiable with the DSR-managed public key (key ID
69B3955C8D2E62A8). The installers do this automatically when minisign is
installed; pass --require-minisign on Unix or -RequireMinisign on Windows to
require that verification path. The v0.6.7 manual release used the retired key
36B847D11BA5A0D0; installer trust is explicitly scoped to that version.
minisign -Vm dcg-. \
-x dcg-..minisig \
-P 'RWSoYi6NXJWzaRs1mJmOwwXrZfPWcq6MXnQlNMLBYKzlIQTLwuVQG6uO'
Release artifacts may also include a Sigstore bundle (.sigstore.json) for
verification with cosign verify-blob. Workflow builds bind that bundle to the
repository's GitHub Actions OIDC identity; local DSR builds use a pinned
self-managed cosign key (public-key DER SHA256 fingerprint
0e6947743daf39d6413cb25f6c96601427e38885f3a756e9f98f37d66e6df7a4).
The installers accept either trust path, require cosign 2.6.2+/3.0.4+, and still
require the per-artifact SHA256 checksum.
Uninstalling
Remove dcg and all its hooks from AI agents:
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/uninstall.sh | bash
On Windows:
irm https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/uninstall.ps1 | iex
The Unix uninstaller:
- Removes dcg hooks and marker-owned bridges from Claude Code, Codex CLI, Cursor IDE, Gemini CLI, GitHub Copilot CLI (user-level plus legacy repo-local), Hermes Agent, Posit Assistant, OpenCode, Oh My Pi, Crush, Reasonix, and Aider
- Removes the dcg binary
- Removes configuration (
~/.config/dcg/) and history (thehistory.dbSQLite files in~/.config/dcg/or${XDG_STATE_HOME:-~/.local/state}/dcg/, plus~/.local/share/dcg/) - Prompts for confirmation before making changes
The PowerShell uninstaller removes the Windows dcg.exe binary, the exact User PATH entry added by install.ps1, dcg hooks or marker-owned extensions from Claude Code, Codex CLI, Gemini CLI, GitHub Copilot CLI, Cursor IDE, Hermes Agent, Posit Assistant, Oh My Pi, Grok, and Antigravity (agy), plus dcg configuration/history from native %APPDATA% / %LOCALAPPDATA% and any legacy ~/.config / ~/.local/share locations.
Options:
--yes- Skip confirmation prompt--keep-config- Preserve configuration files--keep-history- Preserve history database--purge- Remove everything (overrides keep flags)
Claude Code Configuration
Add to ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|PowerShell",
"hooks": [
{
"type": "command",
"command": "/absolute/path/to/dcg"
}
]
}
]
}
}
Replace /absolute/path/to/dcg with the exact output of command -v dcg.
Never register a safety hook as bare dcg: agent hooks run under a
non-interactive shell whose PATH may omit ~/.local/bin, causing the hook to
fail open. On native Windows, let install.ps1 write the PowerShell-safe
absolute invocation (& 'C:\...\dcg.exe' plus "shell": "powershell").
Claude Code exposes separate Bash and PowerShell shell tools on Windows, so
the combined matcher is required for complete shell coverage. The native
PowerShell installer also runs dcg through an explicitly selected PowerShell
hook shell; this prevents Git Bash from stripping backslashes out of an
absolute C:\...\dcg.exe path. Re-running the installer migrates a legacy
dcg-only Bash entry while preserving unrelated Bash-only hooks.
Important: Restart Claude Code after adding the hook configuration.
The matcher is a regex over the tool name and must cover both shells: on
native Windows, Claude Code runs shell commands through a PowerShell tool, so
a Bash-only matcher leaves every PowerShell command unguarded. dcg install,
the installers, and dcg doctor --fix all write Bash|PowerShell and migrate a
pre-existing Bash-only dcg entry in place (no duplicate hook is added).
Codex CLI Configuration
Codex CLI 0.125.0+ supports stable PreToolUse hooks. The installer writes or
merges this automatically, but the manual configuration lives at
~/.codex/hooks.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/absolute/path/to/dcg"
}
]
}
]
}
}
Codex denials intentionally omit dcg's extended Claude-only fields: dcg exits 0
with the minimal documented hookSpecificOutput JSON on stdout. Allowed
commands stay silent with exit code 0.
Gemini CLI Configuration
Add to ~/.gemini/settings.json:
{
"hooks": {
"BeforeTool": [
{
"matcher": "run_shell_command",
"hooks": [
{
"name": "dcg",
"type": "command",
"command": "/absolute/path/to/dcg",
"timeout": 5000
}
]
}
]
}
}
Important: Restart Gemini CLI after adding the hook configuration.
Crush Configuration
dcg install --crush does this for you (and dcg uninstall --crush undoes
it). By hand, add to ~/.config/crush/crush.json (or a project crush.json):
{
"hooks": {
"PreToolUse": [
{
"name": "dcg",
"matcher": "^bash$",
"command": "/absolute/path/to/dcg",
"timeout": 5
}
]
}
}
Crush pipes the tool call to dcg's stdin as
{"event":"PreToolUse","tool_name":"bash","tool_input":{"command":"…"}} and
reads {"decision":"deny","reason":"…"} back on exit 0. Allowed commands stay
silent (Crush's normal permission prompt still applies — dcg never
pre-approves). Start a new Crush session after editing the config. See
docs/crush-integration.md.
Reasonix Configuration
dcg install --reasonix does this for you (and dcg uninstall --reasonix
undoes it). By hand, add to /settings.json, or to a project
.reasonix/settings.json. The home is $REASONIX_HOME if set, else
~/.reasonix, or %APPDATA%\reasonix on Windows:
{
"hooks": {
"PreToolUse": [
{
"match": "bash|pwsh",
"command": "/absolute/path/to/dcg",
"timeout": 5000
}
]
}
}
Reasonix pipes {"event":"PreToolUse","toolName":"bash","toolArgs":{"command":"…"}}
to dcg's stdin and reads only the exit status. dcg exits 2 to block, and
Reasonix shows the reason from stderr. Reasonix has no "ask" answer, so
review requests block too. See
docs/reasonix-integration.md.
CLI Usage
While primarily designed as a hook, the binary supports direct invocation for testing, debugging, and understanding why commands are blocked or allowed.
# Show version with build metadata
dcg --version
# Show help with blocked command categories
dcg --help
# Test a command manually (pipe JSON to stdin)
echo '{"tool_name":"Bash","tool_input":{"command":"git reset --hard"}}' | dcg
Exclusive File Creation (dcg create-new)
Use create-new as a pipeline sink when the destination must be new. It opens
the final path with the operating system's exclusive-create primitive, streams
stdin byte-for-byte, and writes human status/errors to stderr so stdout stays
empty:
producer | dcg create-new ./artifact.bin
The parent directory must already exist. The command exits non-zero without
modifying anything if the destination is already a file, directory, or symlink;
on Unix, a newly created file starts with private permissions (0600, subject
to the process umask). A later stdin or disk error can leave the newly created
path with a partial stream; create-new never removes or replaces that path.
Test Mode (dcg test)
Use dcg test to evaluate a command without executing it. This is useful for CI checks, false-positive debugging, and config validation before rollout.
Basic Usage
# Basic evaluation (human-readable output)
dcg test "rm -rf ./build"
# Structured output for automation
dcg test --format json "kubectl delete namespace prod" | jq -r .decision
# Use a specific config file
dcg test --config .dcg.prod.toml "docker system prune"
# Temporarily enable extra packs only for this test run
dcg test --with-packs containers.docker,database.postgresql "docker system prune"
# Read the candidate from stdin so it need not appear in dcg's own arguments
dcg test --stdin --format json < candidate-command.txt
# Apply the same wall-clock evaluation budget as the live hook
dcg test --enforce-budget --config .dcg.prod.toml "git status"
# Print full evaluation trace (same engine as `dcg explain`)
dcg test --explain "git reset --hard"
# Evaluate on the single dialect the Bash PreToolUse hook resolves
dcg test --dialect posix "echo 'AT&T'"
Exit Codes
0: command would be allowed1: command would be blocked
Flags and Options
-c, --config: use a specific config file--stdin: read the candidate command from standard input; conflicts with the positionalCOMMAND--with-packs: temporarily enable extra packs--explain: print detailed decision trace-f, --format: output format (default:pretty)--no-color: disable ANSI color output--heredoc-scan: force-enable heredoc/inline-script scanning--no-heredoc-scan: force-disable heredoc/inline-script scanning--heredoc-timeout: override heredoc extraction timeout budget--heredoc-languages: limit heredoc AST scanning languages--enforce-budget: apply the effective live-hook wall-clock deadline (general.hook_timeout_ms,DCG_HOOK_TIMEOUT_MS, or the applicable default)--dialect(alsoDCG_DIALECT): evaluate a single shell dialect instead of all of them. The defaultunknownfans out to every dialect because the CLI cannot know the source shell;posixreproduces the path theBashPreToolUse hook takes andpsthePowerShellone (a CodexBashpayload on native Windows is evaluated aspswhen it cannot parse as POSIX and asunknownotherwise — see docs/codex-integration.md). Use it when a diagnostic must match the live hook — an all-dialect run can report costs and evaluation paths the hook never has (for example, a literal&byte defeats quick-reject only on the all-dialect route). Also available ondcg explain.
Output Formats
pretty: human-readable output with command context, matched rule info, and suggestionsjson: structured payload for scripts/CI; includes metadata likeschema_version,dcg_version,command,decision, rule/pack fields, and allowlist/agent context when presenttoon: token-efficient structured encoding of the same payload used byjson(useful for agent-to-agent/tool pipelines)
CI/CD Integration Examples
Fail fast in shell pipelines:
dcg test --format json "rm -rf /" > /tmp/dcg.json
jq -e '.decision == "allow"' /tmp/dcg.json
Minimal GitHub Actions step:
- name: Validate dangerous command policy
run: |
~/.local/bin/dcg test --format json "git reset --hard HEAD~1" > /tmp/dcg-test.json
jq -e '.decision == "allow"' /tmp/dcg-test.json
Troubleshooting
- Use
--format json(orDCG_FORMAT=json) for machine parsing. - Add
--no-colorif logs or parsers choke on ANSI output. - If results differ between environments, check trusted config precedence
(
DCG_CONFIG, user/system config) plus the enforcement-only settings accepted from an automatically discovered project.dcg.toml. - If a command is unexpectedly allowed, inspect active allowlists (
dcg allowlist list) and enabled packs (dcg packs --verbose). - For full decision traces, run
dcg test --explain ""(ordcg explain "").
Explain Mode
When you need to understand exactly why a command was blocked (or allowed), the dcg explain command provides a detailed trace of the decision-making process:
# Explain why a command is blocked
dcg explain "git reset --hard HEAD"
# Explain a safe command
dcg explain "git status"
# Explain with verbose timing information
dcg explain --verbose "rm -rf /tmp/build"
# Output as JSON for programmatic use
dcg explain --format json "kubectl delete namespace production"
JSON output is versioned via schema_version (currently 4). v2 added
matched_span, matched_text_preview, and explanation in the match
object when a pattern is detected. v3 added the conservative indeterminate
decision. v4 added mode and outcome.
decision is the evaluator's finding; outcome is what the hook does. A
rule set to warn, ask, or log in [policy.rules]/[policy.packs] still
produces decision: "deny" — the pattern did match — and mode names the
configured policy that decides what happens next. outcome collapses the two
into the one answer to gate on:
decision |
mode |
outcome |
Command runs? |
|---|---|---|---|
deny |
deny |
deny |
No |
deny |
ask |
ask |
Only after operator review |
deny |
warn |
warn |
Yes, with a warning |
deny |
log |
log |
Yes, silently recorded |
allow |
absent | allow |
Yes |
indeterminate |
absent | indeterminate |
No — evaluation did not finish |
indeterminate is the value a consumer is most likely to forget and least able
to afford forgetting. dcg emits it when the hook deadline is exhausted or a
nested payload could not be fully evaluated, and it means do not run this:
the guard never downgrades an unfinished evaluation to allow. Treat any
unrecognised outcome the same way.
Note that dcg test --format json is a different, separately versioned surface
whose decision field already carries the resolved outcome, so it has no
outcome field. The table above describes dcg explain --format json only.
The human-readable output reports the same resolved outcome, so
dcg explain, dcg test, and the live hook agree on every rule.
Example Output:
Command: git reset --hard HEAD
Normalized: git reset --hard HEAD
Decision: BLOCKED
Pack: core.git
Rule: reset-hard
Reason: git reset --hard destroys uncommitted changes
Evaluation Trace:
[ 0.8μs] Quick reject: passed (contains 'git')
[ 2.1μs] Normalize: no changes
[ 5.3μs] Safe patterns: no match (checked 34 patterns)
[ 12.7μs] Destructive patterns: MATCH at pattern 'reset-hard'
[ 12.9μs] Total time: 12.9μs
Suggestion: Consider using 'git stash' first to save your changes.
The explain mode shows:
- Normalized command: How dcg sees the command after path normalization
- Decision: Whether the command would be blocked or allowed
- Matching rule: Which pack and pattern triggered the decision
- Evaluation trace: Step-by-step timing of each evaluation stage
- Suggestion: Actionable guidance for safer alternatives
This is invaluable for debugging false positives, understanding pack coverage, and verifying that custom allowlist entries work as expected.
Allow-Once (Temporary Exceptions)
Sometimes you need to run a blocked command temporarily without permanently modifying your allowlist. The allow-once system provides short codes:
# When a command is blocked, dcg outputs a short code
# BLOCKED: git reset --hard HEAD
# Allow-once code: 123456
# To allow this: dcg allow-once 123456
# Use the short code to create a temporary exception
dcg allow-once 123456
# The exception is consumed by the first run. To keep it for repeated runs
# until it expires:
dcg allow-once 123456 --reusable
How Allow-Once Works:
- When dcg blocks a command, it generates a short code (currently 6 numeric digits; collisions are handled via
--pick/--hash) - The code is tied to the exact command that was blocked
- Running
dcg allow-oncecreates a temporary exception - The exception is stored in
~/.config/dcg/pending_exceptions.jsonl - Exceptions are consumed by their first use, or expire after 24 hours (with
--reusable, they last until expiry) - While active, the exception allows the same command in the same directory scope
This workflow is useful for:
- One-time administrative operations that are intentionally destructive
- Migration scripts that need to reset state
- Emergency fixes where permanent allowlist changes aren't appropriate
Security Considerations:
- Short codes are derived from SHA256 (or optional HMAC-SHA256 when
DCG_ALLOW_ONCE_SECRETis set) - Codes are never logged or transmitted
- The pending exceptions file is readable only by the current user
- Expired codes are automatically cleaned up
Rebase Recovery Mode
AI coding agents routinely get stuck when git pull --rebase fails partway — unstaged-changes errors, stash-pop conflicts, interrupted rebases. The documented recovery path is almost always git checkout -- . or git restore , both of which dcg hard-blocks (core.git:checkout-discard, core.git:restore-worktree). Agents then have to stop and ask a human to run the command manually.
Rebase-recovery mode is a narrow, bounded relaxation of those two rules that only fires under a genuine recovery signal. Outside that signal the default block is unchanged.
Two complementary signals unlock recovery:
-
Active rebase state (automatic, zero-config). When
.git/rebase-merge/or.git/rebase-apply/exists, a rebase is in progress and the discard operations are the documented recovery path. dcg detects this state and converts the deny into an allow with a[dcg] Allowing ... → rebase-recovery modenote on stderr. No permit needed. -
Explicit permit cookie (opt-in, short-lived). When the rebase already finished but the worktree is still messy (e.g. after a bad
git stash pop), run:dcg rebase-recover # default ttl: 120s dcg rebase-recover --ttl 60 # custom ttl (max: 600s)This writes a timestamp to
.dcg/rebase-recovery-permitat the repo root. For the next N seconds (or until the first matching allow, whichever comes first),git checkout --andgit restoreare allowed. The permit is single-shot — one successful allow consumes it — so it can't silently unblock later unrelated commands within the TTL.
Scope and safety guarantees:
- Only four rules participate:
core.git:checkout-discard,core.git:checkout-ref-discard,core.git:restore-worktree,core.git:restore-worktree-explicit. - Nothing else is affected.
git reset --hard,git clean -f,git push --force, etc. stay blocked even during an active rebase or with a permit active. - The permit is scoped to the current repo's
.dcg/directory. It does not cross repos. - Expired permits are auto-cleaned on the next check.
Typical recovery flow:
$ git pull --rebase
# ... fails with "unstaged changes" ...
$ git stash
$ git pull --rebase # succeeds
$ git stash pop # leaves messy worktree
$ git checkout -- .
BLOCKED by dcg (core.git:checkout-discard)
... Recovering from a failed `git pull --rebase`?
... Run `dcg rebase-recover` in this repo, then retry the command on its own line
... (a leading `cd &&` is fine; nothing else may share the line).
$ dcg rebase-recover
dcg rebase-recovery permit issued ...
$ git checkout -- . # now allowed, permit consumed
$ git push
See issue #104 for background.
The --version output includes build metadata for debugging:
dcg 0.1.0
Built: 2026-01-07T22:13:10.413872881Z
Rustc: 1.94.0-nightly
Rustc release: 1.94.0-nightly
Rustc commit: 0123456789abcdef0123456789abcdef01234567
Rustc date: 2026-01-06
Rustc host: x86_64-unknown-linux-gnu
Target: x86_64-unknown-linux-musl
Commit: v0.1.0
Git SHA: 0123456789abcdef0123456789abcdef01234567
This metadata is embedded at compile time via vergen, making it easy to identify exactly which build is running when troubleshooting.
The absolute performance certificate compares all four stable Rustc identity
fields with rustc -vV. That is deliberately a native-build check: a binary
cross-compiled on a different compiler host needs separate build attestation
instead of weakening the exact compiler identity requirement.
Repository Scanning
While the hook protects interactive command execution, teams also need protection against destructive commands that get committed into repositories. The dcg scan command extracts executable command contexts from files and evaluates them using the same pattern engine.
What Scan Is (and Is Not)
What it is:
- An extractor-based scanner that understands executable contexts
- Uses the same evaluator as hook mode for consistency
- Supports CI integration and pre-commit hooks
What it is NOT:
- A naive grep that matches strings everywhere
- A replacement for code review
- A static analysis tool for arbitrary languages
The key difference from grep: dcg scan understands that "rm -rf /" in a comment is data, not code. It uses extractors that understand file structure (shell scripts, Dockerfiles, CI workflows, package scripts, Makefiles, Terraform, Docker Compose) to find only actually-executed commands.
Supported File Formats
dcg scan includes specialized extractors for each file format, understanding which parts contain executable commands:
| File Type | Detection | Executable Contexts |
|---|---|---|
| Shell Scripts | *.sh, *.bash, *.zsh, *.dash, *.ksh |
Non-comment executable command lines |
| Dockerfile | Dockerfile, Dockerfile.*, *.dockerfile |
RUN instructions (shell and exec forms) |
| GitHub Actions | .github/workflows/*.yml, .github/workflows/*.yaml |
run: fields in steps |
| GitLab CI | .gitlab-ci.yml, *.gitlab-ci.yml |
script:, before_script:, after_script: |
| Azure Pipelines | azure-pipelines.yml, azure-pipelines.yaml, azure-pipelines-*.yml, azure-pipelines-*.yaml |
script:, bash:, powershell:, pwsh: tasks |
| CircleCI | .circleci/config.yml, .circleci/config.yaml |
run: steps and nested command: fields |
| Makefile | Makefile |
Tab-indented recipe lines |
| package.json | package.json |
scripts object values |
| Terraform | *.tf |
provisioner blocks (local-exec, remote-exec) |
| Docker Compose | docker-compose.yml, docker-compose.yaml, compose.yml, compose.yaml |
command:, entrypoint:, healthcheck.test: fields |
| PowerShell | *.ps1, *.psm1, *.psd1 |
Executable statements with line and block comments excluded |
| Batch Scripts | *.cmd, *.bat |
Executable command lines with comments excluded |
Context-Aware Extraction:
Each extractor understands its format's semantics:
# GitHub Actions - only 'run:' is extracted
- name: Build
run: | # ← Extracted
npm install
npm run build
env:
NODE_ENV: production # ← Skipped (not executable)
# Dockerfile - only RUN instructions
FROM node:18
COPY . /app # ← Skipped
RUN npm install # ← Extracted
RUN ["node", "server.js"] # ← Extracted (exec form)
ENV PORT=3000 # ← Skipped
# Makefile - tab-indented lines under targets
build:
npm install # ← Extracted (recipe line)
npm run build # ← Extracted
SOURCES = $(wildcard *.js) # ← Skipped (variable assignment)
Non-Executable Context Filtering:
Extractors intelligently skip data-only sections:
- Shell: Assignment-only lines (
export VAR=value) - YAML:
environment:,labels:,volumes:,variables:blocks - Terraform: Everything outside
provisionerblocks - All formats: Comments (format-appropriate:
#,//, etc.)
Quick Start
# Install the pre-commit hook
dcg scan install-pre-commit
# Or manually run on staged files
dcg scan --staged
# Scan specific paths
dcg scan --paths scripts/ .github/workflows/
# Enable extra packs for this scan without changing persistent config
dcg scan --paths scripts/ --with-packs careful_company_running_windows
Recommended Rollout Plan
Start conservative to avoid developer friction:
# Week 1-2: Warn-first with narrow scope
dcg scan --staged --fail-on error # Only fail on catastrophic rules
Create .dcg/hooks.toml with conservative defaults:
[scan]
fail_on = "error" # Only fail on high-confidence catastrophic rules
format = "pretty" # Human-readable output
redact = "quoted" # Hide sensitive strings
truncate = 120 # Shorten long commands
[scan.paths]
include = [
".github/workflows/**", # Start with CI configs
"Dockerfile", # Container builds
"Makefile", # Build scripts
]
exclude = [
"target/**",
"node_modules/**",
"vendor/**",
]
Gradual expansion:
- Week 1-2: Start with workflows/Dockerfiles only,
--fail-on error - Week 3-4: Add Makefiles and shell scripts in
scripts/ - Month 2: Add
--fail-on warningafter reviewing findings - Ongoing: Add new extractors as team confidence grows
Pre-Commit Integration
One-Command Install
dcg scan install-pre-commit
This creates a .git/hooks/pre-commit that runs dcg scan --staged.
Manual Setup
If you prefer manual control or use a hook manager:
#!/bin/bash
# .git/hooks/pre-commit (or equivalent for your hook manager)
set -e
# Run dcg scan on staged files
dcg scan --staged --fail-on error
# Add other hooks below...
Uninstall
dcg scan uninstall-pre-commit
This only removes hooks installed by dcg (detected via sentinel comment).
Interpreting Findings
The output includes:
scripts/deploy.sh:42:5: [ERROR] core.git:reset-hard
Command: git reset --hard HEAD
Reason: git reset --hard destroys uncommitted changes
Suggestion: Consider using 'git stash' first to save changes.
- File:Line:Col: Location in the source file
- Severity:
ERROR(catastrophic) orWARNING(concerning) - Rule ID: Stable identifier like
core.git:reset-hard - Command: The extracted command (may be redacted/truncated)
- Reason: Why this command is flagged
- Suggestion: How to make it safer
Fixing Findings
Option 1: Change the Code (Preferred)
Replace the dangerous command with a safer alternative:
# Instead of:
git reset --hard
# Use:
git stash push -m "before reset"
git reset --hard
Option 2: Understand with Explain
Get detailed analysis:
dcg explain "git reset --hard HEAD"
Option 3: Allowlist (When Intentional)
If the command is genuinely needed:
# User-owned exception scoped to this checkout
repo_root=$(git rev-parse --show-toplevel)
dcg allowlist add core.git:reset-hard --reason "Required for CI cleanup" \
--user --path "$repo_root" --path "$repo_root/**"
# Or for a specific command
dcg allowlist add-command "rm -rf ./build" --reason "Build cleanup" \
--user --path "$repo_root" --path "$repo_root/**"
The finding output includes a copy-paste allowlist command for convenience.
Heredoc rules use stable IDs like heredoc.python.shutil_rmtree.
Privacy and Redaction
Scan supports redaction of potentially sensitive content in output. Use --redact quoted to hide quoted strings that may contain secrets:
# Original command:
curl -H "Authorization: Bearer $TOKEN" https://api.example.com
# With --redact quoted:
curl -H "..." https://api.example.com
Options:
--redact none: Show full commands (default)--redact quoted: Hide quoted strings (recommended for CI logs)--redact aggressive: Hide more potential secrets
Configuration Reference
.dcg/hooks.toml (project-level, committed):
[scan]
# Exit non-zero when findings meet this threshold
fail_on = "error" # Options: none, warning, error
# Output format
format = "pretty" # Options: pretty, json, markdown
# Maximum file size to scan (bytes)
max_file_size = 1000000
# Stop after this many findings
max_findings = 50
# Redaction level for sensitive content
redact = "quoted" # Options: none, quoted, aggressive
# Truncate long commands (chars; 0 = no truncation)
truncate = 120
[scan.paths]
# Only scan files matching these patterns
include = [
"scripts/**",
".github/workflows/**",
"Dockerfile*",
"Makefile",
]
# Skip files matching these patterns
exclude = [
"target/**",
"node_modules/**",
"*.md",
]
CLI flags override config file values.
CI Integration
GitHub Actions
name: Security Scan
on: [pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install dcg
run: |
curl -fsSL "https://raw.githubusercontent.com/Dicklesworthstone/d