# Execution Admission

Use admission policies to reject YAML function execution and CLI commands, or attach command
restrictions to component instances using literal tags and labels.

## Input kinds

| Kind | Gate | Input fields | Instance selectors |
|---|---|---|---|
| `function` | Before YAML function execution and Go-template calls, including configuration loading | `name`: canonical YAML name without `!` or template function/namespace; `engine`: `yaml` or `go-template` | No; instance metadata may not exist yet |
| `command` | Before a runnable CLI command's argument validator and pre-run hooks | `command`: command path string; `command_path`: array of command names, excluding `atmos`; `path`: deprecated alias of `command_path`; `destroy`: boolean, see [Destroy detection](#destroy-detection) | No |
| `component.command` | During shared stack resolution, before component template/function evaluation and tool execution | `command`: the subcommand, such as `apply` or `shell`; `command_path`: array of command names, excluding `atmos`, such as `["terraform", "apply"]` or `["secret", "shell"]`; `component_type`, `component`, `stack`, `dry_run`, `destroy` (see [Destroy detection](#destroy-detection)), `capabilities`, `agents` | Yes; literal inherited metadata |
| `auth` | Before auth-manager authentication and credential cache access | `identity`, `provider`, `identities` (the complete resolved identity chain) | No |

These gates do not require `--affected`. Function arguments and CLI argument/flag values are not
exposed: they can contain secrets. The one exception is `destroy`, a boolean Atmos derives from the arguments.
These kinds support native policy engines, denial,
and [GitHub approval requirements](/policies/approvals).

## Disable `!exec`

```yaml title="policy.d/no-exec.yaml"
apiVersion: atmos/v1
kind: Policy
metadata:
  name: no-exec
  description: Prevent configuration from executing shell commands through !exec.
spec:
  policies:
    disable:
      on: function
      action: deny
      reason: |-
        **`!exec` is disabled** because stack configuration must not run shell commands.

        **Instead:** Pass precomputed values through environment variables and read them with `!env`.
      rule: input.name == 'exec'
```

The optional `reason` appears in the CLI error when this rule blocks `!exec`. Use it to explain
the restriction and suggest an alternative. Omitting it does not change enforcement.

Every denial names the rule that blocked it, such as `function-disable-exec/guard`, whether or not
the rule has a `reason`, and exits with code `77`. Scripts and CI jobs can use that code to tell a
policy denial apart from a usage error or a failed command. Evaluation errors and pending approvals
do not use this code.

The check applies to `atmos.yaml` mappings and sequences, stack YAML functions, and registry-based
lazy evaluation. Skipped functions are not executed. Literal text containing `!exec` is not shell
execution. This rule does not disable shell hooks, automation scripts, or external tools. Go-template calls have their own function names and `engine`, described below.

## Block a command

```yaml
apiVersion: atmos/v1
kind: Policy
metadata:
  name: no-destroy
spec:
  policies:
    direct-destroy:
      on: command
      action: deny
      rule: input.command == 'terraform destroy' || input.destroy
```

CLI admission covers registered and custom runnable commands, including commands without components
or stacks. Native aliases use the canonical command path. Built-in commands are also checked before
project initialization and configuration loading. Configuration is loaded to register custom
commands before their CLI admission; use function policies to control YAML execution during that loading.
Help rendering and external programs invoked without Atmos are not command admission targets.

`command_path` holds the same command as a list of command names, which is easier to match
exactly or by prefix:

```yaml
rule: input.command_path == ['terraform', 'providers', 'lock']
```

`path` is a deprecated alias of `command_path` with the same value. Policies written for Atmos
1.234.0 that use `input.path` keep working; use `command_path` in new policies.

Admission of built-in commands runs before `atmos.yaml` loads, so a denial here cannot post commit statuses or job
summaries. When a CI provider is detected, such as GitHub Actions, Atmos reports one error annotation per denying
rule instead. It needs no CI configuration. Custom-command denials happen after configuration loads, so they
follow `ci.enabled` and `ci.annotations.enabled`:

```text
::error title=Policy no-destroy/direct-destroy denied execution::Denied by no-destroy/direct-destroy.
```

The annotation message is the rule's `reason` as plain text, or `Denied by <rule>.` when the rule has
none. These annotations have no file location, because the command is denied before any stack is read.
See [failure annotations](/cli/configuration/ci#failure-annotations) for how other failures are reported.

### Destroy detection

Matching `input.command == 'terraform destroy'` alone misses
`atmos terraform apply vpc -s prod -destroy -auto-approve`. Both `command` and `component.command`
inputs therefore carry `destroy`, a normalized boolean that Atmos computes from the arguments. Rules do
not parse flags, and no other argument value is exposed. `destroy` is always present and is `true` when:

- the command is `terraform destroy` (including the `tf` alias) or `helmfile destroy`, or
- a `terraform` command (OpenTofu included), such as `plan`, `apply`, or `deploy`, receives the `-destroy`
  or `--destroy` flag, including after the `--` separator.

A `true` value (`-destroy=true`, `--destroy=1`, `-destroy=T`) counts, and a `false` value
(`-destroy=false`, `-destroy=0`) does not. An unparseable value counts as `true`, and any `true`
occurrence wins over a later `false`. Every other command, such as `helm delete`, `kubernetes delete`, or
any non-Terraform command with a `-destroy` argument, leaves `destroy` `false`; match those by `command` or
`command_path`.

`destroy` cannot see a decision made earlier: `atmos terraform apply <planfile>` is not recognized as a destroy
when the saved plan came from `plan -destroy`. Govern the plan itself with
[Terraform plan policies](/policies/terraform-plan).

## Attach to instances

```yaml
apiVersion: atmos/v1
kind: Policy
metadata:
  name: production
spec:
  policies:
    no-delete:
      on: component.command
      match:
        labels: {stage: prod}
      action: deny
      rule: input.command in ['delete', 'destroy'] || input.destroy
```

Shared stack resolution applies this gate to Terraform, Helmfile, Helm, Kubernetes, Packer, Ansible,
container, emulator, and custom component integrations using that resolver. Commands without an
instance still receive CLI admission, not fabricated component metadata. An integration bypassing
shared stack resolution must add instance admission before advertising that capability.

Commands that resolve a component without running its tool also pass this gate under their own
command. `atmos secret shell -c vpc -s prod` is admitted with `command: "shell"` and
`command_path: ["secret", "shell"]`, and `atmos describe component` with an empty `command` and
`command_path: ["describe", "component"]`. Match `command` to cover every command with that name, or
`command_path` to target one:

```yaml
rule: input.command_path == ['secret', 'shell'] || input.command_path == ['secret', 'exec']
```

Metadata used by selectors must be literal: unresolved templates/functions or malformed metadata
produce an error, not a non-match. `capabilities` always includes `component.command`, `function`, and `source`;
Terraform also supplies `terraform.plan` and `operation`, while Helmfile, Helm, and Kubernetes supply
`operation`. A capability name does not manufacture its input at other gates.

## Enforcement configuration

The CLI loads and pins policy definitions after applying `--chdir`, before configuration loading.
Later configuration changes to the environment or working directory do not replace that in-memory
bundle. Missing default `policy.d/` remains opt-in behavior; protect an explicit `ATMOS_POLICY_DIR`
and the governing files in CI. Pinning does not establish trust in files selected from the PR itself.

Denials, evaluation failures, missing required inputs, and outstanding approvals return errors.
Dry-run does not bypass admission. See [enforcement](/policies/configuration#enforcement) for repository
protection requirements and [affected policies](/policies/affected) for semantic detachment checks.

## Go-template functions and provenance

`on: function` also covers Go-template calls. `input.engine` is `yaml` or `go-template`;
`input.name` retains the existing YAML name or identifies the template function/namespace.
For example:

```yaml
apiVersion: atmos/v1
kind: Policy
metadata:
  name: rendering
spec:
  policies:
    environment:
      on: function
      action: deny
      rule: >-
        input.engine == 'go-template' &&
        input.name in ['env', 'getenv', 'expandenv', 'env.Getenv', 'env.ExpandEnv']
    remote-exec:
      on: function
      action: deny
      rule: input.engine == 'yaml' && input.name == 'exec' && input.provenance != 'local'
```

Functions are checked when called, so an unused branch does not trigger admission. Namespace
constructors such as `net` and `aws` are gates; `file` methods, DNS lookup methods, environment
methods, and `atmos.Component`, `atmos.Store`, `atmos.Resolve`, and `atmos.GomplateDatasource`
have additional method gates. `atmos.Resolve` retains the YAML handler check too.

`provenance` is `local`, `remote`, or `unknown`. Remote cache files retain remote attribution.
Lazy YAML values retain conservative parse-time attribution across merges: identical values with
multiple origins are treated as remote if any recorded origin is remote. Synthetic or unattributed
execution contexts remain unknown; use `!= 'local'` when missing attribution must block.
This is execution provenance, not proof of a trusted author or signed source.

See [source and filesystem policies](/policies/sources) for destination restrictions and include
confinement, and [policy commands](/cli/commands/policy) for offline checks and `--explain` diagnostics.

## Block coding-agent production deployments

The [agent deployment policy](/policies/library/component-deny-agent-production-deploy)
uses the shared `env` context to deny production `apply`, `deploy`, `shell`, and `exec` commands. Atmos captures
process environment before configuration loading can change it. The example checks
`CLAUDECODE=1` or a nonempty `CODEX_THREAD_ID`. Environment values can contain secrets;
do not include them in diagnostics. See [shared context](/policies/context) for all engines.

The example identifies production by `prod`/`production` segments in stack names.
Adapt that condition to your stack convention or use inherited literal labels with `match`.
Agent markers are removable environment hints, not authenticated identity; enforce
production authorization with protected credentials and governing policy files as well.

## Deny production authentication

The [production auth policy](/policies/library/auth-deny-production) checks the entire
resolved credential chain, including intermediate identities and the root provider.
It always applies, locally and in CI, regardless of whether an agent is present.

```json
{
  "identity": "dev",
  "provider": "sso",
  "identities": ["prod-admin", "dev"]
}
```

This chain is denied even though the target is named `dev`. `input.provider` is empty
for standalone identities; direct provider login has an empty `input.identity` and
`input.identities` list. Credentials and provider configuration are never policy input.

The auth-manager gate runs before authentication or cached credential reuse, including
credential-process helpers. Cached credentials do not bypass denial. The library rule
classifies production by name segments and supports additional exact names through
`with.production_names`; adapt these to your auth configuration. It cannot revoke credentials
already exported to another process or govern direct cloud-tool calls outside Atmos auth.
