> ## Documentation Index
> Fetch the complete documentation index at: https://docs.jacobpevans.com/llms.txt
> Use this file to discover all available pages before exploring further.

# OpenTofu check placement

> Static checks in pre-commit, credentialed operations in CI via OIDC. aws-vault never appears in repo automation.

> Static checks belong in pre-commit (mirrored in CI). Credential-requiring operations belong in CI only, via OIDC. `aws-vault` never appears in hooks, workflows, Makefiles, or scripts.

The placement rules below apply to every OpenTofu / Terragrunt repo across the org. Static checks run on every commit locally and are mirrored as a CI job; anything that touches AWS runs in CI only, authenticated with OIDC.

## Canonical pattern

| Check                                                | Pre-commit (local + CI mirror) | CI only (OIDC)                | Never                  |
| ---------------------------------------------------- | ------------------------------ | ----------------------------- | ---------------------- |
| `tofu fmt -check`                                    | yes                            | mirrored                      | —                      |
| `tofu validate` (with `init -backend=false`)         | yes                            | mirrored                      | —                      |
| `tflint`                                             | yes                            | mirrored                      | —                      |
| `terraform_docs`                                     | yes                            | mirrored                      | —                      |
| `gitleaks` / `detect-private-key`                    | yes                            | mirrored                      | —                      |
| Generic hygiene (whitespace, EOL, YAML, large files) | yes                            | mirrored                      | —                      |
| `tofu plan`                                          | —                              | yes (OIDC, post via `tfcmt`)  | wrapped in `aws-vault` |
| `tofu apply`                                         | —                              | yes (OIDC, gated environment) | wrapped in `aws-vault` |
| `trivy` / `checkov` (security)                       | optional                       | dedicated job                 | wrapped in `aws-vault` |
| `terragrunt validate` / `terragrunt plan`            | — (delete the hook)            | yes (OIDC)                    | as a hook              |

## Hard rules

**No credentials in hooks, ever.** `aws-vault`, `doppler`, AWS SDK calls, and anything requiring a keychain or injected secret are forbidden in `entry:` fields, `args:`, Makefile targets, `justfile` recipes, and scripts called from any of these. Pre-commit must run from a fresh checkout with no `AWS_*` env vars set.

**`tofu validate` runs with `init -backend=false`.** No backend creds, no provider creds needed. This is how `hashicorp/terraform-provider-aws` and `opentofu/opentofu` run their own pre-commit hooks.

**Delete `terragrunt-validate` and `terragrunt-plan` hooks entirely.** Do not retain them under `stages: [manual]`. If a developer wants to run `terragrunt plan` locally, they invoke it from the dev shell outside pre-commit. Matches `gruntwork-io/terragrunt` practice (zero credentialed hooks).

**No `stages: [manual]` for static checks.** If a hook is too slow to run on every commit, fix the hook (add caching, scope to changed files). Hiding it behind `stages: [manual]` is equivalent to deleting it.

**CI auth is OIDC exclusively.** Every job that needs AWS access must use `permissions: id-token: write` and `aws-actions/configure-aws-credentials@v4` with `role-to-assume`. No long-lived `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` in repo secrets.

**No `continue-on-error: true` on format or validation steps.** Letting a broken fmt check silently pass defeats the purpose of the check.

**`aws-vault` is allowed only in:**

* READMEs and docs (user-facing examples of how to run things manually)
* `.nix` files under `nix-darwin`, `nix-home`, `nix-ai`, `nix-devenv` (aliases, package inputs)
* `nix-claude-code/data/permissions/allow.nix` (agent runtime permission — not embedded automation)

## CI output is public

GitHub Actions logs and PR comments are world-readable on public repos. Every workflow that handles real credentials or plan output must apply these controls.

**`tofu plan` output is sensitive.** It can reveal AWS account IDs, ARNs, internal IPv4/IPv6 ranges, internal hostnames, resource names, security-group rules, S3 bucket names, IAM principals, EC2 AMI IDs, Proxmox node names, VM/container counts. Do not stream raw `tofu plan -no-color` to a `run:` step without masking. Use `tfcmt --patch` (compact diff mode) when posting to PRs; gate comments to `pull_request` from same-repo branches only — never from public forks.

**Mask before logging, not after.** The first step of every job that handles real creds must `::add-mask::` the AWS account ID and any environment-derived domain or IP prefix. Read the account ID from `aws sts get-caller-identity` and pipe through `::add-mask::` *before* any other step runs.

**No `TF_LOG=DEBUG` or `TF_LOG=TRACE` in CI.** Those leak provider request and response bodies — tokens, signed URLs, full request payloads. Set `TF_LOG=ERROR` or leave unset.

**No `set -x` or `set -o xtrace`.** Bash trace mode echoes every command including variable expansions; masking only fires on stdout content, not the command line itself.

**No `tofu show -json` artifact uploads.** Plan binary files (`tfplan`) contain resolved variable values including `sensitive = true` strings. Default position: do not upload plan files; re-plan in the apply job.

**`pull_request_target` is forbidden for OpenTofu jobs.** Use `pull_request` (same-repo branches only) or `workflow_run` for fork-PR plans with output written to a downloadable artifact.

**Trivy / Checkov SARIF uploads are safe** (rule violations only), but their stdout is not — apply the same masking.

## Audit grep

Run before opening any PR that touches hooks, workflows, Makefiles, or scripts:

```bash theme={null}
git grep -nE 'aws-vault' -- ':!*.md' ':!*.nix' ':!docs/**' ':!CLAUDE.md' ':!AGENTS.md'
```

Expected output in any non-Nix repo: empty. Any match is a violation.

## Industry evidence

These patterns are adopted by every major Terraform ecosystem project:

* [`terraform-aws-modules/terraform-aws-vpc` `.pre-commit-config.yaml`](https://github.com/terraform-aws-modules/terraform-aws-vpc/blob/master/.pre-commit-config.yaml) — `fmt` / `validate` / `tflint` / `docs` on every commit, zero credentials
* [`antonbabenko/pre-commit-terraform` README](https://github.com/antonbabenko/pre-commit-terraform#readme) — all static hooks use `init -backend=false`; plan/apply hooks explicitly documented as "not recommended in pre-commit"
* [`hashicorp/setup-terraform` README](https://github.com/hashicorp/setup-terraform#readme) — OIDC is the documented path for CI; no `aws-vault` anywhere in the action
* [`gruntwork-io/terragrunt`](https://github.com/gruntwork-io/terragrunt) — zero credentialed hooks in any example

## Anti-patterns

These shapes are violations of the rule, regardless of framing.

```yaml theme={null}
# WRONG: aws-vault in a hook entry
- id: terragrunt-validate
  name: terragrunt validate
  entry: aws-vault exec tf-proxmox -- doppler run -- terragrunt validate
```

```yaml theme={null}
# WRONG: hiding a static check behind stages: [manual]
- id: terraform_fmt
  stages: [manual]
```

```yaml theme={null}
# WRONG: nix develop round-trip per commit
- id: tofu-test
  entry: nix develop github:JacobPEvans/nix-devenv?dir=shells/terraform --command bash -c "tofu test"
```

```yaml theme={null}
# WRONG: long-lived access keys in CI
env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
```

```yaml theme={null}
# WRONG: silencing a critical check
- run: tofu fmt -check
  continue-on-error: true
```

Direnv already activates the Nix dev shell on `cd` — no `nix develop` wrapper is needed inside hooks.

## Where to go next

<CardGroup cols={2}>
  <Card title="IaC tooling" icon="layer-group" href="/infrastructure/iac-tooling">
    Why OpenTofu is the engine and where Terragrunt is being phased out — the decision behind these rules.
  </Card>

  <Card title="Terraform on AWS bootstrap" icon="hammer" href="/infrastructure/terraform/aws-bootstrap">
    The admin-runnable module that creates the role this placement rule applies to.
  </Card>

  <Card title="CI/CD policy" icon="scale-balanced" href="/infrastructure/cicd/policy">
    Marketplace actions, release-please, version pinning, runner choice.
  </Card>

  <Card title="Golden laws" icon="shield-halved" href="/security/golden-laws">
    Why credentials never enter hooks and how the boundary is enforced.
  </Card>

  <Card title="aws-vault" icon="key" href="/security/tools/aws-vault">
    Where aws-vault legitimately runs — and where it never does.
  </Card>
</CardGroup>
