{"slug": "how-i-stop-claude-code-from-running-destructive-shell-commands", "title": "How I stop Claude Code from running destructive shell commands", "summary": "A developer published a consultation kit showing how to use a Claude Code PreToolUse hook with a Bash matcher to run a Python guard script that inspects proposed shell commands and blocks recursive deletions and force pushes to main/master. The script parses commands with shlex and returns exit code 2 with a reason on stderr to block the tool call, but the author stresses it is a text filter that cannot prove a command safe and must be combined with Claude Code permissions, restricted access, and GitHub branch protections. The blocking semantics were verified against a working hook in a real project, though running this exact script inside Claude Code remains pending.", "body_md": "I want Claude Code to help me make small, reviewable changes. Recursive deletion and rewriting shared Git history deserve a separate decision. To catch some obvious cases before execution, I use a small Python command filter as an example in my consultation kit.\n\nThe idea is straightforward: inspect the proposed Bash command, look for selected patterns, and return a blocking response when one matches. The implementation is short enough to read alongside its tests.\n\nThere is an important limit behind the title. This is a text filter. It cannot establish that an arbitrary shell command is safe. I combine it with Claude Code permissions, restricted access, and GitHub branch protections.\n\nThe configuration example uses a `PreToolUse` hook with this matcher:\n\n```\n\"matcher\": \"Bash\"\n```\n\nThat scope matters. The script understands the command field of a Bash tool event. Matching every tool would suggest coverage the implementation does not provide. If another tool name reaches the script, it returns successfully without inspecting that tool's input.\n\nInside the command hook entry, the example launches Python with:\n\n```\n\"command\": \"python3 \\\"$CLAUDE_PROJECT_DIR\\\"/.claude/hooks/guard.py\"\n```\n\nI keep this in an example settings file until someone deliberately merges it into `.claude/settings.json`. Existing settings should be preserved. On Windows, the available Python launcher and the hook shell need checking. The kit suggests `py -3` as one possible replacement and calls out environment variable expansion for verification.\n\nThe script reads a JSON event from standard input and extracts `tool_input.command`. It never executes that string.\n\nThe filtering function starts with Python's `shlex`:\n\n```\nlexer = shlex.shlex(command, posix=True, punctuation_chars=True)\nlexer.whitespace_split = True\ntokens = list(lexer)\n```\n\nIt walks those tokens, checks command names, and collects following arguments until it encounters a recognized separator. Using the basename lets the examples recognize both `rm` and `/bin/rm`.\n\nThis gives me a compact way to demonstrate command inspection. It does not reproduce Bash execution semantics. Quoting, nested expressions, scripts, and shell state make the real problem much larger than token matching.\n\nWhen the filter finds a rejection reason, the entry point responds with:\n\n```\nif reason:\n    print(reason, file=sys.stderr)\n    return 2\n```\n\nThe kit documents that exit code 2 blocks the tool call and returns stderr to Claude. Those fields and blocking semantics were checked against a working hook in a real Claude Code project. That check is separate from running this exact script inside Claude Code, which remains pending.\n\nFor deletion, the example looks for recursive `rm` options. Its tests cover `-rf`, `-R`, `--recursive`, and a command following `&&`. The message asks for a separate human review.\n\nThe filter allows a simple single-file deletion example. That does not mean every single-file deletion is appropriate. It means this particular policy targets recursive deletion rather than deciding whether every filesystem operation is authorized.\n\nFor Git, the policy is deliberately conservative about force pushes. It rejects selected force options on every branch, including `--force-with-lease`, and recognizes the leading-plus refspec example and `--mirror`. I do not describe those as equivalent Git operations. They are operations this example chooses to block.\n\nThe protected names are explicit:\n\n```\nPROTECTED = {'main', 'master', 'refs/heads/main', 'refs/heads/master'}\n```\n\nThe implementation also checks selected ways to delete, recreate, or directly change those references. The test cases include force-moving a branch, recreating it with checkout, updating its reference, and deleting a remote branch.\n\nA repository with another default branch name needs adaptation. The script does not discover that name or inspect the current branch.\n\nMy test suite launches the Python guard as a subprocess. It sends a JSON event through standard input and inspects the return code and, where asserted, stderr. The potentially destructive command remains a string.\n\nThe suite has seven test methods covering recursive deletion, force pushes, protected branch changes, ordinary commands, other tool names, malformed Bash events, and an unparseable command. Some methods contain several cases.\n\nThe kit records all seven passing on Linux and Windows. I keep that claim narrow: selected inputs produced the expected responses. It does not demonstrate complete shell coverage or validate every configuration detail in a customer's environment.\n\nTo run those harmless checks from the kit directory:\n\n```\npython3 -m unittest discover -s tests -v\n```\n\nI would verify hook activation separately in a disposable repository before relying on it. A passing Python test and an active Claude Code hook answer different questions.\n\nThe filter does not inspect the contents of scripts, aliases, interpreter programs, or alternate tools. It does not reliably understand nested commands. Equivalent destructive operations can escape its checks.\n\nThere is also a particularly relevant Git limitation: reset or rebase can change the current main branch without spelling its name in the command. This example does not detect that situation. Its protected-name checks cannot substitute for knowing repository state.\n\nFalse positives are possible too. Harmless quoted text can resemble a command or an option the filter rejects. Token matching loses information about how the shell will interpret the original text.\n\nI explain these limits before presenting the hook as useful. Its value is catching selected obvious patterns and making a rejected operation visible for review. Passing the filter is never proof of safety.\n\nI pair the hook with Claude Code permissions and restricted filesystem and credential access. In GitHub, I use branch protections, required reviews, and checks to control shared changes. Deployment environment controls cover another set of decisions.\n\nThe repository instructions support that workflow: use a feature branch, inspect the diff, run relevant checks, and explain unresolved limitations in the pull request. Instructions describe expectations. Permissions and repository settings enforce the boundaries they actually control.\n\nIf your team wants help adapting this workflow, I offer paid consultations in English. We can work through repository instructions, tool permissions, a small reviewed change, and the path to staging. I use this hook as a readable starting point, with its assumptions and gaps visible throughout the discussion.", "url": "https://wpnews.pro/news/how-i-stop-claude-code-from-running-destructive-shell-commands", "canonical_source": "https://dev.to/martinpisacreta/how-i-stop-claude-code-from-running-destructive-shell-commands-5c98", "published_at": "2026-10-29 13:14:07+00:00", "updated_at": "2026-10-29 13:18:01.400321+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-safety"], "entities": ["Claude Code", "Anthropic", "GitHub", "Python"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/how-i-stop-claude-code-from-running-destructive-shell-commands", "markdown": "https://wpnews.pro/news/how-i-stop-claude-code-from-running-destructive-shell-commands.md", "text": "https://wpnews.pro/news/how-i-stop-claude-code-from-running-destructive-shell-commands.txt", "jsonld": "https://wpnews.pro/news/how-i-stop-claude-code-from-running-destructive-shell-commands.jsonld"}}