# 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

```shell
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 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

```yaml title="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](/policies/affected) 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.

```yaml title="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.

```yaml
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.

:::info 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](/policies/sources). Validate the bundle with `atmos policy check`;
[policy commands](/cli/commands/policy) also provide listing, schemas, and offline fixture tests.
