- 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 auditflags that do not exist (--decision DENY,--rule, and--tool bashwithout thesearchsubcommand). The commands below usessg 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 evalreads 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