Progressive trust levels for agent independence.
Instead of binary "ask for everything" or "do whatever you want", autonomy levels give Claude graduated independence:
A0 (Manual) --> Confirm everything
A1 (Guided) --> Skip confirmation for previously-approved categories
A2 (Supervised) --> Skip confirmation for approved file patterns
A3 (Trusted) --> Skip confirmation for all medium-risk if validation passes
A4 (Autonomous) --> Even some high-risk, based on successful history
Every project starts at A0. Claude earns higher levels through successful actions.
Probably not. Most projects work fine with the default behavior:
- Claude asks before risky operations
- You approve or deny
- No explicit level tracking needed
Autonomy levels are useful when:
- You run long-running sessions where constant approvals slow things down
- You have well-understood codebases with clear safe/unsafe zones
- You want to formalize the "I trust Claude with X but not Y" pattern
Everything requires confirmation. Safe for first-time use on any project.
After you approve 5 actions of the same type (e.g., editing Python files), Claude can repeat that category without asking.
{
"level": 1,
"approved_categories": ["python", "typescript"],
"success_count": 7
}You define file patterns where Claude can edit freely:
{
"level": 2,
"grants": [
{ "pattern": "src/**/*.py", "type": "glob", "reason": "All Python source files" },
{ "pattern": "tests/**", "type": "glob", "reason": "Test directory" }
]
}Claude can edit src/routes/users.py without asking, but still needs approval for docker-compose.yml.
All medium-risk actions proceed if validation passes (syntax check, test run). High-risk still requires approval.
Even some high-risk commands auto-approve if the exact same command has been approved before:
{
"level": 4,
"high_risk_history": [
"git push origin feature-branch",
"docker restart myapp"
]
}Note: This uses exact string matching, not pattern matching. docker restart myapp in history does NOT approve docker restart myapp-v2. This is intentional -- it's safer to require re-approval for any variation.
Getting to A4 requires explicit user action -- it never auto-escalates to this level.
Action succeeds --> success_count += 1
5 successes (same category, from A0) --> Suggest A1
10 successes (from A1) --> Suggest A2
25 successes (from A2) --> Suggest A3
Explicit user command --> A4
Action fails/error --> Drop 1 level
2+ errors in 10 minutes --> Drop to A0
Autonomy state is stored per-project in .autonomy-state:
{
"level": 2,
"grants": [
{
"pattern": "src/**/*.py",
"type": "glob",
"granted_at": "2026-01-15T10:00:00Z",
"reason": "Python source files approved after 10 successful edits"
}
],
"approved_categories": ["python", "config"],
"high_risk_history": [],
"success_count": 15,
"error_count": 0,
"last_escalation": "2026-01-15T09:30:00Z"
}If you adopt this module, you need a hook that reads .autonomy-state and decides whether to prompt:
#!/bin/bash
# hooks/check-autonomy
# PreToolUse hook that checks autonomy level
set -uo pipefail
# Fail closed if jq is missing
if ! command -v jq >/dev/null 2>&1; then
echo "ERROR: jq is required for autonomy checks." >&2
exit 2
fi
INPUT=$(cat)
if [ -z "$INPUT" ]; then
echo "ERROR: No input received." >&2
exit 2
fi
TOOL=$(printf '%s' "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // empty')
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty')
# Load autonomy state
STATE_FILE=".autonomy-state"
LEVEL=0
if [ -f "$STATE_FILE" ] && [ ! -L "$STATE_FILE" ]; then
LEVEL=$(jq -r '.level // 0' "$STATE_FILE" 2>/dev/null || echo "0")
fi
# Determine risk (reuse from guard-risk or inline)
RISK="low"
if [ "$TOOL" = "Write" ] || [ "$TOOL" = "Edit" ]; then
RISK="medium"
fi
CMD_UPPER="${CMD^^}"
if [[ "$CMD" =~ (^|[[:space:]])rm[[:space:]].*-[rR] ]] \
|| [[ "$CMD_UPPER" =~ DROP[[:space:]]+(DATABASE|TABLE) ]] \
|| [[ "$CMD" =~ git[[:space:]]+push[[:space:]]+--force ]] \
|| [[ "$CMD" =~ git[[:space:]]+reset[[:space:]]+--hard ]]; then
RISK="high"
fi
# Apply autonomy rules
case "$RISK" in
"low")
exit 0 # Always allow
;;
"medium")
if [ "$LEVEL" -ge 3 ]; then
exit 0 # A3+ auto-approves medium
elif [ "$LEVEL" -ge 2 ] && [ -n "$FILE_PATH" ]; then
# Check grants using a safe while-read loop
MATCHED=false
while IFS= read -r GRANT; do
[ -z "$GRANT" ] && continue
# Reject overly broad patterns
case "$GRANT" in "*"|"/*"|"**") continue ;; esac
# WARNING: bash [[ == ]] glob does NOT support ** without
# shopt -s globstar. For deep path matching, adapt this
# to use a case statement or dedicated tool.
if [[ "$FILE_PATH" == $GRANT ]]; then
MATCHED=true
break
fi
done < <(jq -r '.grants[]?.pattern // empty' "$STATE_FILE" 2>/dev/null)
if [ "$MATCHED" = true ]; then
exit 0
fi
fi
echo "AUTONOMY: Medium-risk action at level A${LEVEL}. Requesting approval." >&2
exit 0 # Advisory (change to exit 2 for enforcement)
;;
"high")
if [ "$LEVEL" -ge 4 ]; then
# Check exact command history (not patterns -- exact strings only)
if jq -e --arg cmd "$CMD" '.high_risk_history | index($cmd)' "$STATE_FILE" >/dev/null 2>&1; then
exit 0 # Previously approved exact command
fi
fi
echo "BLOCKED: High-risk action requires explicit approval." >&2
exit 2
;;
esac## Autonomy
This project uses autonomy levels (A0-A4) to control agent independence.
Current level is stored in `.autonomy-state`.
- Low-risk actions always proceed
- Medium-risk actions check autonomy grants
- High-risk actions always require explicit approval (unless A4 with history)
- Errors demote the autonomy levelSee autonomy-state.schema.json for the full schema.