HowTo use Kiro hooks to enforce IaC standards
Kiro hooks are actions that trigger at specific points in the agent’s lifecycle: before or after a tool call, when a file is saved, when the agent stops, or when you manually fire them. A hook can either run a shell command or send a prompt back to the agent. They’re how you get the agent to actually follow your team’s rules, not just write code that compiles.
When Kiro writes Terraform, the default loop is: agent writes the .tf, you eyeball it, you push, CI complains, you go back to the agent. Hooks shorten that loop by running validators during the agent’s own turn. Bad code never makes it to the PR.
This post shows how to wire up four checks for Terraform work and back them with a steering document. The setup assumes apply happens in CI (Atlantis or similar). Hooks are about the local feedback loop, not gating production.
The four checks:
- terraform validate — catches invalid HCL and schema errors
- terraform fmt — small formatting issues, auto-fix
- tflint — bigger linter issues (provider-specific best practices, deprecations)
- trivy — security misconfigurations (which absorbed tfsec)
Step 1: Where things live
Kiro hooks are .kiro.hook files that live in .kiro/hooks/ inside your workspace (or ~/.kiro/hooks/ for user-level hooks that apply everywhere). Each hook is its own file with a when/then structure — what event triggers it, and what action to take.
There are two action types: runCommand executes a shell command, and askAgent sends a prompt back to the agent so it can act on the result. Some checks work better as agent prompts than shell scripts — more on that below.
Steering documents go in .kiro/steering/. We’ll create one of those too.
Here’s what we’re building:
.kiro/
├── hooks/
│ ├── terraform-fmt-validate.kiro.hook
│ ├── terraform-lint-scan.kiro.hook
│ ├── terraform-lint-scan.sh
│ └── block-terraform-apply.kiro.hook
└── steering/
└── terraform.md
Step 2: Auto-format and validate on save
The first hook fires whenever you save a .tf file. It uses askAgent to tell the agent to run terraform fmt and terraform validate and surface any errors.
.kiro/hooks/terraform-fmt-validate.kiro.hook:
{
"enabled": true,
"name": "Validate",
"description": "Runs terraform fmt to format and terraform validate to check syntax whenever a .tf file is saved.",
"version": "1",
"when": {
"type": "fileEdited",
"patterns": ["**/*.tf"]
},
"then": {
"type": "askAgent",
"prompt": "A .tf file was saved. Run 'terraform fmt' on the saved file, then run 'terraform validate' in its parent directory. Surface any errors."
}
}
Why askAgent and not runCommand? A shell script would just print errors. I like using askAgent here because the agent can read the output and fix issues in the same turn, which is the whole point. The enabled flag also lets you toggle hooks on and off without deleting them.
Step 3: Lint and security scan at end of turn
Running tflint and trivy after every single file save is overkill — they’re slower and the agent often writes multiple files in a turn. Better to run them once when the agent finishes using the agentStop event, which fires at the end of every agent response.
This hook uses askAgent to check whether any .tf files were touched, and if so, calls a shell script that runs both linters. The script lives alongside the hook files.
.kiro/hooks/terraform-lint-scan.sh:
#!/usr/bin/env bash
set -euo pipefail
# Find all directories containing .tf files
tf_dirs=$(find . -name '*.tf' -not -path '*/.terraform/*' -exec dirname {} \; | sort -u)
if [ -z "$tf_dirs" ]; then
echo "No .tf files found, skipping."
exit 0
fi
for dir in $tf_dirs; do
echo "==> Running tflint in $dir"
tflint --chdir="$dir"
echo "==> Running trivy config scan on $dir"
trivy config "$dir"
done
And the hook definition at .kiro/hooks/terraform-lint-scan.kiro.hook:
{
"enabled": true,
"name": "Terraform Lint & Scan",
"description": "Runs tflint and trivy config scan on all directories containing .tf files once the agent finishes its turn.",
"version": "1",
"when": {
"type": "agentStop"
},
"then": {
"type": "askAgent",
"prompt": "Check if any .tf files were created or modified during this session. If yes, run: bash .kiro/hooks/terraform-lint-scan.sh and surface the findings. If no .tf files were touched, do nothing and skip silently."
}
}
If either linter finds something, the agent surfaces the findings and can fix them in the same turn.
Very handy.
I tested this with tflint v0.55+ and trivy v0.58+. Both have to be installed locally. On macOS: brew install tflint trivy.
Step 4: Discourage local apply
Local apply skips CI, skips review, skips state locking conventions. This hook is advisory, not a hard block — see the note at the end — but it catches the common case.
It uses preToolUse on shell commands with an askAgent prompt. The agent checks the command before it runs and refuses if it’s an apply or destroy.
.kiro/hooks/block-terraform-apply.kiro.hook:
{
"enabled": true,
"name": "Block Terraform Apply",
"description": "Prevents terraform apply and terraform destroy from being run locally. Push the PR and let CI handle it.",
"version": "1",
"when": {
"type": "preToolUse",
"toolTypes": ["shell"]
},
"then": {
"type": "askAgent",
"prompt": "Check if the command being executed contains 'terraform apply' or 'terraform destroy'. If it does, STOP and do NOT run the command. Instead, tell the user: 'terraform apply/destroy is blocked locally. Push the PR and let CI handle it.' If the command does not contain terraform apply or destroy, proceed normally."
}
}
terraform plan still works, so the agent can preview changes. Hopefully Kiro will add a proper denyCommand action at some point so we don’t have to rely on the agent being well-behaved for this.
Step 5: A steering document with your team’s IaC standards
Hooks catch mechanical errors. The steering document is where you encode the things linters can’t catch — your team’s conventions, naming patterns, what modules to use. Put it at .kiro/steering/terraform.md in your workspace.
A starting point for what to put in there. Adjust to your team’s actual standards:
# Terraform standards
## File and resource conventions
- One resource per file when possible. Group tightly-related resources
(e.g., a Lambda function and its IAM role) in the same file.
- Resource names use snake_case and start with a noun describing what
the resource is, not what it does. `vpc_main`, not `main_vpc`.
- All variables have `type` and `description`. Sensitive variables have
`sensitive = true`.
- Provider versions are pinned with `~>` in `versions.tf`. Never `>=`.
## Modules
- Use the company `terraform-aws-eks` module for EKS, do not declare
raw `aws_eks_cluster` resources.
- Use the company `terraform-aws-s3-secure` module for any S3 bucket.
- Modules sourced from the internal registry, never from `github.com`
directly.
## Required tags
- Every resource that supports tags must have: `Owner`, `Environment`,
`ManagedBy = "terraform"`, `CostCenter`, `Repository`.
- Use the `default_tags` block in the provider when possible.
## Things to never do
- No public ACLs on S3 buckets (`acl = "public-read"` etc.).
- No `0.0.0.0/0` ingress on security groups except for resources
explicitly tagged `Public = "true"`.
- No hardcoded AWS account IDs or ARNs. Use `data "aws_caller_identity"`
and `data` sources for ARNs.
- No `aws_iam_policy` declared inline outside of approved modules.
## Workflow
- After modifying any .tf file, validate with `terraform validate`.
If it fails, fix it before reporting back to the user.
- After making meaningful changes to a directory, run `tflint` and
`trivy` on it and surface findings.
- Never run `terraform apply` or `terraform destroy` locally.
Apply is handled in CI.
Without a steering doc the agent writes generic Terraform. With one it writes like someone who actually read your wiki.
A note on CI
Run terraform fmt, tflint, and trivy in CI as a backstop. Hooks can be bypassed: they can be disabled with the enabled flag, they only fire when changes go through Kiro’s tools or editor saves, and an agent following askAgent prompts is ultimately advisory — it’s an LLM, not a hard gate. CI is the boundary that actually has to hold.
The point of hooks isn’t to replace CI. It’s faster feedback. Catch the issue during the agent’s turn while it’s still in context, not 20 minutes later in PR review when you’ve moved on to something else.
That’s it.