Prompt Details
Model
(claude-4-6-sonnet)
Token size
540
Example input
[HOOK_TYPE]: PreToolUse
[RULE_DESCRIPTION]: Block any Bash tool call whose command string contains "rm -rf" or "rm -r /" targeting paths outside /tmp β log the attempt to ~/.claude/blocked_commands.log with timestamp and working directory
[PROJECT_CONTEXT]: Node.js monorepo with 3 packages under /packages; CI pipeline has no dry-run mode so accidental deletions in dev have caused incidents twice this quarter
Example output
# Claude Code Hook: Bash `rm -rf` Interceptor
---
## 1. HOOK SCRIPT
Save to: `{PROJECT_ROOT}/.claude/hooks/block_dangerous_rm.sh`
#!/usr/bin/env bash
# Using bash (not sh) for process substitution and robust string ops.
# Requires: jq >= 1.6 OR python3 (fallback). Tested on macOS 14, Ubuntu 22.
set -euo pipefail
# ββ Constants ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
LOG_FILE="${HOME}/.claude/blocked_commands.log"
SAFE_PREFIX="/tmp" # Only path root exempted from blocking
# ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
log_blocked() { # $1=command $2=cwd
mkdir -p "$(dirname "$LOG_FILE")" # Idempotent: no-op if dir exists
printf '[%s] BLOCKED | cwd=%s | cmd=%s\n' \
"$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$2" "$1" \
>> "$LOG_FILE" # Append-only; safe for concurrent runs
}
parse_field() { # $1=json $2=field-path
if command -v jq &>/dev/null; then
printf '%s' "$1" | jq -r "$2"
else # Fallback: python3 always present in Node envs
printf '%s' "$1" | python3 -c \
"import sys,json; d=json.load(sys.stdin); \
keys='$2'.lstrip('.').split('.'); \
v=d; [v:=v[k] for k in keys]; print(v if v else '')"
fi
}
# ββ Read stdin once (hook contract: single JSON object on stdin) βββββββββββ
INPUT="$(cat -)" # Buffer so we can query multiple fields
TOOL_NAME="$(parse_field "$INPUT" '.tool_name')"
CMD="$(parse_field "$INPUT" '.tool_input.command')"
CWD="$(parse_field "$INPUT" '.tool_input.working_directory // ""')"
# Normalise CWD: fall back to $PWD if the field is absent or literal "null"
[[ "$CWD" == "null" || -z "$CWD" ]] && CWD="$PWD"
# ββ Guard: only inspect Bash tool calls βββββββββββββββββββββββββββββββββββ
[[ "$TOOL_NAME" != "Bash" ]] && exit 0 # Allow all non-Bash tools immediately
# ββ Pattern detection βββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Match "rm -rf", "rm -fr", "rm -r /" in any position within the command.
# We intentionally do NOT try to parse shell AST; see Edge Cases section.
DANGEROUS=0
if [[ "$CMD" =~ rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*f|rm[[:space:]]+-[a-zA-Z]*f[a-zA-Z]*r ]]; then
DANGEROUS=1 # Catches -rf, -fr, -Rf, -Rfv, etc.
fi
if [[ "$CMD" =~ rm[[:space:]]+-r[[:space:]] ]]; then
DANGEROUS=1 # Catches bare "rm -r <path>"
fi
# ββ Path extraction & safe-prefix check βββββββββββββββββββββββββββββββββββ
if [[ "$DANGEROUS" -eq 1 ]]; then
# Pull the first non-flag token after rm as a rough target path heuristic.
# Example: "rm -rf /packages/core/dist" β TARGET="/packages/core/dist"
TARGET="$(printf '%s' "$CMD" | \
grep -oP '(?<=rm\s{0,20})(-\S+\s+)*\K/\S+' | head -1 || true)"
# If no absolute path captured, treat as dangerous (deny by default).
if [[ -z "$TARGET" ]]; then
TARGET="<relative or unknown path>"
SAFE=0
elif [[ "$TARGET" == /tmp/* || "$TARGET" == "/tmp" ]]; then
SAFE=1 # /tmp targets are explicitly allowed
else
SAFE=0
fi
if [[ "$SAFE" -eq 0 ]]; then
log_blocked "$CMD" "$CWD" # Persist evidence before blocking
# Human-readable explanation written to stderr (Claude surfaces this).
cat >&2 <<EOF
βββ BLOCKED by Claude Code hook: block_dangerous_rm βββββββββββββββββββββββ
β Command : ${CMD}
β Target : ${TARGET}
β Reason : "rm -rf / rm -r" outside /tmp is prohibited in this repo.
β Two CI incidents this quarter were caused by accidental
β deletions inside /packages. This hook prevents a third.
β Logged to: ${LOG_FILE}
β Safe alt : Use "trash" CLI, move files to /tmp first, or scope to
β a specific sub-path you have confirmed is safe.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
EOF
exit 1 # Exit 1 = block (Claude Code convention)
fi
fi
exit 0 # Exit 0 = allow
---
## 2. SETTINGS JSON SNIPPET
# Integration steps:
# 1. Copy block_dangerous_rm.sh to {PROJECT_ROOT}/.claude/hooks/
# 2. chmod +x {PROJECT_ROOT}/.claude/hooks/block_dangerous_rm.sh
# 3. Merge the JSON below into ~/.claude/settings.json under "hooks".
# If "hooks" does not exist yet, add the entire "hooks" key.
# 4. Replace {PROJECT_ROOT} with the absolute path to your monorepo root.
# 5. Validate JSON: python3 -m json.tool ~/.claude/settings.json
# 6. Open a new Claude Code session; hooks reload on session start.
#
# Timeout note: 10 s is generous for a pure string-check script (~5 ms
# typical). Raise to 30 s only if you later add network policy lookups.
{
"hooks": {
"PreToolUse": [
{
"description": "Block dangerous rm -rf calls outside /tmp",
"matcher": {
"tool_name": "Bash"
},
"command": "{PROJECT_ROOT}/.claude/hooks/block_dangerous_rm.sh",
"timeout": 10,
"enabled": true
}
]
}
}
---
## 3. TEST CASES
Input (tool_input.command) | Exit | Reason
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
rm -rf /packages/core/dist | 1 | BLOCK β absolute path outside /tmp;
| | matches -rf pattern, SAFE=0
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
rm -r / --no-preserve-root | 1 | BLOCK β targets filesystem root;
| | matches "rm -r /" pattern, SAFE=0
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
rm -rf /tmp/build_cache_abc123 | 0 | ALLOW β path starts with /tmp,
| | SAFE=1, no block triggered
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
rm -f ./packages/core/dist/index.js | 0 | ALLOW β single file, no -r flag;
| | DANGEROUS remains 0, exits 0
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
git clean -fdx | 0 | ALLOW β Bash tool but no rm pattern;
| | exits 0 immediately after guard
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
rm -Rfv /packages/shared/node_modules | 1 | BLOCK β -Rfv contains both r and f
| | flags; TARGET outside /tmp
---
## 4. EDGE CASES & KNOWN LIMITATIONS
**EC-1 β Shell variable expansion not evaluated**
CMD="TARGET=/packages/core; rm -rf $TARGET"
The hook sees the literal string before the shell expands $TARGET.
A variable-expanded path could bypass the /tmp safe-prefix check.
Mitigation: Pair with a shell AST linter (shellcheck --severity=error)
as a PostToolUse hook, or run commands inside a chroot/container.
**EC-2 β Subshell and pipe chaining**
CMD="echo ok && rm -rf /packages/utils/dist || true"
The regex matches this correctly only because "rm -rf /packages" is
present literally. But a heavily obfuscated chain:
CMD="eval 'rm -rf /packages'"
would NOT be caught because "rm" does not appear adjacent to "-rf".
Mitigation: Block all eval/exec calls containing single-quoted rm via
a second hook matcher on the eval pattern.
**EC-3 β Relative paths are blocked conservatively but imprecisely**
CMD="rm -rf dist" (run from /packages/core)
No absolute path is extracted; TARGET becomes "<relative or unknown path>"
and SAFE=0 β blocked. This is intentionally conservative but will
frustrate developers cleaning local build artifacts.
Mitigation: Add an allowlist of known-safe relative paths
(e.g., "dist", "node_modules", ".cache") and resolve them against CWD
before applying the safe-prefix check.
**EC-4 β Hook bypass via Python/Node sub-process**
CMD="node -e \"require('fs').rmSync('/packages', {recursive:true})\""
The hook only inspects Bash tool calls for the rm pattern.
Language-level recursive-delete APIs are invisible to this hook.
Mitigation: Add a PreToolUse hook for the Bash tool that also scans
for fs.rmSync, shutil.rmtree, and similar patterns; or enforce
filesystem-level write ACLs via OS-level controls (e.g., Linux
immutable flags on /packages during dev sessions).
**Suggested complementary safeguards**
- chattr +i on /packages in CI pre-step (Linux only); remove after build.
- A git pre-commit hook that rejects commits deleting tracked files in bulk.
- Docker volume mount /packages as :ro in non-build containers.
- PostToolUse hook that diffs the git working tree and alerts on
unexpected mass deletions even if the PreToolUse hook was bypassed.
---
## 5. UPGRADE PATH
**Evolve to a Policy-as-Code enforcement layer with Open Policy Agent (OPA)**
Replace the ad-hoc regex logic with a declarative Rego policy evaluated
by a locally running OPA sidecar:
# policy/bash_safety.rego
package claude.bash
default allow = false
allow {
not dangerous_rm
}
dangerous_rm {
re_match(`rm\s+-[a-zA-Z]*r[a-zA-Z]*f`, input.command)
not safe_target
}
safe_target {
startswith(input.target, "/tmp")
}
The hook script shrinks to a thin shim:
#!/usr/bin/env bash
set -euo pipefail
INPUT="$(cat -)"
RESULT="$(printf '%s' "$INPUT" | \
curl -sf http://localhost:8181/v1/data/claude/bash/allow \
-d @- -H 'Content-Type: application/json')"
[[ "$(printf '%s' "$RESULT" | jq -r '.result')" == "true" ]] \
&& exit 0 || exit 1
Benefits of this upgrade:
1. Policies are version-controlled, peer-reviewed Rego files β the same
workflow used for infra and k8s policy.
2. A single OPA bundle can enforce rules across Claude Code, pre-commit,
and CI (GitHub Actions OIDC β OPA policy check) from one source of truth.
3. New rules (block shutil.rmtree, block aws s3 rm --recursive, etc.)
are added as Rego clauses with zero changes to the hook script.
4. OPA's decision log provides tamper-evident audit records superior to
the append-only log file used in the current implementation.
5. The sidecar can be started as a launchd/systemd user service so it
survives session restarts with no manual intervention.
By purchasing this prompt, you agree to our terms of service
CLAUDE-4-6-SONNET
πGenerates production-ready Claude Code hook scripts that enforce dev rules deterministically β no more hoping the model remembers.
π‘οΈ Block dangerous commands automatically
π Inject context on keyword detection
β
Run linters on every file edit
π Auto-commit with formatted messages
π Trigger security review on auth keywords
...more
Added 2 days ago
