ESLint blocks a pull request when it sees console.log in production code. Biome refuses to compile when it encounters an any type. These tools enforce rules on human-written code. When an AI agent writes thousands of lines in seconds, who enforces the rules?
The gap between prompt engineering and production governance is real. Runtime logging tells you what happened. Policy-as-code tells the agent what it can and cannot do before it acts.
The Governance Problem
Agents operate at a different scale than humans. A coding agent can:
- Modify 50 files in one tool call
- Execute shell commands with filesystem access
- Install dependencies that introduce supply-chain risk
- Commit code that bypasses review workflows
Traditional guardrails fail here:
- Post-hoc logging shows you the damage after it happens
- Prompt constraints are advisory, not enforceable
- Manual review does not scale to agent velocity
- Sandboxing blocks legitimate workflows alongside dangerous ones
You need declarative rules that sit between the agent runtime and the codebase, checked before execution, versioned alongside code, and enforced in CI.
Policy File Structure
An agent governance policy is a declarative constraint file. It defines what the agent can touch, which commands it can run, and which actions require human approval.
# .agent-policy.yml
version: "1.0"
agent_id: "code-assistant-prod"
permissions:
filesystem:
allow:
- "src/**/*.ts"
- "tests/**/*.spec.ts"
deny:
- ".env*"
- "secrets/**"
- "node_modules/**"
commands:
allow:
- "npm test"
- "npm run lint"
deny:
- "rm -rf"
- "curl *"
- "git push --force"
network:
allow_outbound: false
exceptions:
- "registry.npmjs.org"
constraints:
max_files_per_action: 10
require_human_approval:
- file_pattern: "package.json"
- file_pattern: "Dockerfile"
- command_pattern: "git commit"
audit:
log_all_actions: true
retention_days: 90
alert_on_deny: true
This file lives in version control. Changes go through pull requests. Diffs show exactly what permissions changed.
Enforcement Boundaries
Policy enforcement happens at three layers:
1. Pre-execution validation
The agent runtime checks the policy before executing any tool call. If the action violates a rule, the call is blocked and logged.
// agent-runtime/policy-guard.ts
export async function validateToolCall(
call: ToolCall,
policy: AgentPolicy
): Promise<ValidationResult> {
if (call.type === 'filesystem') {
const path = call.params.path;
// Check deny list first
if (policy.permissions.filesystem.deny.some(pattern =>
minimatch(path, pattern)
)) {
return {
allowed: false,
reason: `Path ${path} matches deny pattern`,
requires_approval: false
};
}
// Check allow list
if (!policy.permissions.filesystem.allow.some(pattern =>
minimatch(path, pattern)
)) {
return {
allowed: false,
reason: `Path ${path} not in allow list`,
requires_approval: false
};
}
}
// Check if action requires human approval
const needsApproval = policy.constraints.require_human_approval.some(rule =>
matchesApprovalRule(call, rule)
);
return {
allowed: true,
requires_approval: needsApproval
};
}
2. CI/CD integration
A pre-commit hook or GitHub Action validates that the agent’s planned actions comply with policy before merging.
#!/bin/bash
# .husky/pre-commit
# Run agent policy linter
agent-policy-lint --config .agent-policy.yml \
--agent-log .agent-actions.jsonl
if [ $? -ne 0 ]; then
echo "Agent policy violations detected. Run 'agent-policy-lint --fix' or update policy."
exit 1
fi
3. Runtime sandbox boundaries
Even if policy allows an action, the agent runs inside a container or VM with hard limits on filesystem access, network egress, and process execution.
# docker-compose.agent.yml
services:
agent-runtime:
image: agent-runtime:latest
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
read_only: true
volumes:
- ./src:/workspace/src:ro
- ./agent-scratch:/workspace/scratch:rw
network_mode: none
Approval Workflow
Some actions cannot be fully automated. When the agent wants to modify package.json or run a deployment command, it requests human approval.
// agent-runtime/approval-queue.ts
export async function requestApproval(
action: ToolCall,
reason: string
): Promise<ApprovalResponse> {
const request = {
id: generateId(),
timestamp: Date.now(),
action,
reason,
status: 'pending'
};
// Write to approval queue
await db.approvalQueue.insert(request);
// Notify via Slack, email, or dashboard
await notifyApprovers({
message: `Agent requests approval: ${reason}`,
action: action.type,
details: action.params
});
// Block until approved or timeout
return await waitForApproval(request.id, {
timeout: 3600000 // 1 hour
});
}
Approvers see a dashboard with the full context: which files the agent wants to change, the diff, and the policy rule that triggered the approval gate.
Versioning and Conflict Resolution
Policy files evolve. A new agent capability requires new permissions. A security incident tightens constraints. How do you version policies without breaking existing agents?
Policy versioning
Each policy file declares a version. The agent runtime refuses to execute if the policy version does not match its expected schema.
# .agent-policy.yml
version: "2.1" # Breaking change from 2.0
schema_url: "https://agent-policy.dev/schemas/v2.1.json"
Team-specific overrides
Large codebases have multiple teams. The frontend team allows different filesystem paths than the backend team.
# .agent-policy.yml (root)
version: "2.1"
default_permissions:
filesystem:
deny: [".env*", "secrets/**"]
# frontend/.agent-policy.override.yml
permissions:
filesystem:
allow:
- "frontend/src/**/*.tsx"
- "frontend/components/**"
# backend/.agent-policy.override.yml
permissions:
filesystem:
allow:
- "backend/src/**/*.go"
- "backend/migrations/**"
The agent runtime merges the root policy with the directory-specific override. Deny rules always win.
Conflict detection
When two policies conflict, the linter fails CI:
$ agent-policy-lint --check-conflicts
ERROR: Policy conflict detected
Root policy denies: backend/secrets/**
Backend override allows: backend/secrets/test-fixtures/**
Resolution: Remove override or add explicit exception in root policy.
Audit Trail
Every agent action generates an audit log entry, even if the action was blocked.
{"timestamp":"2026-06-08T14:32:01Z","agent_id":"code-assistant-prod","action":"filesystem.write","path":"src/utils/logger.ts","allowed":true,"policy_version":"2.1"}
{"timestamp":"2026-06-08T14:32:15Z","agent_id":"code-assistant-prod","action":"command.exec","command":"rm -rf /tmp/cache","allowed":false,"reason":"Command matches deny pattern","policy_version":"2.1"}
{"timestamp":"2026-06-08T14:33:42Z","agent_id":"code-assistant-prod","action":"filesystem.write","path":"package.json","allowed":true,"requires_approval":true,"approval_id":"apr_8x3k2","policy_version":"2.1"}
This log is append-only, stored in S3 or a SIEM, and queryable for compliance audits.
Bypass Mechanism
Sometimes you need to override policy. A production incident requires an agent to modify a locked file. The bypass mechanism requires explicit justification and leaves a permanent audit trail.
$ agent-policy bypass --reason "P0 incident: fix auth service crash" \
--allow "backend/auth/config.yml" \
--duration 30m \
--approver alice@example.com
Bypass granted: bypass_7h2k9
Expires: 2026-06-08T15:05:00Z
Audit log: s3://audit-logs/bypasses/bypass_7h2k9.json
The bypass is time-limited. After expiration, the agent reverts to normal policy enforcement.
Trade-offs and Failure Modes
| Approach | Benefit | Risk |
|---|---|---|
| Strict deny-by-default | Prevents unauthorized actions | Blocks legitimate workflows, slows development |
| Permissive allow-by-default | Faster iteration | Agent can cause damage before policy catches up |
| Approval gates on sensitive actions | Human oversight on risky changes | Bottleneck if approvers are unavailable |
| Policy versioning | Safe evolution of rules | Version mismatch can halt all agents |
| Bypass mechanism | Escape hatch for emergencies | Abuse risk if not audited properly |
Common failure modes:
- Policy drift: Local overrides accumulate, root policy becomes meaningless
- Approval fatigue: Too many approval requests, humans rubber-stamp without review
- Sandbox escape: Agent finds a way to execute commands outside the container
- Log flooding: Agent generates millions of audit entries, making real violations hard to find
Implementation Checklist
To build agent governance as code:
- Define the policy schema: YAML, JSON, or a custom DSL
- Build the policy validator: A library that checks tool calls against rules
- Integrate with agent runtime: Block disallowed actions before execution
- Add CI/CD checks: Lint policy files and agent logs in pre-commit hooks
- Create an approval UI: Dashboard for humans to review and approve gated actions
- Implement audit logging: Append-only log of all agent actions and policy decisions
- Version the policy: Schema versioning and backward compatibility checks
- Test the bypass flow: Ensure emergency overrides work and are auditable
Technical Verdict
Use agent governance as code when:
- Agents operate in production codebases with compliance requirements
- Multiple teams share the same agent infrastructure
- You need to prove to auditors that agent actions are controlled and logged
- Agent velocity exceeds human review capacity
Avoid it when:
- Agents are experimental or sandboxed in isolated environments
- The overhead of policy management exceeds the risk of agent misbehavior
- Your team lacks the tooling to enforce policies at runtime
- Policy conflicts and versioning would create more friction than value
The linter analogy holds: just as ESLint prevents bad code from reaching production, agent governance prevents bad actions from reaching your codebase. The difference is that agents move faster, have broader access, and require enforcement at runtime, not just in CI.