Safe Claude Code settings: permissions and a guard hook

Project settings that stop Claude Code from force-pushing, wiping work, reading secrets, or piping downloads into a shell, while leaving everyday commands alone. Two files, copy and commit.

What is in this template

File What it does
.claude/settings.json Permission rules: read-only git runs without asking; pushes, commits, publishing, and infrastructure tools ask first; secrets, force-pushes, hard resets, rm -rf, and sudo are denied. Also turns off bypass mode for this project.
.claude/hooks/guard.sh A PreToolUse hook that checks every shell command before it runs, including chained ones such as npm test && git push --force. It blocks with exit code 2, and Claude sees the reason.

Why both

Permission rules match the start of a command, and Claude Code's own docs warn that argument patterns are fragile. A Read(.env) deny rule also does not stop cat .env in the shell. The hook covers those gaps by reading the whole command line. Hooks cannot loosen the rules: a deny rule still wins even if the hook allows the call (docs). Each layer only adds limits.

What the hook blocks:

Install

From the root of your repo:

mkdir -p .claude/hooks
curl -fsSL https://raw.githubusercontent.com/RyanAlberts/best-of-Agent-Harnesses/main/templates/claude-code-safe-settings/.claude/settings.json -o .claude/settings.json
curl -fsSL https://raw.githubusercontent.com/RyanAlberts/best-of-Agent-Harnesses/main/templates/claude-code-safe-settings/.claude/hooks/guard.sh -o .claude/hooks/guard.sh
chmod +x .claude/hooks/guard.sh

If you already have a .claude/settings.json, merge the permissions and hooks blocks by hand instead of overwriting it. The hook needs jq or python3.

Open Claude Code in the repo once and accept the trust prompt. Until you do, Claude Code ignores the project's allow rules.

Check that it works

Test the hook without starting a session:

echo '{"tool_name":"Bash","tool_input":{"command":"npm test && git push --force"}}' | .claude/hooks/guard.sh; echo "exit $?"

You should see a Blocked by .claude/hooks/guard.sh message and exit 2. Then, in a session, run /permissions to see the rules Claude Code loaded and /hooks to see the hook.

Adjust it

Limits

Pattern checks catch mistakes and obvious prompt injection, not a determined attacker: a script file the agent writes and then runs is not inspected. For real isolation, run the agent in a container or turn on Claude Code's sandbox; the sandboxing guide compares options.

Go further

.claude/hooks/guard.sh

Raw file

#!/usr/bin/env bash
# PreToolUse hook for Claude Code: blocks dangerous shell commands anywhere in
# the command line, including chained ones (`ls && git push --force`), which
# permission rules can miss. Exit 2 blocks the call and shows stderr to Claude.
# Needs jq or python3 to read the hook's JSON input.

input="$(cat)"
if command -v jq >/dev/null 2>&1; then
  cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // ""')"
else
  cmd="$(printf '%s' "$input" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))')"
fi

block() {
  echo "Blocked by .claude/hooks/guard.sh: $1. Ask the user to run it themselves if it is really needed." >&2
  exit 2
}

# Destructive git
printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+push[^;&|]*(--force|[[:space:]]-f([[:space:]]|$)|--mirror|--delete|[[:space:]]:[^[:space:]])' && block "force-push or remote delete"
printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+reset[[:space:]]+--hard' && block "git reset --hard discards work"
printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+clean[[:space:]]+-[a-zA-Z]*f' && block "git clean deletes untracked files"
printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+(checkout|restore)[[:space:]]+(--[[:space:]]+)?\.([[:space:]]|$)' && block "discarding all local changes"
printf '%s' "$cmd" | grep -Eq 'git[[:space:]]+branch[[:space:]]+-D' && block "force-deleting a branch"

# Destructive files and privilege
printf '%s' "$cmd" | grep -Eq 'rm[[:space:]]+(-[a-zA-Z]*[rR][a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*[rR]|--recursive[[:space:]]+--force|--force[[:space:]]+--recursive)' && block "recursive forced delete"
printf '%s' "$cmd" | grep -Eq '(^|[;&|[:space:]])sudo[[:space:]]' && block "sudo"

# Secrets: reading them through the shell gets around Read() deny rules.
# Example files (.env.example, .env.sample, .env.template) are not secrets.
scan="$(printf '%s' "$cmd" | sed -E 's/\.env\.(example|sample|template)//g')"
printf '%s' "$scan" | grep -Eq '(cat|less|more|head|tail|grep|awk|sed|scp|base64|xxd)[^;&|]*(\.env([.[:space:]]|$)|\.pem|id_rsa|id_ed25519|\.aws/credentials|\.ssh/)' && block "reading secrets through the shell"
printf '%s' "$cmd" | grep -Eq '(^|[;&|[:space:]])(printenv|env)([[:space:]]*$|[[:space:]]*[;&|])' && block "dumping environment variables"

# Remote code
printf '%s' "$cmd" | grep -Eq '(curl|wget)[^;&]*\|[[:space:]]*(sudo[[:space:]]+)?(ba|z)?sh' && block "piping a download into a shell"

exit 0

.claude/settings.json

Raw file

{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "permissions": {
    "allow": [
      "Bash(git status *)",
      "Bash(git diff *)",
      "Bash(git log *)",
      "Bash(git show *)"
    ],
    "ask": [
      "Bash(git push *)",
      "Bash(git commit *)",
      "Bash(npm publish *)",
      "Bash(docker *)",
      "Bash(kubectl *)",
      "Bash(terraform *)"
    ],
    "deny": [
      "Read(.env)",
      "Read(.env.*)",
      "Read(**/*.pem)",
      "Read(**/*.key)",
      "Read(**/id_rsa*)",
      "Read(**/id_ed25519*)",
      "Read(./secrets/**)",
      "Read(~/.ssh/**)",
      "Read(~/.aws/**)",
      "Edit(.env)",
      "Edit(.env.*)",
      "Bash(git push --force *)",
      "Bash(git push -f *)",
      "Bash(git reset --hard *)",
      "Bash(git clean *)",
      "Bash(rm -rf *)",
      "Bash(sudo *)"
    ],
    "disableBypassPermissionsMode": "disable"
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/guard.sh"
          }
        ]
      }
    ]
  }
}