Policy sets check every plan against your own Rego rules before it can be applied. You upload policies, publish a version, and attach it to a workspace.
Policies run after every plan, including speculative pull-request plans. Sentinel isn’t supported.
Write a policy
Every policy set must define decision in the package stackship.v1, using
Rego v1 syntax. decision returns an object with a findings array. An
empty array means the plan passes.
package stackship.v1
import rego.v1
decision := {"findings": findings}
findings contains finding if {
some change in input.resource_changes
change.change.after.acl == "public-read"
finding := {
"id": "no-public-buckets",
"title": "Public bucket",
"message": "Buckets must not be publicly readable.",
"resource_address": change.address,
}
}Each finding has:
| Field | Required | Limit |
|---|---|---|
id |
Yes, unique within the result | 128 characters |
title |
Yes | 160 characters |
message |
Yes | 4,000 characters |
resource_address |
No | 512 characters |
details |
No | A small object for your own metadata |
A policy can return at most 500 findings. Unknown fields, a missing
decision, or an error during evaluation count as an error, which blocks the
run.
Policy input
Your policy receives the plan as JSON:
{
"version": "stackship.policy.input/v1",
"run": { "kind": "tracked" },
"source": {
"commit_sha": "0123456789abcdef0123456789abcdef01234567",
"repository_id": "123456789",
"working_directory": "."
},
"workspace": {
"id": "ws_22222222222222222222222222222222",
"metadata": { "environment": "production" }
},
"resource_changes": [
{
"address": "aws_s3_bucket.assets",
"provider_name": "registry.opentofu.org/hashicorp/aws",
"change": {
"actions": ["create"],
"before": null,
"after": { "bucket": "northwind-assets", "secret": null },
"before_sensitive": false,
"after_sensitive": { "secret": true }
}
}
]
}run.kindistracked(push to the tracked branch),speculative(pull request), ormanual(CLI or API). Manual runs have anullcommit_sha.resource_changesfollows the shape oftofu show -json.- Sensitive values are replaced with
null. Usebefore_sensitiveandafter_sensitiveto tell a redacted value from a realnull. - Your policy never sees state, variables, or credentials.
What policies can’t do
Policies run with OPA 1.18.2 and a restricted set of built-ins. Network
calls (http.send), environment and time functions, randomness, UUIDs,
cryptography, JWT functions, and print aren’t available. The same plan
always produces the same result.
Test your policy
Add *_test.rego files next to your policies. Publishing runs
opa check --strict and opa test and fails if either fails. Run them
locally first:
opa check --strict .
opa test .Publish a version
Package your .rego files and tests as a .tar.gz archive (up to 100 MB):
tar -czf policies.tar.gz *.regoThen, in the dashboard:
- Open Policies and select Create policy set. Enter a name (lowercase letters, numbers, and hyphens) and an optional description.
- On the policy set’s page, under Publish a version, choose a Build workspace and your Rego source archive, then select Publish version.
- Wait for the version to show Published. If it shows Failed, fix the source using the failure code next to the version and upload again.
| Failure code | Meaning |
|---|---|
POLICY_COMPILE_FAILED |
opa check --strict found an error |
POLICY_TEST_FAILED |
A test failed |
POLICY_BUNDLE_FAILED |
The policies couldn’t be packaged; check that stackship.v1.decision is defined |
Published versions never change. To update a policy, publish a new version, detach the old version, and attach the new one.
Attach a policy
- Open the workspace, then Settings → Policies. You can also use Attach to a workspace on the policy set’s page.
- Choose a published version and an Enforcement mode.
- Select Attach policy version.
To remove a policy, select Detach next to it and confirm.
The policy set’s Attachments list shows every organization, project, and workspace the set is attached to. Owners and admins can Detach any of them there; it’s the only place in the dashboard to remove an organization or project attachment.
Attaching and detaching affect new runs only; runs already started keep their policies.
Organization and project scope
Use the API to attach a policy set to every workspace in an organization or project:
POST /v1/policy-sets/{policy_set_id}/attachments
Content-Type: application/json
Idempotency-Key: 6f1c...
{
"policy_version_id": "pol_...",
"scope": "project",
"target_id": "prj_...",
"enforcement_mode": "hard_mandatory"
}scope is organization, project, or workspace. A policy set can be
attached only once along a workspace’s organization → project → workspace
path. An overlapping attachment fails with 409 POLICY_ATTACHMENT_OVERLAP —
for example, attaching at a project when the set is already attached to the
organization or to a workspace in that project, or attaching twice to the same
workspace. Detach the existing attachment first.
To list and detach attachments at any scope:
GET /v1/policy-sets/{policy_set_id}/attachments
DELETE /v1/policy-sets/{policy_set_id}/attachments/{attachment_id}
Idempotency-Key: 9b2e...Each attachment has its scope, target_id and target_name (both null at
organization scope), version, and enforcement mode. Detaching requires owner
or admin, returns 204, and returns 404 if the attachment is already
detached. To change an attachment’s version or scope, detach it and attach
again.
Enforcement modes
| Mode | When the policy returns findings |
|---|---|
| Advisory | Findings are shown on the run; the run continues |
| Soft mandatory | Approval is blocked until someone overrides the finding |
| Hard mandatory | The run fails with Policy failed; it can’t be overridden |
A policy error always blocks the run, whatever the mode. On plan-only and speculative runs, which can’t be approved, a soft-mandatory finding also ends the run as Policy failed.
Override a soft policy
Owners and admins can override a soft-mandatory finding so the plan can be approved.
- On the run page, expand the Policy stage.
- Enter an Override justification (up to 2,000 characters).
- Select Override soft policy. Repeat for each failing soft-mandatory policy.
A dashboard override expires after one hour. It applies only to that run and plan and is recorded in the audit log.
With the API, POST /v1/runs/{run_id}/policy-overrides takes the
policy_result_digest from GET /v1/runs/{run_id}/policy, a
justification, and an expires_at up to 24 hours away.
Answering the override prompt during tofu apply records an override with the
justification “OpenTofu CLI soft policy override” that expires after 15
minutes.
Read results
Results appear in the run’s Policy stage, in tofu plan and tofu apply
output, and at GET /v1/runs/{run_id}/policy.
Limits
| Item | Limit |
|---|---|
| Policy source archive | 100 MB |
| Policy sets attached to one workspace | 256 |
| Resource changes in the policy input | 5,000 |
| Policy input size | 2 MiB |
| Values in the policy input | 50,000 |
| Sensitivity markers in the policy input | 20,000 |
| Findings per policy set | 500 |
| Result size per policy set | 512 KiB |
A plan larger than these limits fails policy, and the policy log names the limit, but only when a policy set applies to the workspace. Without one, nothing reads the policy input, so plans of any size continue to approval.