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.

PlatformMechanism
Claude CodeCLAUDE.md (project root, ~/.claude/CLAUDE.md global)
OpenAI CodexAGENTS.md / system prompts in Agents SDK
Gemini CLIGEMINI.md (project root, ~/.gemini/GEMINI.md global)
Google ADKSystem instructions defined in agent constructor
LangGraphSystem prompts in graph node configuration
CrewAIRole + 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.

PlatformMechanism
Claude CodeHooks in .claude/settings.json (PreToolUse, PostToolUse, etc.)
OpenAI CodexGuardrails in Agents SDK (input/output validators)
Google ADKBefore/after callbacks + built-in plugins (PII redaction, Gemini-as-Judge)
LangGraphMiddleware nodes inserted into the execution graph
CrewAITask-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.

PlatformMechanism
Claude CodePermission model: allowedTools, deniedTools, filesystem allow/deny paths
OpenAI CodexOS-enforced sandbox (restricted shell), network isolation, approval policies
Google ADKVPC Service Controls, code execution sandbox, identity-based auth
Gemini CLIPermission system + Cloud Shell environment isolation
LangGraphTool-level access restrictions + middleware policies
CrewAICrew-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

PolicyTierMechanismRationale
Code style (quotes, indentation)GuidelineInstruction fileLow stakes. Violations are cosmetic. Linter can catch later.
Auto-lint after file writesRulePostToolUse hookMust always happen. Cannot rely on agent remembering.
Auto-format on saveRulePostToolUse hookDeterministic transformation. No judgment required.
Run tests after code changesRulePostToolUse hookAgent may skip tests under time pressure. Hook ensures it.
Secret detection before commitRulePreToolUse hook on git commitSecrets in version control are a security incident. Must scan every time.
Block rm -rf /GatePermission deny listCatastrophic. Must be technically impossible, not just discouraged.
No push to mainGateBranch protection + permission denyOrganizational policy. Cannot be overridden by any agent.
Restrict filesystem to project dirGateSandbox / filesystem allow listPrevents lateral movement. OS-level enforcement.
Block outbound networkGateNetwork sandbox policyPrevents data exfiltration. Cannot be a suggestion.
Prefer small functionsGuidelineInstruction fileSubjective. Agent needs judgment about when to split.
Always read before editingGuidelineInstruction fileGood practice but context-dependent. Agent may have file cached.
PII redaction in outputsRuleOutput guardrail / after-tool callbackPrivacy requirement. Must fire on every output.
Require PR description formatRulePreToolUse hook on PR creationTeam standard. Deterministic template check.
Block access to prod credentialsGateEnvironment isolation / deny listSecurity 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”: