CASE FILE#5670CRITFiled 2026-06-16

How to Stop an AI Coding Agent from Running Destructive Commands

An illustrative scenario where Claude Code is about to wipe a production database, force-delete a directory, and force-push over main. SSG blocks all three before they run. Here are the rules.

Case ID
#5670
Severity
CRIT
Decision
BLOCK
Agent
Claude Code
Category
Shell Safety

Correction note (2026-10-13). An earlier version of this article used ssg audit flags that do not exist (--decision DENY, --rule, and --tool bash without the search subcommand). The commands below use ssg audit search. The per-call timings and latency figures that appeared in this article had no benchmark behind them and were removed. Sample output in this article is illustrative, not a recorded run. The only published measurement is p50 0.39 ms and p99 1.91 ms (sigmashake-bench daemon-socket run, 2026-06-10, ssg 0.29.156, n=500). Sub-2 ms evaluation is a design target. The local audit log records decisions on a best-effort basis: entries can be dropped if the daemon queue is full, and old rows are deleted at the row caps. It is local, not append-only, and not immutable. The init adapter ids are claude-code and copilot, not claude or vscode. ssg eval reads the tool call as JSON on stdin and has no --content, --content-file, --file or positional form.

Three commands that should have stopped your heart

You hand Claude Code a cleanup task: remove stale migration files, clear a local test database, and squash a few commits before pushing. Normal housekeeping. Ten seconds later the agent has queued three tool calls.

In YOLO mode, all three execute. The application directory is gone. The production database is dropped. Three days of commits from two other engineers are overwritten.

With SSG active, the daemon intercepts each one before it runs:

# illustrative sample output
[BLOCK] bash -> recursive force-delete on /home/deploy/app
        rule: no-rf-outside-project
[BLOCK] bash -> wipe production database via SQL
        rule: no-drop-prod-db
[ASK]   bash -> force-push to main
        rule: ask-force-push-main

The first two are hard blocks. The third goes to the approval dashboard because a force-push on a feature branch is sometimes legitimate -- but on main it should never be automatic.

Why these errors keep happening

Each of these commands looks reasonable in isolation. That is the problem.

The rm command with the recursive and force flags is the standard way to clear a directory. An agent tasked with "clearing the build cache" constructs the correct shell form; it just miscalculates the path. The command is structurally correct and executes instantly.

A SQL database-drop is the right way to wipe a local test fixture. Claude Code has no way to distinguish test_db from production by name alone unless you tell it -- and agents routinely inherit database URLs from environment variables without noticing the string says production in it.

Force-push is how you push a squashed branch. The agent knows this. It does not know that your team has a hard rule against rewriting shared history on main, because that rule lives in a Slack channel message from six months ago, not anywhere the agent can read.

The common thread: the agent is doing what it was asked to do, using the correct tool, and it is wrong because context that lives outside the code is not available to it at the moment it acts.

The rules that stop these three

Add to .sigmashake/rules/destructive.rules:

rule no-rf-outside-project {
  enabled true
  priority 100
  severity error
  DENY execution
  IF command REGEX "rm\\s+-[a-zA-Z]*r[a-zA-Z]*f"
  AND command NOT CONTAINS "./dist"
  AND command NOT CONTAINS "./build"
  AND command NOT CONTAINS "./node_modules"
  AND command NOT CONTAINS "./.next"
  AND command NOT CONTAINS "/tmp/"
  MESSAGE "Recursive force-delete outside known build directories is blocked. Target a specific build path."
}

rule no-drop-prod-db {
  enabled true
  priority 100
  severity error
  DENY execution
  IF command REGEX "([dD][rR][oO][pP]\\s+[dD][aA][tT][aA][bB][aA][sS][eE]|[dD][rR][oO][pP]\\s+[sS][cC][hH][eE][mM][aA])"
  AND command REGEX "([pP][rR][oO][dD]|[pP][rR][oO][dD][uU][cC][tT][iI][oO][nN]|[lL][iI][vV][eE]|[mM][aA][iI][nN])"
  MESSAGE "Dropping a production-named database is blocked. Target a local test database explicitly."
}

rule ask-force-push-main {
  enabled true
  priority 95
  severity error
  ASK execution
  IF command CONTAINS "push" AND command CONTAINS "--force"
  AND command REGEX "\\s+[oO][rR][iI][gG][iI][nN]\\s+([mM][aA][iI][nN]|[mM][aA][sS][tT][eE][rR]|[rR][eE][lL][eE][aA][sS][eE]|[pP][rR][oO][dD])"
  MESSAGE "Force-push to a protected branch requires human approval."
  PROMPT "Allow this force-push to a protected branch?"
}

Pull a pre-built version from the Hub:

ssg hub pull rules-destructive

Why DENY for two commands but ASK for the third

The distinction matters. A recursive force-delete targeting an absolute path outside your project's build directories, and a database-drop targeting a production-named database, have essentially no legitimate automated use case that outweighs the catastrophic downside. DENY is the right call.

Force-push is different. Pushing forcefully to a feature branch is normal and correct after an interactive rebase. The ask-force-push-main rule is scoped to protected branch names (main, master, release, prod). On those branches, force-push rewrites shared history -- a decision a human should make explicitly, not a side effect of a cleanup task. ASK routes it to the dashboard; you approve it once, it runs, and the audit log records your decision.

If your team never force-pushes anything, change ASK to DENY. The rule syntax is identical.

The audit trail

Blocked and approved actions are written to SSG's local SQLite audit log on a best-effort basis. After a session you can query it:

# See everything that was blocked in the last 24 hours
ssg audit search --since 24h --decision block

# See force-push attempts specifically
ssg audit search --rule-id ask-force-push-main

# See what ran on a specific day
ssg audit search --since 2026-06-15 --until 2026-06-16 --capability execute

The log includes the full command string, the matched rule, the decision, and the timestamp. If an agent did manage to run something unexpected, the log tells you exactly when and what.

Guide the agent to safe alternatives

When a command is blocked, the agent receives the MESSAGE field as a tool error. It reads this and can course-correct. Extend the message with specific guidance.

For the recursive delete block, tell the agent which paths are whitelisted:

The recursive delete outside build directories was blocked.
To clear build artifacts, target a specific path:
  rm -r -f ./dist    (or ./build, ./.next -- these are whitelisted)
Do not target absolute paths or paths outside the project.

For the database drop block, name the correct target explicitly:

Dropping a production-named database was blocked.
The local test database is named "app_test", not "production".
Target it by name: psql -c "DROP SCHEMA app_test CASCADE;"

For the force-push, offer the non-destructive alternative:

Force-push to main is pending human approval in the SSG dashboard.
To push without rewriting history: git push origin main
(without the force flag)

Agents recover well when a block comes with a concrete alternative. The SSG MESSAGE field is the right place to embed that guidance permanently so every future block on that rule delivers it automatically.

Tune the recursive-delete rule for your project

The no-rf-outside-project rule uses AND command NOT CONTAINS to whitelist known build output paths. Extend this list to match your actual layout:

rule no-rf-outside-project {
  enabled true
  priority 100
  severity error
  DENY execution
  IF command REGEX "rm\\s+-[a-zA-Z]*r[a-zA-Z]*f"
  AND command NOT CONTAINS "./dist"
  AND command NOT CONTAINS "./build"
  AND command NOT CONTAINS "./node_modules"
  AND command NOT CONTAINS "./.next"
  AND command NOT CONTAINS "./target"
  AND command NOT CONTAINS "./.turbo"
  AND command NOT CONTAINS "/tmp/"
  AND command NOT CONTAINS "/var/tmp/"
  MESSAGE "Recursive force-delete outside known build directories is blocked."
}

Validate your changes:

ssg lint .sigmashake/rules/destructive.rules

Test specific command strings before going live:

echo '{"tool":"Bash","input":{"command":"rm -r -f /home/deploy/app"}}' | ssg eval   # should DENY
echo '{"tool":"Bash","input":{"command":"rm -r -f ./dist"}}' | ssg eval              # should ALLOW

Use ASK mode first to learn what your agent actually runs

If you are not sure which commands your agent runs day-to-day, start in ASK mode for all destructive patterns rather than DENY. This surfaces every match in the dashboard without blocking anything:

rule review-destructive-cmds {
  enabled true
  priority 95
  severity warning
  ASK execution
  IF command REGEX "rm\\s+-[a-zA-Z]*r[a-zA-Z]*f"
  OR command REGEX "[dD][rR][oO][pP]\\s+([dD][aA][tT][aA][bB][aA][sS][eE]|[tT][aA][bB][lL][eE]|[sS][cC][hH][eE][mM][aA])"
  OR command CONTAINS "push" AND command CONTAINS "--force"
  MESSAGE "Destructive command -- review before approving."
  PROMPT "Allow this destructive command to run?"
}

Run a few normal agent sessions. Review the approvals in ssg serve. When you know which are always-safe and which are always-risky, graduate from ASK to ALLOW or DENY per specific pattern.

Get started

# macOS / Linux — free installer
curl -fsSL https://install.sigmashake.com | sh

# Windows (PowerShell)
# iwr -useb https://install.sigmashake.com/install.ps1 | iex

# Initialize in your project (wires the Claude Code PreToolUse hook automatically)
ssg init --client=claude-code

# Pull the destructive-command ruleset from the Hub
ssg hub pull rules-destructive

# Start the daemon
ssg daemon &

# Open the approval dashboard (where ASK decisions queue up)
ssg serve

Links: Download · Docs · Playground · Hub

Filed by Claude Code
Engine ssg v1 · 2026-06-16
End of case file

Stop reading. Start governing.

Test a rule against any tool call right now. No install required.