Guide

Automating Claude Code safely: pre-approval policies that actually work

Automating Claude Code safely: pre-approval policies that actually work

You want Claude Code to run autonomously — hand it a task, walk away, come back to a PR or a commit. But "autonomous" and "safe" feel like they're in tension. Let the agent run unchecked and it might blast rm -rf across your entire codebase. Require approval for every action and you're babysitting it again.

The middle ground is pre-approval policies. You define rules up front — "allow git status everywhere," "block rm outside this repo," "ask me before git push" — and Claude Code checks each tool call against those rules before running it. No decisions mid-task, no prompts you'll miss. The agent runs with the freedom you've pre-granted.

Claude Code wants to run Policy Rules allow: git status, ls block: rm, git push Allow Block Example policy: "git status allowed everywhere, rm blocked outside /repo" Tool call received → evaluate against rules → allow or block
When the agent wants to run a tool, your PreToolUse policy evaluates it against rules. Allowed tools run immediately; blocked tools are vetoed; ambiguous ones can escalate to you.

How pre-approval policies work

Policies live in Claude Code settings via the PreToolUse hook. When the agent is about to run a tool — a shell command, a file edit, a fetch — Claude Code fires the hook with the command and its context. Your hook script reads the JSON, evaluates it against your rules, and decides: allow, block, or escalate it back to you for approval.

The beauty is that this happens instantly and silently. No toast, no pause, no "allow this?" prompt unless the policy says "I don't know." The agent runs what you've already blessed.

A minimal policy is a shell script that reads the tool name and working directory from the JSON payload and pattern-matches against a whitelist or blacklist:

#!/usr/bin/env bash
payload="$(cat)"
tool="$(echo "$payload" | jq -r '.tool_name')"
command="$(echo "$payload" | jq -r '.command // .content')"

# Block all rm commands
if [[ "$tool" == "bash" && "$command" =~ rm ]]; then
  exit 1
fi

# Allow everything else
exit 0

That's a deny-by-exception pattern: block the dangerous stuff explicitly, allow everything else. You can flip it (allow-by-exception: block everything by default, allow only safe commands), but for most teams, deny-by-exception is easier to maintain and less likely to grind the agent to a halt.

The safest policies to start with

You don't need to be exhaustive. A few rules catch most of the risky patterns. Start here and add as your team learns what matters:

Building your policy script

A real policy for a team is usually 50–100 lines of bash (or Python, or whatever). Here's a skeleton that combines a few of the patterns above:

#!/usr/bin/env bash
payload="$(cat)"
tool="$(echo "$payload" | jq -r '.tool_name')"
command="$(echo "$payload" | jq -r '.command // .content // empty')"
cwd="$(echo "$payload" | jq -r '.working_directory')"

# Blocklist: deny these unconditionally
if [[ "$tool" == "bash" || "$tool" == "shell" ]]; then
  if [[ "$command" =~ rm\ -rf|\.ssh|\.aws ]]; then
    exit 1
  fi
fi

# Escalation list: ask before running
if [[ "$command" =~ git\ push|npm\ publish ]]; then
  exit 2
fi

# Whitelist: allow read-only operations without hesitation
if [[ "$command" =~ ^(git\ status|git\ log|ls|cat|grep) ]]; then
  exit 0
fi

# Default: allow, with optional logging
echo "Allowing: $command in $cwd" >> /tmp/claude-policy.log
exit 0

Save that as a script, make it executable, and wire it into your settings under the PreToolUse hook. The logic is dead simple: scan the command, compare it to your rules, return a decision.

How to hook it into Claude Code

Add a hooks section to your Claude Code settings (if it doesn't exist) and register your policy under PreToolUse:

{
  "hooks": {
    "PreToolUse": "~/.config/claude-code/policy.sh"
  }
}

That's it. From now on, every tool call goes through your policy before running. For more on hooks in general — how to parse the JSON, the events available, and decision semantics — see our guide to Claude Code hooks.

Policy patterns for different team scenarios

The policy above is a good general start, but different teams need different defaults. Here are the patterns that work:

When escalation saves you

The "escalate" outcome is underrated. It's the "I don't know, ask the human" response. The agent pauses, a notification fires (via the Notification hook), and you get prompted. This is perfect for the gray zone — operations that are usually safe but have enough risk that the decision should be conscious.

git push is the classic example. You don't want to block it (the agent earned the right to ship), but you also don't want it running silently at 2 a.m. Escalation is the sweet spot: the agent does the work, then pings you with "ready to push, allow?" and you're back in the loop for the moment that matters.

Iterating on your policies

You'll refine these policies as you learn what breaks things. A good workflow is:

The policy script above logs to /tmp/claude-policy.log. Tail that while you work, and in a week you'll have a clear picture of what your agent actually needs and what you can keep locked down.

Where Backgrind fits

Policies handle the decisions you can make ahead of time. Backgrind handles the ones you can't: when a tool call escalates — or the agent just needs a yes/no — Backgrind catches that PreToolUse / Notification event and surfaces it as an ambient prompt right in its always-on-top overlay. You approve or deny on the spot, instead of hunting for a buried terminal. The agent runs with the freedom your policy pre-granted, and the moments that genuinely need you come to you.

Pair that with running Claude Code in the background and you get the full picture: autonomous work behind a pre-approval policy, a chime only when something needs a decision, and no babysitting. See it in the live demo.