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
- Agent Tool Allowlists: Least Privilege for Filesystem and Shell Access
- Claude Code Hooks: Gate Risky Shell Commands Before CI Runs
- AI On-Call Copilots: Suggest Runbooks Without Mutating Production
- Secure AI Sandboxes: Ephemeral ECS Tasks for Agent Tool Execution
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
