Keeping Humans in the Loop: Rego Approval Gates Before AI Agent Remediation
An OPA Rego policy enforces mandatory human approval before AI agent remediation runs: automatic isolation, sign-off required for any destructive action.
Vendors in 2026 love to advertise sub-ten-second automatic remediation. Aqua Compass, Elastic Workflows, and a handful of others promise an agent that detects, decides, and fixes before an analyst has finished reading the alert. On paper, that is the ultimate promise of automated incident response. On the ground, it is exactly what keeps a security leader up at night: an agent that alone decides to disable a critical service account, encrypt a disk, or cut a network path is not saving time, it is relocating the risk.
The real question is not "should remediation be automated?" but "how do you insert a human checkpoint before an agent touches production, without giving up speed on the actions that genuinely don't need one?" The answer sits in a precise mechanism: a policy-as-code layer that classifies every action by blast radius, and an enforcement point that queries that policy before anything executes.
A distinction worth making: this is not tool scoping
This topic often gets conflated with static tool-permission scoping (which API calls an agent is allowed to make, fixed once in its configuration). This is something different: a decision made per action, at execution time, that takes the actual target and context into account. An agent may technically have the right to call the account-disable API; the question asked on every single call is whether this disablement, on this account, in this context, can run alone or must wait for a human.
Classifying actions by blast radius
The starting point is not technical, it is doctrinal: not every remediation action carries the same weight. Two axes are enough to classify them.
The first axis is reversibility. Isolating a host from the network undoes in one click. Encrypting a disk, deleting an account, or revoking certificates does not undo, or only after hours of restoration work. The second axis is scope: an action touching a single isolated asset does not carry the same weight as one touching a domain account, a user group, or an entire network range.
These two axes do not live in an architect's head, they are encoded as policy data, versioned like code:
# policy/data/actions.yaml
low_risk:
- action: isolate_host
reversible: true
scope: single_asset
- action: block_ip
reversible: true
scope: single_asset
- action: quarantine_file
reversible: true
scope: single_asset
requires_approval:
- action: disable_account
reversible: false
scope: identity
- action: delete_account
reversible: false
scope: identity
- action: encrypt_disk
reversible: false
scope: single_asset
- action: revoke_all_sessions
reversible: false
scope: domainThis classification is the real business decision. Everything that follows is just the mechanism that enforces it.
OPA as the decision engine (PDP)
Open Policy Agent plays the role of PDP (Policy Decision Point) here: the agent submits a structured request (the action, its target, the context) and gets back one of three verdicts: allow, require_approval, or deny. The engine knows nothing about the agent's internal business logic, it evaluates a standalone Rego policy:
package threatclaw.remediation
default decision := {"result": "deny", "reason": "action not classified"}
low_risk_actions := {"isolate_host", "block_ip", "quarantine_file"}
approval_required_actions := {"disable_account", "delete_account", "encrypt_disk", "revoke_all_sessions"}
decision := {"result": "allow", "reason": "reversible action, single-asset scope"} if {
input.action in low_risk_actions
input.target.scope == "single_asset"
}
decision := {"result": "require_approval", "reason": "irreversible or wide-scope action"} if {
input.action in approval_required_actions
}The most important line is the default at the top: if an action shows up on neither list, the default verdict is deny. An unclassified action is never treated as low-risk by omission. It is the classic deny-by-default posture, applied here to an agent's autonomy.
The enforcement point: the agent asks, it no longer decides alone
Before every remediation call, the agent queries the PDP over HTTP against the OPA server:
curl -s -X POST http://opa.internal:8181/v1/data/threatclaw/remediation/decision \
-H "Content-Type: application/json" \
-d '{
"input": {
"action": "disable_account",
"target": {"type": "user", "id": "j.smith", "scope": "identity"},
"context": {"initiator": "agent", "case_id": "INC-4821"}
}
}'The response is deterministic and readable by the agent's orchestrator:
{"result": {"result": "require_approval", "reason": "irreversible or wide-scope action"}}Before this gate existed, the logic was hardcoded into the agent, a plain branch with no nuance:
# before: hardcoded decision, no external checkpoint
if action == "isolate_host":
execute(action)
elif action == "disable_account":
execute(action) # no risk distinction whatsoever
Once the PDP is wired in, the agent no longer decides anything on its own: it executes, queues for human review, or rejects, based on an external and auditable answer:
decision=$(curl -s -X POST http://opa.internal:8181/v1/data/threatclaw/remediation/decision \
-d @input.json | jq -r '.result.result')
case "$decision" in
allow) execute_action ;;
require_approval) queue_for_human_review ;;
*) reject_action ;;
esacA change of a few lines moves the responsibility elsewhere: the risk logic leaves the agent's code and lives in a versioned policy that gets reviewed and tested independently.
The trap: an untested Rego policy is a silent regression
A Rego policy is code. Left untested, it breaks silently: an action gets renamed, a typo slips into a set literal, the input structure gets refactored, and an action that used to require approval quietly gets auto-approved, with nobody noticing until the incident happens. This is the single most expensive trap in this design, and the easiest to avoid.
The fix is a unit-test suite against the policy itself, run with conftest, in a dedicated policy_test.rego file:
package threatclaw.remediation
test_isolate_host_is_autonomous if {
decision.result == "allow" with input as {
"action": "isolate_host",
"target": {"type": "host", "id": "WKS-042", "scope": "single_asset"}
}
}
test_disable_account_requires_approval if {
decision.result == "require_approval" with input as {
"action": "disable_account",
"target": {"type": "user", "id": "test", "scope": "identity"}
}
}
test_unclassified_action_is_denied if {
decision.result == "deny" with input as {
"action": "wipe_disk",
"target": {"type": "host", "id": "WKS-042", "scope": "single_asset"}
}
}These tests plug directly into CI, right next to the application's regular test suite:
policy-tests:
stage: test
script:
- conftest verify --policy policy/Any policy change that would break the classification (say, accidentally moving disable_account into the autonomous bucket) fails the pipeline before merge, exactly like a broken unit test on application code. The policy stops being a config file hand-edited in production; it becomes a versioned, reviewed, tested artifact.
Aligned with the human-control doctrine
This is not an isolated technical trade-off, it reflects a plain principle: a human signs off on every write action that changes production state, and automation only covers what is both reversible and narrow in scope. Isolating a compromised host undoes in a single gesture; disabling an account, encrypting a volume, or revoking access across a whole domain does not come back the same way. Until an action has been explicitly classified as reversible and single-scope by the policy, it stays queued for a human, never executed by default.
That blast-radius classification, tested and enforced on every remediation decision, is exactly what we ship in the ThreatClaw policy pack: ready-to-use approval gates covering the most common remediation actions, tested in CI before every release.
Related articles
We detonated a live Phobos sample. Here is what it does, deleting shadow copies, killing the firewall, and the Sigma rule that catches it, validated across multiple samples with zero false positives.
Emotet, a notorious botnet and malware loader, remains a critical threat to SMBs. Learn how ThreatClaw’s new YARA rules help MSSPs detect and mitigate this persistent adversary.
Drokbk, a remote access trojan, evades defenses with keylogging and screen capture. ThreatClaw now detects it with zero false positives—protect SMBs and MSSP clients.
Dosia malware targets SMBs with obfuscation and tool transfer tactics. Learn how ThreatClaw’s new YARA rules help MSSPs detect and mitigate this threat effectively.