Protect (PR) · Authorization
Authorization at the client side layer
deterministic · Outside the model's reasoning
If the agent decides to violate this concern, what stops it at this layer?
What this cell does
Deny-by-default tool allowlist, capability-based restriction, PreToolUse hooks, pre-commit hooks.
Artifacts (4)
deny-protected-paths.shview on GitHub#!/usr/bin/env bash
# ABOUTME: pre-commit hook that fails when the diff touches operator-only paths.
# ABOUTME: This catches operator-machine commits; --no-verify bypasses it. Server-side pre-receive is the backstop.
set -euo pipefail
# Operator may set this env var to bypass the check for legitimate human commits
# performed via a controlled session. Setting it from inside an agent context
# requires write access to the operator's shell profile, which the agent should
# not have.
if [[ "${OPERATOR_OVERRIDE:-}" == "1" ]]; then
exit 0
fi
cat <<'EOF' >&2
BLOCKED: edits to protected paths are operator-only.
The agent must not commit to:
- infrastructure/prod/ (production IaC)
- .github/workflows/ (CI configuration)
- secrets/ (sealed secrets and key material)
- .claude/ (agent runtime configuration)
- /etc/agents/ (system-level agent config)
If you are the operator and need to make this change manually, set
OPERATOR_OVERRIDE=1 in your shell and re-run the commit.
This pre-commit hook is bypassable with `git commit --no-verify`. The
server-side Git pre-receive hook in controls/authorization/server-side/
enforces the same rule and is not bypassable. Try to push with --no-verify
applied locally; the push will fail there too.
EOF
exit 1
pre-commit-config.yamlview on GitHub# ABOUTME: pre-commit framework config running gitleaks and a protected-paths gate.
# ABOUTME: Drop at repo root as .pre-commit-config.yaml and run `pre-commit install`. Backstopped by server-side pre-receive.
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.30.1
hooks:
- id: gitleaks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: detect-private-key
- id: check-added-large-files
args: ["--maxkb=512"]
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: local
hooks:
- id: deny-protected-paths
name: Deny edits to protected paths from agent commits
language: script
entry: ./scripts/deny-protected-paths.sh
files: '^(infrastructure/prod/|\.github/workflows/|secrets/|\.claude/|/etc/agents/)'
always_run: false
pass_filenames: true
pre_tool_use.shview on GitHub#!/usr/bin/env bash
# ABOUTME: Claude Code PreToolUse hook with deny-then-ask-then-allow precedence.
# ABOUTME: Receives JSON on stdin. Exit 0 = allow. Exit 2 = deny (Claude Code shows the message). Exit other = error.
set -euo pipefail
LOG_DIR="${LOG_DIR:-/var/log/agents/claude-code}"
mkdir -p "$LOG_DIR" 2>/dev/null || true
# Hook receives JSON on stdin per Claude Code hook spec.
INPUT="$(cat)"
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name // empty')"
TOOL_INPUT="$(echo "$INPUT" | jq -r '.tool_input.command // empty')"
SESSION_ID="$(echo "$INPUT" | jq -r '.session_id // "default"')"
# ----- Tier-4 hard-deny patterns -----
# These cannot be confirmed past at the client side. The agent must not even
# be asked to run them. Each pattern is a regex applied to TOOL_INPUT.
DENY_PATTERNS=(
'\brm\s+-rf\s+/'
'\brm\s+-rf\s+~'
'\brm\s+-rf\s+\$HOME'
'\bterraform\s+destroy'
'\bterraform\s+apply.*-auto-approve'
'\bDROP\s+TABLE'
'\bDROP\s+DATABASE'
'\bTRUNCATE\s+TABLE'
'\bkubectl\s+delete\s+ns'
'\bkubectl\s+delete\s+namespace'
'\bkubectl\s+scale\s+.*--replicas=0'
'\baws\s+s3\s+rb'
'\baws\s+ec2\s+terminate-instances'
'\baws\s+rds\s+delete-db-instance'
'\bgh\s+repo\s+delete'
'\bgit\s+push\s+(--force|-f)'
'\bgit\s+reset\s+--hard'
'\bdd\s+if=/dev/zero\s+of='
'\bmkfs\b'
':\(\)\s*\{\s*:\|:&\s*\};:'
)
for pattern in "${DENY_PATTERNS[@]}"; do
if echo "$TOOL_INPUT" | grep -qE "$pattern"; then
echo "BLOCKED: command matches deny pattern: ${pattern}" >&2
echo "$(date -Iseconds) DENY $SESSION_ID $TOOL_NAME pattern=${pattern} input=${TOOL_INPUT}" \
>> "$LOG_DIR/pre_tool_use.log" 2>/dev/null || true
exit 2
fi
done
# ----- Obfuscation defenses -----
# Catches the trivial cases. Not a guarantee. An adversarial agent has more
# obfuscation options than a regex list can enumerate.
OBFUSCATION_PATTERNS=(
'\bbase64\s+(--decode|-d|-D)\b'
'\beval\s+\$\('
'\bbash\s+-c\s+\$\('
'\bsh\s+-c\s+\$\('
'\bpython3?\s+-c\s+["\x27]exec\('
'\bcurl\s+[^|]+\|\s*(bash|sh)\b'
'\bwget\s+[^|]+\|\s*(bash|sh)\b'
'\bxargs\s+rm\b'
)
for pattern in "${OBFUSCATION_PATTERNS[@]}"; do
if echo "$TOOL_INPUT" | grep -qE "$pattern"; then
echo "BLOCKED: obfuscation pattern detected: ${pattern}" >&2
echo "$(date -Iseconds) DENY-OBFUSCATION $SESSION_ID $TOOL_NAME pattern=${pattern} input=${TOOL_INPUT}" \
>> "$LOG_DIR/pre_tool_use.log" 2>/dev/null || true
exit 2
fi
done
# ----- Allow with logging -----
echo "$(date -Iseconds) ALLOW $SESSION_ID $TOOL_NAME input=${TOOL_INPUT}" \
>> "$LOG_DIR/pre_tool_use.log" 2>/dev/null || true
exit 0
settings.jsonview on GitHub{
"_comment": "defaultMode dontAsk auto-denies anything that would otherwise prompt: only the allow rules below, built-in read-only Bash, and calls a PreToolUse hook approves will run. Valid modes are default (aka manual), acceptEdits, plan, auto, dontAsk, bypassPermissions. There is no deny mode; the deny LIST below is what blocks specific tools.",
"permissions": {
"defaultMode": "dontAsk",
"allow": [
"Read",
"Glob",
"Grep"
],
"ask": [
"Edit",
"Write",
"Bash(git:status)",
"Bash(git:diff)",
"Bash(git:log)",
"Bash(git:add:*)",
"Bash(git:commit:*)",
"Bash(npm:install)",
"Bash(pip:install)"
],
"deny": [
"Bash(rm:-rf)",
"Bash(rm:-rf:/)",
"Bash(terraform:destroy)",
"Bash(terraform:apply:-auto-approve)",
"Bash(kubectl:delete:*)",
"Bash(kubectl:scale:*--replicas=0*)",
"Bash(aws:s3:rb)",
"Bash(aws:ec2:terminate-instances)",
"Bash(aws:rds:delete-db-instance)",
"Bash(gh:repo:delete)",
"Bash(curl:*-X:DELETE*)",
"Bash(docker:system:prune)",
"Bash(helm:uninstall)",
"Bash(git:push:--force)",
"Bash(git:reset:--hard)",
"Bash(dd:*)",
"Bash(mkfs:*)"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/etc/agents/hooks/pre_tool_use.sh"
}
]
}
]
}
}
Cell notes
Authorization / Client-side
Control. --allowedTools deny-by-default. Capability-based tool restriction at the SDK layer. PreToolUse pattern hooks with deny-then-ask-then-allow precedence. Pre-commit hooks blocking changes to protected paths. Hooks directory and config owned by the operator, not the agent.
Strength. Deterministic when the hook config is uncompromised and the agent runtime honors precedence. Bypassable through allowlisted-shell shell-out, command obfuscation (base64, eval, env-var indirection), equivalent commands not in the pattern list, --no-verify, and filesystem tampering when the agent has write access to the hook surface.
Tooling
- - Claude Code v2.1.40 or later (the May 2026 PreToolUse precedence patch, pre-patch versions allow
allowto overridedeny). - -
pre-commitframework on the operator's machine. - -
jqfor the hook script. - - A server-side Git pre-receive hook to backstop
--no-verify. Lives in../server-side/.
Files in this directory
- -
settings.json, Claude Code project settings withpermissionsdefaultMode: deny, an explicitallowlist (read-only ops), anasklist (mutation ops), adenylist (destructive ops). Drop in your project at.claude/settings.json(operator-owned, agent runs as a different user). - -
pre_tool_use.sh, PreToolUse hook that receives JSON on stdin per the Claude Code hook spec. Hard-deny patterns (rm -rf /,terraform destroy, fork bombs) and obfuscation patterns (base64 decode, eval). Deploy to/etc/agents/hooks/with mode 0755 and owner root. - -
pre-commit-config.yaml, pre-commit framework config that runs gitleaks plus a deny-protected-paths hook. Drop at the repo root as.pre-commit-config.yamland runpre-commit install. - -
deny-protected-paths.sh, pre-commit hook that fails when the diff touchesinfrastructure/prod/,.github/workflows/, orsecrets/. Operator commits to those paths; agent commits do not.
Verification
# 1. Hook fires on a denied command
echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/test"}}' \
| /etc/agents/hooks/pre_tool_use.sh
echo "exit code: $?"
# expected: BLOCKED message + exit 2
# 2. Hook does not fire on an allowed command
echo '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}' \
| /etc/agents/hooks/pre_tool_use.sh
echo "exit code: $?"
# expected: exit 0
# 3. Hook config ownership
ls -la /etc/agents/hooks/pre_tool_use.sh
# expected: owner root, mode 0755
# 4. Agent cannot edit the hook
sudo -u agent-runner touch /etc/agents/hooks/pre_tool_use.sh
# expected: Permission denied
# 5. Pre-commit fires on a protected path
git checkout -b test-protected
echo "test" >> infrastructure/prod/main.tf
git add . && git commit -m "test"
# expected: failure at the deny-protected-paths hook
Common mistakes
- - Allowing
Bash(*)in the allow list, which gates nothing. - - Pattern hooks that match
kubectl deletebut notkubectl scale --replicas=0. Pattern hooks catch casual misuse, not adversarial agents. Document the equivalent-command bypass and accept the matrix's framing. - - Hook config owned by the agent's own user. The agent edits its own deny list at runtime.
- - Forgetting to enable the May 2026 Claude Code patch where
allowno longer beatsdeny. Verify withclaude --versionand check the changelog. - - Pre-commit only on the operator's machine. The agent runs
git commit --no-verifyand skips it. Mitigation is the server-side pre-receive hook in../server-side/.
Citation
NIST CSF 2.0 PR.AA-05 (least privilege, separation of duties), PR.PS-01 (configuration management practices). NIST AI RMF MANAGE 2.4, MANAGE 4.1. OWASP LLM06 (Excessive Agency); LLM05 (Improper Output Handling). OWASP ASI02 (Tool Misuse), ASI05 (Unexpected Code Execution). OWASP MCP02, MCP05. OWASP Agentic Least Agency principle. NIST SP 800-207 §2.1 (least privilege).
Primary bypasses
Documented, not hypothetical. A control whose bypass is undocumented is worse than no control, because somebody trusted it.
- shell-out via allowlisted Bash
- pattern evasion (equivalent commands)
- --no-verify on pre-commit
Crosswalk
| NIST CSF 2 0 | PR.AA-05, PR.PS-01 |
|---|---|
| NIST AI RMF | MANAGE 2.4, MANAGE 4.1 |
| OWASP LLM | LLM05, LLM06 |
| OWASP AGENTIC | ASI02, ASI05 |
| OTHER | OWASP Agentic Least Agency, NIST SP 800-207 §2.1 |
Cite this cell:
https://agenticcovenants.com/protect/authorization/client-side/