Protect your repositories from destructive commands before they're committed.
This guide covers how to integrate dcg scan into your pre-commit workflow safely, with a focus on gradual rollout and minimizing false positive friction.
dcg scan analyzes files for destructive commands in executable contexts:
- GitHub Actions workflows (
.github/workflows/*.yml) - Dockerfiles (
RUNcommands) - Shell scripts (
*.sh,*.bash) - Makefiles (recipe lines)
- GitLab CI (
.gitlab-ci.yml) - Docker Compose (
command:fields) - Terraform (
local-execprovisioners)
The key difference from the real-time hook (dcg on its own):
| Mode | Protects | When |
|---|---|---|
Hook (dcg) |
Interactive agent commands | At execution time |
Scan (dcg scan) |
Commands in committed files | At commit/CI time |
- Not a full static analysis engine. It does not understand your shell logic, variable expansion, or conditional branches.
- Not a naive grep. It uses extractors that understand file formats and only matches commands in executable contexts (not comments, documentation, or string literals).
- Not a replacement for the hook. Use both: the hook protects interactive execution; scan protects your repository.
# Navigate to your git repository
cd /path/to/your/repo
# Install the pre-commit hook
dcg scan install-pre-commitThis creates .git/hooks/pre-commit with a dcg-managed hook that runs dcg scan --staged before each commit.
If you already have a pre-commit hook or use a hook manager:
# Add this line to your existing hook
dcg scan --stagedFor hook managers like Husky, Lefthook, or pre-commit.com, see Hook Manager Examples below.
dcg scan uninstall-pre-commitTL;DR: Start conservative, expand gradually. Don't turn on warning-as-fail on day one.
Enable scanning with defaults - only catastrophic rules (fail_on = error) block commits.
# .dcg/hooks.toml
[scan]
fail_on = "error" # Only block on high-confidence catastrophic rules
format = "pretty" # Human-readable outputDuring this phase:
- Collect feedback from the team on false positives
- Use
dcg explain "<command>"to understand why something was flagged - Build up your allowlist for legitimate use cases
After the team is comfortable:
-
Add more file types to scanning:
[scan.paths] include = [ ".github/workflows/**", "Dockerfile*", "Makefile", "scripts/**/*.sh", ]
-
Consider enabling warning-as-fail for specific high-risk patterns:
# Test locally before enforcing dcg scan --staged --fail-on warning
Once local pre-commit is stable, add CI enforcement:
# .github/workflows/dcg-scan.yml
name: DCG Scan
on: [pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: |
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/destructive_command_guard/main/install.sh | bash
- run: dcg scan --git-diff origin/${{ github.base_ref }}...HEAD --fail-on error[ERROR] .github/workflows/ci.yml:42
Rule: core.git:reset-hard
Severity: critical
Reason: git reset --hard destroys uncommitted changes
→ dcg allow core.git:reset-hard -r "CI cleanup" --project
| Field | Description |
|---|---|
[ERROR]/[WARN]/[INFO] |
Severity level - ERROR blocks by default |
| File path + line | Where the command was extracted from |
Rule: |
Stable rule ID (pack_id:pattern_name) for allowlisting |
Severity: |
critical, high, medium, low - how dangerous |
Reason: |
Why this command is flagged |
→ suggestion |
How to fix or allowlist |
| Severity | Default action | Examples |
|---|---|---|
critical |
Block (error) |
git reset --hard, rm -rf /, DROP DATABASE |
high |
Block (error) |
git push --force, docker system prune -a |
medium |
Warn | Context-dependent patterns |
low |
Inform | Advisory patterns, low confidence |
Many destructive commands have safer alternatives:
| Instead of | Use |
|---|---|
git reset --hard |
git stash or targeted git checkout |
git push --force |
git push --force-with-lease |
rm -rf ./build |
Use build tool's clean (e.g., cargo clean, make clean) |
docker system prune -af |
docker image prune --filter "until=24h" |
# See why a command is flagged
dcg explain "git reset --hard HEAD~1"This shows the full decision trace: which pack matched, what pattern triggered, and whether any allowlists applied.
If the command is safe in your context (e.g., a CI cleanup step), allowlist it:
# Allowlist by rule ID (recommended - most stable)
dcg allow core.git:reset-hard -r "Used for CI cleanup after tests" --project
# Or allowlist a specific command (exact match)
dcg allowlist add-command "git reset --hard HEAD" -r "CI cleanup" --projectImportant safety notes:
- Always provide a reason (
-r "...") - document why this is safe - Prefer
--projectfor project-specific overrides (stored in.dcg/allowlist.toml) - Use expiration for temporary overrides:
dcg allow core.git:reset-hard -r "Migration" --expires "2026-02-01T00:00:00Z" --project
# List all allowlist entries
dcg allowlist list
# List project-level only
dcg allowlist list --project
# Remove an entry
dcg allowlist remove core.git:reset-hard --project
# Validate allowlist files
dcg allowlist validateBy default, scan output shows full commands. In CI, this could expose secrets.
# .dcg/hooks.toml
[scan]
redact = "quoted" # Redact quoted strings: rm -rf "[REDACTED]"
# redact = "aggressive" # Redact more aggressively
# redact = "none" # Show full commands (default, for local use)- Use
redact = "quoted"in CI to avoid printing secrets in logs - Limit output with
truncateandmax_findings:[scan] truncate = 100 # Truncate long command output max_findings = 50 # Limit total findings per scan
- Use JSON format for machine parsing in CI:
[scan] format = "json"
Create this file in your repository root to configure scan behavior:
[scan]
# Output format: "pretty" (human-readable) or "json"
format = "pretty"
# When to fail: "error", "warning", or "none"
fail_on = "error"
# Maximum file size to scan (bytes) - larger files are skipped
max_file_size = 1048576 # 1MB
# Maximum findings to report per run
max_findings = 100
# Command redaction: "none", "quoted", or "aggressive"
redact = "none"
# Truncate long command output (0 = no truncation)
truncate = 200
[scan.paths]
# Glob patterns to include (default: all supported file types)
include = [
".github/workflows/**",
"Dockerfile*",
"**/Makefile",
"scripts/**/*.sh",
]
# Glob patterns to exclude
exclude = [
"vendor/**",
"node_modules/**",
"**/testdata/**",
]dcg scan --staged \
--format json \
--fail-on warning \
--max-file-size 2097152 \
--exclude "tests/**" \
--include "*.sh"CLI flags always take precedence over .dcg/hooks.toml.
# .husky/pre-commit
dcg scan --stagedCreate with: npx husky add .husky/pre-commit "dcg scan --staged"
# lefthook.yml
pre-commit:
commands:
dcg-scan:
run: dcg scan --staged# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: dcg-scan
name: dcg scan
entry: dcg scan --staged
language: system
pass_filenames: falseThe pre-commit hook cannot find dcg. Either:
- Install dcg globally:
cargo install destructive_command_guard - Add dcg's location to PATH before running git commands
- Use an absolute path in your hook configuration
You already have a pre-commit hook not installed by dcg. Options:
- Add dcg to your existing hook: Add
dcg scan --stagedto your hook script - Replace the hook: Delete it manually, then re-run
dcg scan install-pre-commit
If dcg flags a command that's safe:
- Investigate:
dcg explain "the-command"to understand why - Allowlist if needed:
dcg allow <rule_id> -r "reason" --project - Report: If it's a pattern bug, file an issue
Scan performance depends on:
- Number of staged files - only changed files are scanned
- File sizes - use
max_file_sizeto skip large files - Pattern count - enable only needed packs in your config
- Install:
dcg scan install-pre-commit - Configure: Create
.dcg/hooks.tomlwith your settings - Start conservative:
fail_on = "error"initially - Expand gradually: Add more file types, consider warning-as-fail
- Allowlist false positives:
dcg allow <rule_id> -r "reason" --project - Add CI enforcement: Scan PR diffs in GitHub Actions/GitLab CI