The problem
When Claude Code runs a Bash command, it checks permissions before executing. You have two broad options:
-
Approve every AWS command manually — safe, but you'll be clicking "Allow" dozens of times a day, even for harmless
aws s3 lscalls on dev buckets. -
Add
Bash(aws *)to the allow list — zero friction, but now every production command runs without any confirmation gate.
Neither is great. You want Claude to move fast on dev and staging, but you need a hard stop before anything touches production.
The goal
Allow all AWS CLI commands by default. Force an explicit confirmation only when a command targets a specific production profile. No changes to the daily workflow — prod is the only thing that needs friction.
The solution
What is settings.json?
Claude Code reads a settings.json file (global at ~/.claude/settings.json, or per-project at .claude/settings.json) that controls permissions, hooks, environment variables, and more. The permissions section lets you define which tool calls are allowed, denied, or always require confirmation.
{
"permissions": {
"allow": [
"Bash(aws *)"
]
}
}
This blanket rule tells Claude: run any aws command without asking. That's the starting point — broad permission, then we layer in selective protection via a hook.
Official docs: Settings & Permissions
What is a hook?
Hooks are shell commands that Claude Code fires at specific lifecycle events. The most useful for this case is PreToolUse: it runs before Claude executes a tool call, and can influence what happens next.
Claude passes the tool input as JSON on stdin. Your hook reads it, inspects it, and can return a JSON response that tells Claude to allow, deny, or ask the user for confirmation.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(aws *)",
"command": "~/.claude/hooks/aws-prod-guard.sh"
}
]
}
]
}
}
Two filters work together here. The outer matcher: "Bash" selects the tool type. The inner "if": "Bash(aws *)" is a pre-filter that only invokes the script when the actual command starts with aws — so Claude doesn't spawn the shell script on every single Bash call (git, npm, make, etc.). Only AWS commands reach the guard.
Official docs: Hooks
The guard script
The script reads the command Claude is about to run, checks whether it targets a protected profile, and responds accordingly.
~/.claude/hooks/aws-prod-guard.sh
#!/bin/bash
# Intercepts Bash tool calls and asks for confirmation when an AWS command
# targets a production profile. Edit aws-prod-profiles.json to add/remove profiles.
PROFILES_FILE="$(dirname "$0")/aws-prod-profiles.json"
[ -f "$PROFILES_FILE" ] || exit 0
CMD=$(jq -r '.tool_input.command // ""')
while IFS= read -r entry; do
profile=$(echo "$entry" | jq -r '.profile')
account=$(echo "$entry" | jq -r '.account')
description=$(echo "$entry" | jq -r '.description')
if echo "$CMD" | grep -qE "(--profile[[:space:]=]+$profile|AWS_PROFILE[[:space:]]*=[[:space:]]*$profile|AWS_DEFAULT_PROFILE[[:space:]]*=[[:space:]]*$profile)"; then
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"Production profile detected: %s — %s (account %s)"}}\n' \
"$profile" "$description" "$account"
exit 0
fi
done < <(jq -c '.[]' "$PROFILES_FILE")
exit 0
The key detail: when a production profile is detected, the hook returns permissionDecision: "ask" — which tells Claude to pause and prompt the user for explicit approval, overriding the blanket allow rule from settings.json. If no protected profile is matched, the hook exits 0 with no output and Claude proceeds normally.
The profiles file
Protected profiles live in a separate JSON file, so you can add or remove entries without touching the script.
~/.claude/hooks/aws-prod-profiles.json
[
{
"profile": "my-production",
"account": "000000000000",
"description": "Main production account"
}
]
Add as many profiles as you need. The account and description fields are included in the confirmation message Claude shows the user, making it clear exactly what is about to be touched.
The key insight
The real value of hooks is not blanket blocking — it is interception at the right boundary. You let everything through until it reaches a dangerous environment, then you step in with an ask (or a hard deny if you prefer zero tolerance).
This gives you surgical control: Claude moves freely across dev and staging, but the moment a production profile appears in a command, the hook fires and puts a human back in the loop. The dangerous part gets flagged; everything else stays frictionless.
File structure
~/.claude/
├── settings.json # permissions + hook registration
└── hooks/
├── aws-prod-guard.sh # the guard script (chmod +x)
└── aws-prod-profiles.json # list of protected profiles
~/.claude/settings.json — registers the hook and grants the broad AWS permission:
{
"permissions": {
"allow": ["Bash(aws *)"]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(aws *)",
"command": "~/.claude/hooks/aws-prod-guard.sh"
}
]
}
]
}
}
~/.claude/hooks/aws-prod-profiles.json — the only file you need to edit to add or remove protected profiles:
[
{
"profile": "my-production",
"account": "000000000000",
"description": "Main production account"
}
]
~/.claude/hooks/aws-prod-guard.sh — the guard script (see above).
Top comments (0)