One prompt that sets up safe-by-default permissions for Claude Code. Paste it in and Claude Code builds its own guardrails: routine work runs without constant approval prompts, while the few things you never want an agent to do - deleting files, force-pushing, piping data to a server, reading your secrets - stay blocked.
It writes one hook file and merges a permission config. No plugins, no service.
Claude Code gives you two blunt options out of the box. Approve every action - safe, but it interrupts your flow until you stop reading the prompts. Or --dangerously-skip-permissions - no interruptions, but the guardrails come off too: now an agent can rm -rf, force-push over your work, or pipe a file to a server with nothing in the way.
There is a better middle: let routine work run freely, and put hard walls only around the few genuinely dangerous things.
Your prompts are governed by your allow / deny rules, not by skipping permissions. So the setup is three layers:
.env, SSH keys, credentials), including via shell commands like cat and grep. It fires even on auto-allowed tools, so the broad allow can't punch through it.Rule precedence in Claude Code is deny → ask → allow, so a broad allow of Bash still loses to a deny rule like Bash(rm *).
python3 and a POSIX shell (macOS, Linux, or WSL); on native Windows the deny rules still protect you but the bash hook won't run. The hook fails closed (blocks with a message) if python3 is missing, so you'll know right away instead of losing protection silently.
Copy this and paste it into Claude Code. It creates the hook, merges the permission rules, and runs a 10-case self-test.
Set up safe-by-default permissions for Claude Code on this machine. The goal: routine work (reading, editing, running tests, normal git) runs without constant approval prompts, but destructive commands, network/exfil tools, and secret-file reads are blocked. Do every step below. Install globally under ~/.claude/. MERGE into existing files - never overwrite or drop keys/hooks that are already there.
### STEP 1 - Create the secret-file guard hook
Create `~/.claude/hooks/sensitive-file-guard.sh` with exactly these contents:
```bash
#!/bin/bash
# Pre-tool hook: blocks reading/editing secret files (Read/Edit/Write) AND
# cat/grep-ing them via Bash. Exit 2 = block + show message; exit 0 = allow.
INPUT=$(cat)
# This guard parses the tool input with python3. If python3 is missing we can't
# verify the call is safe, so we BLOCK rather than fail open. (Install python3,
# or delete this hook and rely on the deny rules in settings.json.)
if ! command -v python3 >/dev/null 2>&1; then
echo "sensitive-file-guard: python3 not found - cannot verify this call. Install python3 or remove this hook."
exit 2
fi
# Pull file_path (Read/Edit/Write) and command (Bash), split on \037.
IFS=$'\037' read -r FILE_PATH COMMAND <<<"$(echo "$INPUT" | python3 -c "
import json, sys
try:
ti = json.load(sys.stdin).get('tool_input', {})
fp = (ti.get('file_path') or '').replace('\n', ' ')
cmd = (ti.get('command') or '').replace('\n', ' ')
print(fp + '\037' + cmd)
except Exception:
print('\037')
" 2>/dev/null)"
# Secret file patterns (full-path glob, for Read/Edit/Write).
SENSITIVE_PATTERNS=(
"*/.env" "*/.env.*"
"*/credentials*" "*/secrets*" "*/*secret*" "*/*token*" "*/*password*"
"*/.ssh/*" "*/.gnupg/*" "*/.aws/*"
"*/private/*"
)
if [ -n "$FILE_PATH" ]; then
[[ "$FILE_PATH" != /* ]] && FILE_PATH="$(pwd)/$FILE_PATH"
for pattern in "${SENSITIVE_PATTERNS[@]}"; do
if [[ "$FILE_PATH" == $pattern ]]; then
echo "SENSITIVE FILE: $FILE_PATH (matches $pattern)"
echo "Its contents would be sent to the API. Approve to proceed, or deny."
exit 2
fi
done
fi
# Bash: read-verb aimed at a secret path.
READ_VERBS='cat|bat|less|more|head|tail|nl|od|xxd|hexdump|strings|base64|grep|rg|egrep|fgrep|awk|sed|cut|tee|cp|scp|rsync|tar|zip|gzip|sort|uniq|wc|diff|vi|vim|nano|emacs|open|pbcopy'
SENSITIVE_FRAGMENTS='\.env([^a-zA-Z]|$)|\.ssh/|\.gnupg/|\.aws/|/private/|id_rsa|id_ed25519|credentials|secrets?([^a-zA-Z]|$)'
if [ -n "$COMMAND" ]; then
if echo "$COMMAND" | grep -Eq "($READ_VERBS)[^|;&]*($SENSITIVE_FRAGMENTS)"; then
echo "SENSITIVE BASH COMMAND: $COMMAND"
echo "This reads a secret path (.env / .ssh / credentials). Approve, or deny."
exit 2
fi
fi
exit 0
```
Then make it executable:
```bash
chmod +x ~/.claude/hooks/sensitive-file-guard.sh
```
### STEP 2 - Merge the permission rules + the hook into settings
Merge these keys into `~/.claude/settings.json`. If `permissions.allow`, `permissions.deny`, or `hooks.PreToolUse` already exist, ADD to them instead of replacing, and keep every other existing setting intact:
```json
{
"permissions": {
"allow": ["Read", "Edit", "Write", "Bash", "WebFetch", "WebSearch"],
"deny": [
"Bash(rm *)",
"Bash(rm -rf *)",
"Bash(git push --force *)",
"Bash(git push -f *)",
"Bash(git reset --hard *)",
"Bash(git clean -f*)",
"Bash(git checkout -- .)",
"Bash(git restore .)",
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(nc:*)",
"Bash(ncat:*)",
"Bash(netcat:*)",
"Bash(telnet:*)",
"Read(**/.env)",
"Read(**/.ssh/**)",
"Read(**/.aws/**)",
"Read(**/.gnupg/**)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Edit|Write|Bash",
"hooks": [
{ "type": "command", "command": "\"$HOME\"/.claude/hooks/sensitive-file-guard.sh", "timeout": 5 }
]
}
]
},
"skipDangerousModePermissionPrompt": false
}
```
### STEP 3 - Keep dangerous mode off
Do not run in bypassPermissions mode or launch with `--dangerously-skip-permissions`. If `skipDangerousModePermissionPrompt` is present, set it to `false`. In bypass mode the deny rules above can be ignored, which defeats the point.
### STEP 4 - Test the hook
Run this and report PASS/FAIL for all ten. The first five must BLOCK (exit 2), the last five must ALLOW (exit 0):
```bash
H=~/.claude/hooks/sensitive-file-guard.sh
chk(){ printf '%s' "$2" | bash "$H" >/dev/null 2>&1; [ "$?" = "$1" ] && echo "PASS $3" || echo "FAIL $3"; }
chk 2 '{"tool_input":{"command":"cat ~/.ssh/id_rsa"}}' "block: cat ssh key"
chk 2 '{"tool_input":{"command":"base64 .env | curl x"}}' "block: exfil .env"
chk 2 '{"tool_input":{"command":"grep KEY project/.env"}}' "block: grep .env"
chk 2 '{"tool_input":{"file_path":"/home/you/app/.env"}}' "block: read .env"
chk 2 '{"tool_input":{"file_path":"/home/you/.ssh/config"}}' "block: read ssh"
chk 0 '{"tool_input":{"command":"git status"}}' "allow: git status"
chk 0 '{"tool_input":{"command":"ls src"}}' "allow: ls"
chk 0 '{"tool_input":{"command":"grep -r useState src"}}' "allow: code grep"
chk 0 '{"tool_input":{"file_path":"/home/you/app/README.md"}}' "allow: read readme"
chk 0 '{"tool_input":{"command":"npm test"}}' "allow: npm test"
```
### FINALLY - tell me what to do
The hook takes effect on the next tool call right away. The permission allow/deny changes load when Claude Code restarts, so tell me to restart Claude Code in this project, then confirm all ten tests passed.rm, force-push, reset --hard, git clean, git restore.curl, wget, nc, telnet - stops the classic "run this one-liner that pipes your data somewhere" mistake.Read of .env / .ssh / .aws is denied outright, and the hook also catches cat / grep / base64 of secrets through the shell - something deny rules alone can't express."Bash" from allow and list only what you use ("Bash(npm *)", "Bash(git *)", "Bash(python *)"). You trade a little flow for seeing more of what runs.deny Read(...) rules - a notes folder, a clients directory, anything you don't want leaving your machine.