After the basics, Rego becomes useful when you learn four things: default rules so every query gets an answer, set rules like deny that collect messages, the some and every keywords for loops and universal checks, and unit tests run with opa test. Together they turn a one-line allow into a reviewable policy.
Why start with a default?
In Rego, a rule that does not match produces no value at all, which is not the same as false. The Open Policy Agent documentation shows the fix: declare a default so the answer is defined.
package authz
default allow := false
allow if {
input.user == "bob"
input.method == "GET"
}
The documentation notes that without the default, a request that matches nothing leaves allow undefined. Callers then have to handle two outcomes. Declaring the default means they get one.
How do you collect reasons with a deny set?
An allow rule returns yes or no. A deny rule can return why. OPA's documentation shows partial set rules that add values to a set, and the pattern deny contains msg if is a common way to build a list of violations. Here is a constructed policy for a container spec in input:
package containers
deny contains msg if {
some c in input.containers
c.privileged == true
msg := sprintf("container %s must not be privileged", [c.name])
}
deny contains msg if {
some c in input.containers
not c.image_digest
msg := sprintf("container %s must pin its image by digest", [c.name])
}
Each rule body is an AND of its lines. Two rules with the same head are an OR. The result is a set of messages, and an empty set means the input passed. A pipeline can fail the build if the set has any members and print the messages for the author.
What do some and every mean?
Variables in Rego are existentially quantified by default, which means a rule fires if any binding makes the body true. The some x in collection form makes that explicit and reads like a loop, although it is a search.
When you need "all", use every. The documentation example checks that no app has a particular name:
no_miners if {
every app in input.apps {
app.name != "bitcoin-miner"
}
}
A common mistake is writing a rule that passes if any container is compliant, when the intent was all of them. If your sentence has the word "all" or "none" in it, reach for every or a negated search, and test it.
How do you test a policy?
Put tests in a separate file in the same directory. Test rules start with test_, and you can supply fake input with the with keyword. The OPA testing page shows this layout and the command opa test . -v, which prints PASS or FAIL for each test and a total.
package containers_test
import data.containers
test_privileged_denied if {
count(containers.deny) > 0 with input as {"containers": [{"name": "a", "privileged": true, "image_digest": "sha256:x"}]}
}
test_clean_passes if {
count(containers.deny) == 0 with input as {"containers": [{"name": "a", "privileged": false, "image_digest": "sha256:x"}]}
}
Write a failing test first for each rule. It proves the rule can fire, which matters because an undefined rule silently allows.
What trips beginners up?
Three things come up repeatedly. First, a rule that references a field missing from the input is undefined, so a typo in input.containers can make a deny rule never fire and the policy quietly allows everything. Tests with a known bad input catch that.
Second, mixing up == comparison with := assignment. Assignment declares a local variable, and comparison checks equality. Reading a rule aloud as a sentence helps: "deny with this message if some container is privileged."
Third, treating Rego like a procedural language. There is no loop order and no early return. Each rule body is a set of conditions that must all hold, and OPA works out which values satisfy them. Once that clicks, policies get shorter.
How to check or apply this
- Add a default to every top-level decision rule so callers see a consistent value.
- Return messages from deny sets, not bare booleans, so developers can fix what failed.
- Replace vague loops with
somefor "any" andeveryfor "all". - Write one passing and one failing test per rule, and run
opa test . -vin CI. - Keep policy and tests in version control and review them like code.
- Check the language reference when an unfamiliar message appears. Rego has precise rules about unsafe variables.
Where does Safeguard fit?
Safeguard supports policies and gates on findings in pull requests and CI. If you already author policy as code, the same habits of tests, review and clear failure messages apply to whatever gate you configure.
Quick check
Why does my Rego rule return nothing instead of false?
Because an unmatched rule is undefined, not false. Add a default declaration to turn that case into a defined value.
What is the difference between some and every?
some asks whether any element satisfies the condition. every asks whether all elements do. Choosing the wrong one flips the meaning of the policy.
Sources
Last reviewed: 18 October 2026.