Skip to main content

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/:

  1. The directories of --config files and --config-path directories. Without those flags, the ATMOS_CONFIG and ATMOS_CONFIG_PATH environment variables. A flag takes precedence over the environment variable.
  2. The directory named by ATMOS_CLI_CONFIG_PATH. A path to a file resolves to the file's directory.
  3. The working directory (after --chdir).
  4. 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, unless ATMOS_GIT_ROOT_BASEPATH=false. Outside a repository, the nearest parent directory containing atmos.yaml or .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 ruleBehavior
File typesRegular .yaml, .yml, .json files; other extensions are ignored
OrderFilename order, then rule-name order within each document
DocumentsExactly one document per file; maximum 1 MiB per file
SubdirectoriesNot searched
Templates and importsNot expanded; documents contain literal values
Invalid directoryAn explicit missing directory, an unreadable directory, or an existing directory containing no rules blocks the command
Invalid documentUnknown fields, duplicate rule identities, invalid expressions, and unsupported field values block the command

Document schema​

policy.d/limits.yaml
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, or component.command. Selects the input kind.
engine
Optional string: cel (default) or starlark.
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. deny blocks the operation when selected. hooks selects hook requirements and is valid only with on: operation. require_approval selects GitHub review requirements.
approvals
Required nonempty list for action: require_approval; forbidden for other actions. Each entry specifies id, provider: github-actions, and a provider-owned spec with reviewers and minimum.
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 in findings[].effects.reason.
with
Optional JSON-compatible object exposed as params in CEL or ctx.params in 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 with action: 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.

policy.d/production.yaml
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:

  1. Check out the governing policies from a protected revision into a separate directory.
  2. Set ATMOS_POLICY_DIR in the trusted workflow and run Atmos with the required rules loaded.
  3. Require the policy-running job to pass before merging. Do not use continue-on-error for enforcement.
  4. Protect policy files, workflow definitions, tool binaries, hook dependencies, and their configuration with required review. Protect CODEOWNERS itself 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.

Policies inherit the trust of their runtime

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.