Three-Tier Enforcement
Why Three Tiers
Every agentic coding system starts with an instruction file: CLAUDE.md, AGENTS.md, GEMINI.md, or a system prompt. Over extended sessions (100+ tool calls), three failure modes erode these soft instructions. First, context compaction discards them — when the context window fills, compaction algorithms may reduce your rules to a single line or drop them entirely. Second, the agent deprioritizes them — instructions at the beginning of a long session compete with hundreds of more recent tool results, and models attend more to recent tokens. Third, the agent reasons around them — given a constraint and a goal expressed in natural language, the model may find a path that satisfies the goal while technically violating the constraint. For non-critical concerns like code style, this degradation is tolerable. For safety-critical behavior — blocking destructive commands, preventing secret leakage, enforcing branch protection — it is not. This is why every mature agentic platform has converged on three tiers: soft guidelines the agent reads, deterministic rules it cannot skip, and hard gates it cannot bypass.
graph LR
subgraph g ["Guideline"]
G["CLAUDE.md<br/><i>Soft — agent tries to follow</i>"]
end
subgraph r ["Rule"]
R["Hooks<br/><i>Deterministic — always fires</i>"]
end
subgraph ga ["Gate"]
GA["Permissions<br/><i>Hard block — cannot bypass</i>"]
end
G -->|"agent forgets"| R -->|"hook misconfigured"| GA
style g fill:#eef2ff,stroke:#c7d2fe
style r fill:#fefce8,stroke:#fde68a
style ga fill:#fef2f2,stroke:#fecaca
The Three Tiers
Tier 1: Guidelines (Soft)
Guidelines are natural-language instructions the agent reads at session start and attempts to follow. They are the most flexible tier and the easiest to author. They are also the weakest.
| Platform | Mechanism |
|---|---|
| Claude Code | CLAUDE.md (project root, ~/.claude/CLAUDE.md global) |
| OpenAI Codex | AGENTS.md / system prompts in Agents SDK |
| Gemini CLI | GEMINI.md (project root, ~/.gemini/GEMINI.md global) |
| Google ADK | System instructions defined in agent constructor |
| LangGraph | System prompts in graph node configuration |
| CrewAI | Role + Goal + Backstory fields per agent |
The fundamental weakness: Guidelines live in the context window. They are subject to compaction, attention decay, and prompt injection. An agent may follow a guideline 95% of the time, but the 5% failure rate on “never delete the production database” is unacceptable.
Tier 2: Rules (Deterministic)
Rules are scripts or functions that execute automatically at defined lifecycle points. They run outside the model — in the host process, the CLI harness, or middleware — and fire regardless of what the agent decides.
| Platform | Mechanism |
|---|---|
| Claude Code | Hooks in .claude/settings.json (PreToolUse, PostToolUse, etc.) |
| OpenAI Codex | Guardrails in Agents SDK (input/output validators) |
| Google ADK | Before/after callbacks + built-in plugins (PII redaction, Gemini-as-Judge) |
| LangGraph | Middleware nodes inserted into the execution graph |
| CrewAI | Task-level constraints + agent-level tool restrictions |
How rules work in practice: The agent decides to call a tool. Before execution, the harness fires a hook. A script inspects the tool name and arguments, then returns one of three signals: allow, block, or modify. After execution, another hook can inspect the output — running a linter on written code, scanning for secrets in command output, or verifying test results. These hooks run in the host process, not in the model’s reasoning.
Limitation: Rules execute at lifecycle points defined by the harness. They cannot prevent the agent from thinking about a forbidden action, and they cannot enforce constraints outside the tool-call lifecycle.
Tier 3: Gates (Hard Block)
Gates are technical barriers enforced by the operating system, network infrastructure, or platform runtime. They operate below the agent and the harness.
| Platform | Mechanism |
|---|---|
| Claude Code | Permission model: allowedTools, deniedTools, filesystem allow/deny paths |
| OpenAI Codex | OS-enforced sandbox (restricted shell), network isolation, approval policies |
| Google ADK | VPC Service Controls, code execution sandbox, identity-based auth |
| Gemini CLI | Permission system + Cloud Shell environment isolation |
| LangGraph | Tool-level access restrictions + middleware policies |
| CrewAI | Crew-level tool access control configuration |
How gates differ from rules: A rule is a script that inspects and blocks. A gate is a capability removal. If the agent is denied write access to /etc/, no amount of clever tool arguments will change that — the filesystem itself rejects the operation. If the sandbox blocks outbound network, the agent cannot exfiltrate data regardless of what commands it runs.
Trade-off: Gates are the least flexible tier. They cannot express nuanced policies (“allow deletion of test fixtures but not source files”) without becoming complex. Use them for bright-line rules where the answer is always yes or always no.
Decision Matrix
| Policy | Tier | Mechanism | Rationale |
|---|---|---|---|
| Code style (quotes, indentation) | Guideline | Instruction file | Low stakes. Violations are cosmetic. Linter can catch later. |
| Auto-lint after file writes | Rule | PostToolUse hook | Must always happen. Cannot rely on agent remembering. |
| Auto-format on save | Rule | PostToolUse hook | Deterministic transformation. No judgment required. |
| Run tests after code changes | Rule | PostToolUse hook | Agent may skip tests under time pressure. Hook ensures it. |
| Secret detection before commit | Rule | PreToolUse hook on git commit | Secrets in version control are a security incident. Must scan every time. |
Block rm -rf / | Gate | Permission deny list | Catastrophic. Must be technically impossible, not just discouraged. |
No push to main | Gate | Branch protection + permission deny | Organizational policy. Cannot be overridden by any agent. |
| Restrict filesystem to project dir | Gate | Sandbox / filesystem allow list | Prevents lateral movement. OS-level enforcement. |
| Block outbound network | Gate | Network sandbox policy | Prevents data exfiltration. Cannot be a suggestion. |
| Prefer small functions | Guideline | Instruction file | Subjective. Agent needs judgment about when to split. |
| Always read before editing | Guideline | Instruction file | Good practice but context-dependent. Agent may have file cached. |
| PII redaction in outputs | Rule | Output guardrail / after-tool callback | Privacy requirement. Must fire on every output. |
| Require PR description format | Rule | PreToolUse hook on PR creation | Team standard. Deterministic template check. |
| Block access to prod credentials | Gate | Environment isolation / deny list | Security boundary. Must be impossible, not discouraged. |
The general principle: if the consequence of a violation is measured in inconvenience, use a guideline. If it is measured in time (debugging, reverting), use a rule. If it is measured in incidents (security, data loss, downtime), use a gate.
The Degradation Problem
Guidelines degrade predictably over long sessions.
journey
title Guideline Compliance Over a Long Session
section 0–30 min
Instructions at top of context: 5: Agent
Agent runs tests after every edit: 5: Agent
All guidelines followed reliably: 5: Agent
section 30–60 min
Context filling with tool results: 3: Agent
Edits 4 files, tests once at end: 3: Agent
Costly rules skipped first: 2: Agent
section 60–90 min
Compaction fires: 2: Agent
Instructions reduced to one line: 1: Agent
Commits without running tests: 1: Agent
section 90+ min
Agent reverts to base behavior: 1: Agent
Pushes to main (was forbidden): 1: Agent
Skips tests (was required): 1: Agent
The rules the agent drops first are the ones that cost the most: running a full test suite (slow), writing verbose commit messages (effort), reading files before editing (extra tool calls). These are precisely the rules that matter most for code quality.
The solution is not to write better guidelines. Move critical behavior into Tier 2 (rules) or Tier 3 (gates), where compliance does not depend on the agent’s context window.
Implementation Examples
Tier 1: Guidelines
Claude Code — CLAUDE.md:
# Project Guidelines
## Code Style
- Use TypeScript strict mode
- Prefer named exports over default exports
- Maximum function length: 40 lines
## Workflow
- Read existing tests before writing new ones
- Run the test suite after modifying any source file
- Use conventional commit messages (feat:, fix:, refactor:, etc.)
Google ADK — System Instructions:
agent = Agent(
name="code_assistant",
model="gemini-2.5-pro",
instruction="""You are a code assistant for a Go microservices project.
- Follow the Go standard project layout
- All errors must be wrapped with fmt.Errorf
- Run `go test ./...` after modifying any .go file
- Use structured logging (slog) not fmt.Println
""",
)
Tier 2: Rules
Claude Code — Hooks in .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hook": "npx eslint --fix $CLAUDE_FILE_PATH && npx prettier --write $CLAUDE_FILE_PATH"
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hook": "python3 .claude/hooks/check_dangerous_commands.py"
}
]
}
}
Supporting hook script (.claude/hooks/check_dangerous_commands.py):
import json, sys, re
BLOCKED_PATTERNS = [
r"rm\s+-rf\s+/", r"rm\s+-rf\s+~", r"mkfs\.", r"dd\s+if=.*of=/dev/",
r"chmod\s+-R\s+777", r"git\s+push.*--force.*main", r"git\s+push.*main",
]
def check_command():
tool_input = json.loads(sys.stdin.read())
command = tool_input.get("command", "")
for pattern in BLOCKED_PATTERNS:
if re.search(pattern, command):
print(json.dumps({"decision": "block",
"reason": f"Blocked: matches dangerous pattern '{pattern}'"}))
sys.exit(0)
print(json.dumps({"decision": "allow"}))
if __name__ == "__main__":
check_command()
Google ADK — Before-Tool Callback:
BLOCKED_COMMANDS = [r"rm\s+-rf\s+/", r"git\s+push.*main", r"DROP\s+TABLE"]
def before_tool_callback(tool_name: str, tool_input: dict) -> dict | None:
if tool_name == "execute_command":
command = tool_input.get("command", "")
for pattern in BLOCKED_COMMANDS:
if re.search(pattern, command, re.IGNORECASE):
return {"blocked": True, "reason": f"Matches '{pattern}'"}
return None
agent = Agent(
name="code_assistant", model="gemini-2.5-pro",
before_tool_callback=before_tool_callback,
)
Tier 3: Gates
Claude Code — Permission Configuration (.claude/settings.json):
{
"permissions": {
"allowedTools": [
"Read", "Write", "Edit",
"Bash(npm test:*)", "Bash(npx eslint:*)", "Bash(npx prettier:*)",
"Bash(git add:*)", "Bash(git commit:*)", "Bash(git diff:*)",
"Bash(git log:*)", "Bash(git status:*)"
],
"deniedTools": [
"Bash(rm -rf:*)", "Bash(git push:*)", "Bash(sudo:*)",
"Bash(chmod:*)", "Bash(curl:*)", "Bash(wget:*)"
]
}
}
The harness rejects denied tool calls before they reach the shell.
OpenAI Codex — Sandbox Policy:
[sandbox]
network = false
writable_paths = ["./src", "./tests", "./docs"]
readonly_paths = ["./config", "./package.json"]
[approval]
require_approval = ["git push", "npm publish", "docker push"]
Codex enforces these at the OS level. The sandbox intercepts system calls; network requests fail at the socket level. Even if the agent pipes a command through bash -c or uses a language-native HTTP client, the sandbox blocks it.
Layering the Tiers
The three tiers are not alternatives — they are layers. Consider “never push to main”:
- Guideline: CLAUDE.md states “Create a feature branch and open a pull request.” Handles the 90% case.
- Rule: A
PreToolUsehook onBashblocks commands matchinggit push.*main. Catches guideline failures after context compaction. - Gate: Repository branch protection requires a pull request with approval before merging to main. Even if both the guideline and rule fail, GitHub/GitLab rejects the push.