What it is and why it matters
The video was about Jev: an AI that doesn't write text, but answers with a precise type, meaning a choice, a score or a true/false, with the probabilities and how confident it is. What to do with the answer is up to your code. For now there's a waitlist. Here I care about one idea only: a check that sits between the AI and the command, before the command runs. Your Claude Code can have that today, without waiting on any list.
The problem is simple. Claude Code runs commands in your computer's terminal. Almost always they're harmless: it installs a package, runs the tests, reads a file. But some can't be undone. rm -rf deletes a folder with everything in it, without going through the trash. git push --force overwrites the project's history online. git reset --hard throws away the changes you haven't saved in a commit yet. A drop table deletes a database table.
Claude doesn't have to get it wrong on purpose. A misunderstood instruction, a wrong path, a long session where it got confused is enough. And if you approve the confirmations quickly, or cut them down to go faster, the moment when you could have said no goes by without you noticing.
There's one more thing to know. Claude Code's rewind (Esc twice, or /rewind) undoes the changes made with its file tools, but not the ones made by commands in the terminal. If an rm deletes a file, rewind won't bring it back.
So the brake has to go in first. Two ways: a list of banned commands, the simple one, and a small script that reads every command before it runs, the smarter one. Then there's the prompt to have Claude do them.
1. The list of banned commands
Claude Code reads the project settings from the .claude/settings.json file, in the project folder. If you want the same rules in every project, use ~/.claude/settings.json in your user folder. Put this inside:
{
"permissions": {
"deny": [
"Bash(rm -rf *)",
"Bash(git push --force *)",
"Bash(git reset --hard *)"
]
}
}A “deny” always wins over any permission you've given. Claude Code checks the deny rules first, then the ones that ask for confirmation, then the ones that allow. So even if one day you answered “yes, don't ask again” to a command, the deny stays stronger. And it applies in every permission mode, even bypassPermissions.
The limit: the rule blocks the command written that way, not its variants. Bash(rm -rf *) stops rm -rf build/, but not the same command inside bash -c, or written as /bin/rm -rf build/. rm -fr, with the letters swapped, gets through too. It's a net, not a wall.
2. The guard: a hook that checks every command
A hook is a script that Claude Code runs on its own at a precise moment. The one we need is called PreToolUse: it runs before every tool call, and with the “Bash” filter it only looks at terminal commands. It receives the command as JSON. If the script exits with code 2, the command doesn't run and Claude reads the reason you wrote. If it exits with 0, the command goes on through the normal permissions.
The advantage over the list: the script looks for the dangerous words in the whole text of the command. So it also catches rm -rf hidden inside bash -c.
Create the file .claude/hooks/guard.sh in the project and paste this in:
#!/bin/bash
command -v jq >/dev/null || { echo "jq required" >&2; exit 2; }
CMD=$(jq -r '.tool_input.command')
if echo "$CMD" | grep -Eiq 'rm -rf|git push --force|git reset --hard|drop table'; then
echo "Blocked: destructive command. Ask me for permission first." >&2
exit 2
fi
exit 0Make it executable with chmod +x .claude/hooks/guard.sh. The script uses jq to read the JSON: if you don't have it, on a Mac you install it with brew install jq. If it's missing, the first line of the script blocks every command instead of letting it through.
Then add this part to .claude/settings.json, next to “permissions”:
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard.sh" }]
}
]
}$CLAUDE_PROJECT_DIR points to the project folder, and the quotes around it keep the path in one piece even if it contains spaces. Put together, the two parts give you a file like this:
{
"permissions": {
"deny": [
"Bash(rm -rf *)",
"Bash(git push --force *)",
"Bash(git reset --hard *)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{ "type": "command", "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard.sh" }]
}
]
}
}You extend the list in the script yourself: add the commands that do damage in your project, for example the one that wipes the database or the script that publishes the app.
3. If you don't feel like doing it by hand
You can ask Claude to do all of it: the script, the settings file and the test. Write to it:
Set up in .claude/settings.json a PreToolUse hook that blocks destructive commands before they run (rm -rf, git push --force, git reset --hard, drop table) and add the same rules to the permissions deny list. Then create an empty test folder and check that rm -rf on it gets blocked.
When it's done, read the .claude/settings.json file and the script yourself. They're only a few lines. If something doesn't add up, have it explained to you:
Show me the contents of .claude/settings.json and of the hook script, and explain line by line what it blocks and what it lets through.
4. The test
A rule you haven't tested is a rule you hope works. Type /hooks in Claude Code: it opens the list of configured hooks, and yours has to show up under PreToolUse. Claude Code usually picks up changes to the settings file right away. If your hook doesn't show up, close and reopen Claude Code.
Watch out for one detail: with the deny in place, a plain rm -rf is already stopped by the list. To see whether the guard works on its own, ask Claude for the same command inside bash -c: the list doesn't see it, the guard does.
Create an empty folder called guard-test and try to delete it with bash -c 'rm -rf guard-test'. Then tell me what stopped you: the deny list or the hook.
If the command gets through, the guard isn't working: usually the script isn't executable or the path in settings.json is wrong, and in those cases Claude Code doesn't get exit code 2 and lets the command go ahead. If instead everything is blocked, even a plain ls, and the reason is “jq required”, jq is missing.
5. The net underneath: git
The guard lowers the risk, it doesn't remove it. A command has many variants and you won't foresee them all. That's why the real net underneath is git: make a commit before you give Claude a long job, and keep a copy of the project online too, for example on GitHub. If something goes wrong, you go back to the last commit.
And bypassPermissions mode, the one that skips the confirmation prompts, is something the Claude Code docs recommend only in isolated environments, like a container or a virtual machine. Not on the computer where you keep everything.
Common mistakes
- Overwriting the settings.json that was already there: add the new parts to what's in it and check the commas, because one missing comma is enough to break the file.
- Thinking the deny covers every variant: it blocks the command written that way, so pair it with the guard that reads the whole text of the command.
- Forgetting chmod +x: the script doesn't run and the guard blocks nothing without telling you, so always run the test on an empty folder.
- Counting on rewind for deleted files: rewind doesn't undo terminal commands, so make a commit before long jobs.
Checklist
- Deny rules in .claude/settings.json
- Guard script created and made executable
- jq installed
- Hook visible in /hooks under PreToolUse
- Test on an empty folder: command blocked
- Commit made before long jobs
Frequently asked questions
Doesn't Claude Code already ask permission before commands?
In Manual mode, yes: it asks before every command that isn't read-only. But if you answer “yes, don't ask again”, or work in a mode that asks less, that confirmation goes away. The deny and the guard, on the other hand, stay active there too.
Which is better, the deny list or the hook?
Both. The deny is three lines and needs nothing else. The hook reads the whole text of the command and catches the most common variants. Each one covers the other's gaps.
Can the guard also block harmless commands?
Yes: if a command contains those words, for example a text search with “rm -rf” in it, it gets stopped. That's fine: Claude reads the reason and asks you how to proceed. If it happens often, make the list more precise.
Does it apply to all my projects?
In .claude/settings.json it applies to that project, and if the file is in git it also applies to whoever works on it with you. In ~/.claude/settings.json it applies to all your projects: in that case put the script in ~/.claude/hooks and write that path in the hook's command.