Dry-Run and Preview Patterns
The single most important safety rule for AI agents interacting with infrastructure: always preview before applying. This lesson covers dry-run flags, preview commands, and wrapper scripts that enforce this pattern across every tool.
The Golden Rule: Plan Before Apply
Every major infrastructure tool has a "preview" or "dry-run" mode that shows what would happen without actually making changes. When AI agents work with infrastructure, these preview commands should always run first:
| Tool | Preview Command | Apply Command | Key Flag |
|---|---|---|---|
| Terraform | terraform plan | terraform apply | -out=plan.tfplan |
| Pulumi | pulumi preview | pulumi up | --diff |
| Kubernetes | kubectl diff | kubectl apply | --dry-run=server |
| Helm | helm template / helm diff | helm upgrade | --dry-run |
| Azure CLI | az deployment what-if | az deployment create | --what-if |
| AWS CDK | cdk diff | cdk deploy | N/A |
| CloudFormation | aws cloudformation create-change-set | aws cloudformation execute-change-set | Change sets |
| Ansible | ansible-playbook --check | ansible-playbook | --check --diff |
Terraform Plan: The Gold Standard
Terraform's plan/apply workflow is the model all agent-infrastructure interactions should follow:
#!/bin/bash # STEP 1: Always plan first and save the plan file terraform plan -out=agent-plan.tfplan # STEP 2: Review the plan output for destructive changes # Look for: "destroy", "replace", "forces replacement" terraform show agent-plan.tfplan # STEP 3: Only if the plan looks safe, apply the saved plan # Using the saved plan ensures exactly what was reviewed gets applied terraform apply agent-plan.tfplan # NEVER do this - it skips the review step: # terraform apply -auto-approve ← DANGEROUS!
terraform apply -auto-approve. This skips the plan review entirely. Always use the two-step plan-then-apply workflow with a saved plan file.Kubernetes --dry-run Modes
Kubernetes offers two dry-run modes, and the distinction matters for safety:
# Client-side dry-run: validates YAML locally only # Does NOT check against the cluster (less accurate) kubectl apply -f deployment.yaml --dry-run=client -o yaml # Server-side dry-run: sends to API server for validation # Checks admission webhooks, schema, etc. (more accurate) kubectl apply -f deployment.yaml --dry-run=server -o yaml # kubectl diff: shows what would change (BEST for review) # Compares your YAML against the live cluster state kubectl diff -f deployment.yaml # Safe agent workflow: # 1. Run kubectl diff first # 2. Review the diff output # 3. Only then run kubectl apply
Azure What-If Deployments
Azure Resource Manager provides a what-if operation that previews changes before deploying:
# Preview what an ARM/Bicep deployment would change az deployment group what-if \ --resource-group myResourceGroup \ --template-file main.bicep \ --parameters @parameters.json # Output shows color-coded changes: # + Create (green) - new resources # ~ Modify (purple) - changed properties # - Delete (red) - resources to be removed # = NoChange - unchanged resources # For subscription-level deployments: az deployment sub what-if \ --location eastus \ --template-file main.bicep
Building Dry-Run into Agent Workflows
The key challenge is making sure agents always run the preview step, not just sometimes. Here's how to enforce this:
CLAUDE.md / Agent Config Rules
Add explicit rules: "ALWAYS run terraform plan before terraform apply" and "ALWAYS run kubectl diff before kubectl apply."
Shell Wrapper Scripts
Replace the real CLI tools with wrapper scripts that enforce the plan-first pattern automatically.
Pre-execution Hooks
Use Claude Code hooks or custom middleware to intercept apply commands and inject a plan step.
CI/CD Enforcement
Move all apply operations to CI/CD pipelines where plan-before-apply is structurally enforced.
Parsing Plan Output to Detect Destructive Changes
After running a plan/preview, you need to programmatically check for dangerous changes. Here's a Python script that parses Terraform plan output:
import json import subprocess import sys def check_terraform_plan(plan_file): """Parse a Terraform plan and flag destructive changes.""" # Convert plan to JSON for parsing result = subprocess.run( ["terraform", "show", "-json", plan_file], capture_output=True, text=True ) plan = json.loads(result.stdout) destroys = [] replaces = [] creates = [] for change in plan.get("resource_changes", []): actions = change.get("change", {}).get("actions", []) resource = f"{change['type']}.{change['name']}" if "delete" in actions: destroys.append(resource) if "create" in actions and "delete" in actions: replaces.append(resource) if actions == ["create"]: creates.append(resource) # Report findings print(f"Plan Summary:") print(f" Creates: {len(creates)}") print(f" Replaces: {len(replaces)}") print(f" Destroys: {len(destroys)}") if destroys: print(f"\n⛔ DESTRUCTIVE CHANGES DETECTED:") for r in destroys: print(f" - {r} will be DESTROYED") print("\nBlocking apply. Review required.") sys.exit(1) if replaces: print(f"\n⚠️ REPLACEMENT CHANGES (destroy + recreate):") for r in replaces: print(f" - {r} will be REPLACED") print("\nManual approval required.") sys.exit(2) print("\n✅ Plan looks safe. No destructive changes.") sys.exit(0) if __name__ == "__main__": check_terraform_plan(sys.argv[1])
Wrapper Scripts That Enforce Plan-Before-Apply
Replace the actual CLI commands with wrapper scripts that enforce the preview step. Place these wrappers earlier in $PATH than the real tools:
#!/bin/bash # /usr/local/bin/safe-terraform # Wrapper that prevents terraform apply without a saved plan REAL_TERRAFORM="/usr/bin/terraform" if [[ "$1" == "apply" ]]; then # Check if a plan file was provided if [[ "$2" != *.tfplan ]]; then echo "❌ BLOCKED: terraform apply requires a saved plan file." echo " Run: terraform plan -out=plan.tfplan" echo " Then: terraform apply plan.tfplan" exit 1 fi # Check for -auto-approve flag if echo "$@" | grep -q "auto-approve"; then echo "❌ BLOCKED: -auto-approve is not allowed." echo " Use a saved plan file instead." exit 1 fi fi if [[ "$1" == "destroy" ]]; then echo "❌ BLOCKED: terraform destroy is not allowed via CLI." echo " Use the CI/CD pipeline for destroy operations." exit 1 fi # Pass through all other commands $REAL_TERRAFORM "$@"
#!/bin/bash # /usr/local/bin/safe-kubectl # Wrapper that enforces diff-before-apply for kubectl REAL_KUBECTL="/usr/bin/kubectl" if [[ "$1" == "delete" ]] && [[ "$2" == "namespace" ]]; then echo "❌ BLOCKED: kubectl delete namespace is not allowed." echo " Use the CI/CD pipeline for namespace deletion." exit 1 fi if [[ "$1" == "apply" ]]; then echo "Running kubectl diff first..." $REAL_KUBECTL diff "${@:2}" DIFF_EXIT=$? if [[ $DIFF_EXIT -eq 0 ]]; then echo "No changes to apply." exit 0 elif [[ $DIFF_EXIT -eq 1 ]]; then echo "" read -p "Apply these changes? (y/N): " confirm if [[ "$confirm" == "y" ]]; then $REAL_KUBECTL apply "${@:2}" else echo "Apply cancelled." exit 0 fi else echo "❌ Diff failed. Not proceeding with apply." exit 1 fi else $REAL_KUBECTL "$@" fi
bin/ directory and add it to $PATH at the start of each agent session. This way, every terraform or kubectl command the agent runs goes through your safety layer automatically.Dry-Run Flags Across CLI Tools
A comprehensive reference of dry-run flags across common CLI tools that AI agents might use:
| Tool | Dry-Run Flag | What It Does |
|---|---|---|
rsync | --dry-run or -n | Shows what files would be transferred |
rm | -i (interactive) | Prompts before each removal |
docker compose | --dry-run | Shows what commands would execute |
npm | --dry-run | Shows what would be published/installed |
pip | --dry-run | Shows what would be installed |
aws s3 rm | --dryrun | Shows what objects would be deleted |
gcloud | --dry-run (limited) | Validates without executing |
make | -n | Shows commands without executing |
Key Takeaways
- Always preview infrastructure changes before applying — no exceptions
- Use saved plan files (e.g.,
terraform plan -out=plan.tfplan) to ensure what was reviewed is exactly what gets applied - Enforce plan-before-apply with wrapper scripts placed in the agent's
$PATH - Parse plan output programmatically to auto-detect and block destructive changes
- Block
-auto-approve,destroy, anddelete namespacecommands at the wrapper level - Move apply operations to CI/CD pipelines where plans are structurally required
Lilly Tech Systems