Policy Configuration
Store policies in YAML or JSON files under policy.d/, or set ATMOS_POLICY_DIR to another directory.
Policy documents are separate from atmos.yaml and stack configuration.
Directory
export ATMOS_POLICY_DIR=/trusted/policy.d
ATMOS_POLICY_DIR- Overrides the default
policy.d/directory. An unset or empty value uses the default. A missing default directory means no directory policies; a missing explicit override is an error. Use an absolute protected path in CI.
Default discovery
The default policy.d/ is found next to the configuration that Atmos selects for atmos.yaml, so
running Atmos from a repository subdirectory loads the same project root's policies. Atmos checks
these directories in order and uses the first one that contains a policy.d/:
- The directories of
--configfiles and--config-pathdirectories. Without those flags, theATMOS_CONFIGandATMOS_CONFIG_PATHenvironment variables. A flag takes precedence over the environment variable. - The directory named by
ATMOS_CLI_CONFIG_PATH. A path to a file resolves to the file's directory. - The working directory (after
--chdir). - When none of the above selected a location and the working directory has no Atmos configuration
(
atmos.yaml,.atmos.yaml,.atmos/,atmos.d/,.atmos.d/), the Git repository root, unlessATMOS_GIT_ROOT_BASEPATH=false. Outside a repository, the nearest parent directory containingatmos.yamlor.atmos.yaml.
A working directory with its own Atmos configuration is its own project root, so a policy.d/ in a
parent directory or at the Git root does not apply to it.
ATMOS_POLICY_DIR has no flag equivalent and always overrides this discovery.
| Loading rule | Behavior |
|---|---|
| File types | Regular .yaml, .yml, .json files; other extensions are ignored |
| Order | Filename order, then rule-name order within each document |
| Documents | Exactly one document per file; maximum 1 MiB per file |
| Subdirectories | Not searched |
| Templates and imports | Not expanded; documents contain literal values |
| Invalid directory | An explicit missing directory, an unreadable directory, or an existing directory containing no rules blocks the command |
| Invalid document | Unknown fields, duplicate rule identities, invalid expressions, and unsupported field values block the command |
Document schema
apiVersion: atmos/v1
kind: Policy
metadata:
name: limits
description: Limit the number of managed resources changed by one Terraform plan.
spec:
policies:
limit-changes:
on: terraform.plan
engine: cel
action: deny
reason: Too many managed resources change in this plan
with:
maximum: 10
rule: >-
has(input.resource_changes) && input.resource_changes.filter(r,
r.mode == 'managed' && r.change.actions != ['no-op']).size() > params.maximum
apiVersion- Required string. Must be
atmos/v1. kind- Required string. Must be
Policy. metadata.name- Required nonempty string. Namespaces rule and hook identifiers. The example rule is
identified as
limits/limit-changes. metadata.description- Optional plain-English string describing the policy document's purpose. Multiline text is accepted. It is descriptive metadata, not a CEL expression, template, or denial reason.
spec.policies- Required nonempty map of rule names to rule definitions.
spec.hooks- Optional map of hook names to governing hook definitions. These definitions do not merge with component hooks.
spec.tools- Optional map of tool names to version strings for hook dependencies. Installation uses these pins, not component tool overrides. Conflicting pins across documents are errors.
Rule fields
on- Required string:
terraform.plan,operation,atmos.affected,function,source,command, orcomponent.command. Selects the input kind. engine- Optional string:
cel(default) orstarlark. rule- Required CEL expression or Starlark function body returning a boolean. True selects the action; false makes the rule not applicable. Maximum source size: 64 KiB.
action- Required string.
denyblocks the operation when selected.hooksselects hook requirements and is valid only withon: operation.require_approvalselects GitHub review requirements. approvals- Required nonempty list for
action: require_approval; forbidden for other actions. Each entry specifiesid,provider: github-actions, and a provider-ownedspecwithreviewersandminimum. reason- Optional static Markdown string explaining why the rule blocks an operation and, if useful,
what to do instead. When a rule denies an operation, the CLI displays its reason with
the blocking rule ID. Use Markdown for paragraphs, emphasis, inline code, lists, and links;
use a YAML literal block (
|-) to preserve paragraph and list breaks. Omit it to keep the default denial message. Atmos does not evaluate expressions, expand templates, or insert input values into them. JSON decisions retain the text infindings[].effects.reason. with- Optional JSON-compatible object exposed as
paramsin CEL orctx.paramsin Starlark. Default: an empty object. match- Optional instance selector. A matching instance receives the rule; other instances are not applicable. Omit it to evaluate all requests of the selected input kind. See instance attachments below.
required_ci- Optional list of CI input requirements, checked before the predicate. Missing or incomplete required evidence blocks evaluation.
hooks- Required nonempty list for
action: hooks; invalid withaction: deny. See hook requirement fields.
Instance attachments
Use match with plan and lifecycle rules. Affected rules instead inspect their
before/after inventory in the expression.
Attach a central rule to instances using their resolved metadata.tags and metadata.labels.
Stack defaults and component inheritance supply that metadata through the existing stack merge.
apiVersion: atmos/v1
kind: Policy
metadata:
name: production
spec:
policies:
protect-releases:
on: operation
match:
component_types: [helm]
labels:
stage: prod
action: deny
reason: Production releases must not be deleted
rule: input.event == 'before.helm.delete'
An instance with metadata.labels.stage: prod receives this policy regardless of its stack name.
The selector runs before the predicate. Missing instance context or unresolved selector metadata
blocks evaluation rather than silently detaching the policy. Selector collection does not execute
YAML functions or templates to obtain missing data; use materialized tags and labels. This holds at
every gate. The saved-plan gate reads the instance after Atmos resolves inherited metadata, but
lifecycle events read the component section before any template runs, so a label such as
stage: "{{ .vars.stage }}" selects nothing and blocks the lifecycle gate until it is a literal value.
match.labels- Map of string label keys and values. Every entry must match.
match.tags- List of tags. Use
!any [pci, sensitive]to match at least one or!all [pci, sensitive]to require every tag. A plain list defaults to any. match.components,match.stacks,match.component_types- Lists of exact names. At least one value in each supplied list must match; no glob expansion.
match:
labels:
stage: prod
tags: !any [pci, sensitive]
Use tags: !all [pci, sensitive] to require both tags. These annotations are literal policy
selector syntax, not executable YAML functions. Labels still all match and combine with tags using AND.
The separate tag_mode field is not accepted in policy documents.
Selector fields combine with AND. An empty selector matches every available instance. Selectors apply at the documented plan and lifecycle gates; they do not introduce new command interception points. Removing a label can change applicability, so protect metadata and inherited defaults with the same review controls as policy definitions.
Enforcement
For a mandatory CI gate:
- Check out the governing policies from a protected revision into a separate directory.
- Set
ATMOS_POLICY_DIRin the trusted workflow and run Atmos with the required rules loaded. - Require the policy-running job to pass before merging. Do not use
continue-on-errorfor enforcement. - Protect policy files, workflow definitions, tool binaries, hook dependencies, and their configuration
with required review. Protect
CODEOWNERSitself when it supplies ownership rules.
Discovery does not establish trust. A caller who can redirect the policy directory (including through the config flags and variables above, or the working directory), delete or modify its contents, alter instance metadata, modify the workflow, or run the underlying tool directly can bypass this gate.
A policy engine governs the process it runs in. Whoever shapes that process (its environment variables,
working directory, binaries, and the files it reads) also shapes which policies load and how they apply.
The same is true of any policy engine. Treat the runtime as part of the policy: point ATMOS_POLICY_DIR
at an absolute, protected path, and enforce mandatory gates from a trusted CI job.
Source rules can declare allowed_roots for canonical filesystem containment. See
source policies. Validate the bundle with atmos policy check;
policy commands also provide listing, schemas, and offline fixture tests.