Policy-as-Code for AI: OPA Checks Before Every Shell Execution

Policy-as-Code for AI: OPA Checks Before Every Shell Execution

Allowlists help; OPA makes the rules reviewable, testable, and versioned. Every agent shell proposal becomes an input document—deny metadata IPs, kubectl delete, force-push, and naked aws s3 sync before the process starts.

⚡ TL;DR: Wrap the shell tool with an OPA/Rego gate. Pass argv, cwd, env fingerprints, and actor; deny by default; unit-test policies in CI. Elevation requires dual-control. Pair with Agent Tool Allowlists and Claude Code Hooks.

Input document

{
  "argv": ["curl", "-s", "http://169.254.169.254/latest/meta-data/"],
  "cwd": "/workspace/app",
  "actor": "agent:pr-4421",
  "elevation": false,
  "network": true
}

Rego that fails closed

package agent.shell

default allow := false

dangerous_host if {
  some i
  host := input.argv[i]
  host == "169.254.169.254"
}

dangerous_host if {
  some i
  startswith(input.argv[i], "http://169.254.169.254")
}

deny[msg] if {
  dangerous_host
  msg := "metadata_ip_blocked"
}

deny[msg] if {
  input.argv[0] == "kubectl"
  some i
  input.argv[i] == "delete"
  not input.elevation
  msg := "kubectl_delete_needs_elevation"
}

deny[msg] if {
  input.argv[0] == "aws"
  input.argv[1] == "s3"
  input.argv[2] == "sync"
  not startswith(input.argv[3], "s3://my-org-scratch/")
  msg := "s3_sync_unscoped"
}

allow if {
  count(deny) == 0
  input.argv[0] in {"pnpm", "npm", "pytest", "go", "cargo", "git"}
}

✅ Default deny + explicit command allowlist.
❌ “Permit all except a few regexes” maintained in prompt text only.

Hook integration

// beforeShell(cmd)
const decision = await opa.evaluate("agent/shell/allow", {
  argv: parse(cmd),
  cwd: process.cwd(),
  actor: session.actor,
  elevation: session.elevated,
});
if (!decision.result) {
  throw new Error(`opa_deny:${decision.denyReasons.join(",")}`);
}

Same placement as Claude Code / Cursor hooks—policy is code, not chat. For IAM mutations specifically, still run Access Analyzer gates.

Policy CI

opa test ./policy/agent -v
conftest verify --policy policy/agent fixtures/shell/*.json

Break the build when a fixture that should deny suddenly allows.

Elevation and audit

Elevation is a separate Rego path, not a prompt wink. Require input.elevation == true and a change ticket ID that exists in your tracker API before allowing kubectl delete or cloud destructive verbs. Emit every decision to CloudWatch / OpenSearch with argv hash, actor, and policy version SHA so postmortems can answer “why did the agent run this?”

type ShellDecision = {
  allow: boolean;
  reasons: string[];
  policySha: string;
  argvHash: string;
  actor: string;
  ticketId?: string;
};

// Persist deny and allow — allows matter for forensics too
await logDecision(decision);

Pair elevation UX with AI On-Call Copilots so Slack approval and OPA elevation stay in sync.

Closing checklist

  • [ ] OPA bundled beside the agent runner; offline eval (no remote PDP required for deny path)
  • [ ] Unit tests for metadata IP, kubectl delete, force-push, curl file://
  • [ ] Elevation path audited with ticket ID
  • [ ] Decisions logged with argv hash (redact secrets)
  • [ ] Policy PRs require security CODEOWNERS
  • [ ] Prompt text never overrides OPA

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply