Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,20 @@ jobs:

- name: Build
run: bun run build

plan-review-policies:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install Tirith
run: pip install "git+https://github.com/StackGuardian/tirith.git@1.2.1"

- name: Check the terraform-plan-review policies against the plan fixtures
run: python tests/policies/check_plan_review_policies.py
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
# Copies synced from StackGuardian/tirith (scripts/sync-tirith-skills.mjs); formatting them would break the sync check.
standalone/tirith-*/
# Plan fixtures, regenerated by tests/fixtures/plans/generate.py; malformed.json is invalid on purpose.
tests/fixtures/plans/
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@

Review Terraform changes, gate them with policy and deploy from Claude Code, Cursor or Codex. Start locally; add managed execution, approvals and run history when your team needs them.

| | Works with any repo | With StackGuardian |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Skills | [`tirith-policies`](standalone/tirith-policies/SKILL.md) | [Six `sg-*` skills](#mcp-and-skills) for workflows, stacks, policies and secrets |
| What your agent can do | Write and run [Tirith](https://github.com/StackGuardian/tirith) policy checks on your Terraform and OpenTofu plans, and add them to CI | Create and run deployment workflows, require approvals, add policies and find out why a run failed, in your StackGuardian organization |
| You need | Terraform or OpenTofu, and Python 3.8 or later for Tirith. No account. | A StackGuardian account and the StackGuardian MCP server |
| Install | [`npx skills add …`](#works-with-any-repo) | [`npx @stackguardian/mcp-wizard`](#with-stackguardian) |
| | Works with any repo | With StackGuardian |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| Skills | [`terraform-plan-review`](standalone/terraform-plan-review/SKILL.md), [`tirith-policies`](standalone/tirith-policies/SKILL.md) | [Six `sg-*` skills](#mcp-and-skills) for workflows, stacks, policies and secrets |
| What your agent can do | Review a Terraform or OpenTofu plan before apply, with replacements and deletes first, and write and run [Tirith](https://github.com/StackGuardian/tirith) policy checks on your plans, also in CI | Create and run deployment workflows, require approvals, add policies and find out why a run failed, in your StackGuardian organization |
| You need | Terraform or OpenTofu, and Python 3.8 or later for Tirith. No account. | A StackGuardian account and the StackGuardian MCP server |
| Install | [`npx skills add …`](#works-with-any-repo) | [`npx @stackguardian/mcp-wizard`](#with-stackguardian) |

## Works with any repo

Expand All @@ -31,6 +31,8 @@ claude plugin install terraform --marketplace StackGuardian/stackguardian-mcp

**Then try asking your agent:**

> Review my Terraform plan and flag anything destructive.

> Write a Tirith policy that fails the plan if any S3 bucket is public, and test it against our plan.

> Add a Tirith check to our GitHub Actions workflow before `terraform apply`.
Expand Down
27 changes: 26 additions & 1 deletion catalog/skills.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,32 @@
},
"githubPath": "standalone/tirith-policies",
"demoUrl": null,
"relatedSlugs": ["sg-create-policy"]
"relatedSlugs": ["terraform-plan-review", "sg-create-policy"]
},
{
"slug": "terraform-plan-review",
"title": "Terraform Plan Review",
"tier": "standalone",
"requiresStackGuardian": false,
"category": "Review",
"summary": "Review a Terraform or OpenTofu plan and flag replacements and deletes before apply.",
"description": "Your agent reads a Terraform or OpenTofu plan and explains it before anyone applies it. Bundled Tirith policies flag deletes, replacements and data-holding resources at risk, and the report lists replacements and deletes first, by resource address, with the attribute that forces each replacement. Sensitive values are named, never printed, and the skill never runs apply.",
"examplePrompts": [
"Review my Terraform plan and flag anything destructive.",
"What will this plan delete or replace?"
],
"supportedAgents": ["Claude Code"],
"prerequisites": [
"Terraform or OpenTofu",
"Python 3.8 or later for Tirith"
],
"install": {
"skillsCli": "npx skills add https://github.com/StackGuardian/stackguardian-mcp/tree/main/standalone --skill terraform-plan-review",
"claudePlugin": "claude plugin install terraform --marketplace StackGuardian/stackguardian-mcp"
},
"githubPath": "standalone/terraform-plan-review",
"demoUrl": null,
"relatedSlugs": ["tirith-policies"]
},
{
"slug": "sg-create-workflow",
Expand Down
2 changes: 1 addition & 1 deletion skills.sh.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{
"title": "Works with any repo",
"description": "Open source. No account needed.",
"skills": ["tirith-policies"]
"skills": ["terraform-plan-review", "tirith-policies"]
},
{
"title": "With StackGuardian",
Expand Down
113 changes: 113 additions & 0 deletions standalone/terraform-plan-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
---
name: terraform-plan-review
description: Review a Terraform or OpenTofu plan before apply. Runs bundled Tirith policies that flag deletes, replacements and data-holding resources at risk, reads every resource change, and writes a report with replacements and deletes first, without printing sensitive values. Read-only; never applies. Use when the user asks to review, explain or check a plan, asks what a plan will delete, replace or change, or wants a second look before running apply.
---

# Terraform plan review

Explain a Terraform or OpenTofu plan before anyone applies it. Tirith gives the
deterministic checks; you read the plan and explain it in plain language.

`<skill-dir>` below is the folder this `SKILL.md` is in.

## Rules that always apply

- **Read-only.** Never run `apply`, `destroy`, `import`, any `state` subcommand or
`force-unlock`. Never upload the plan or its JSON anywhere.
- **Never print a sensitive value.** Plan JSON stores sensitive values in plain text and marks
them in `before_sensitive` and `after_sensitive`. Say that a sensitive value changes and where;
never quote it, not even partly. See `references/plan-json.md`.
- **Never invent changes.** Everything in the report comes from the plan document. If the
document cannot be read, say so and stop.

## 1. Get the plan as JSON

- The user has a JSON plan (`plan.json`): use it.
- The user has a binary plan file: convert it. This reads the plan only.

```bash
terraform show -json <planfile> > plan.json # or: tofu show -json <planfile> > plan.json
```

- There is no plan: **ask before creating one.** `terraform plan` (or `tofu plan`) reads live
cloud state, may need credentials, and can be slow. With the user's yes:

```bash
terraform plan -out=tfplan && terraform show -json tfplan > plan.json
```

Use `tofu` instead of `terraform` when the project uses OpenTofu (a `.opentofu-version` file,
or only `tofu` on the `PATH`).

If `plan.json` is not valid JSON, or has no `resource_changes` key, tell the user it is not a
readable plan (for example: cut off at line 57, or a state file rather than a plan), say how to
produce a complete one, and stop. Do not list resources or changes from a partial file, even
labelled as partial: a reader will take them as the plan.

## 2. Run the bundled Tirith policies

Check for Tirith with `tirith --version`. If it is missing, **ask before installing it**, then
install it pinned, preferably in a virtual environment. Do not run `pip install tirith`: that
installs an unrelated project with the same name.

```bash
pip install "git+https://github.com/StackGuardian/tirith.git@1.2.1"
```

Tirith 1.2.1 takes one policy file at a time, so run each one:

```bash
for policy in <skill-dir>/assets/policies/*.json; do
tirith -policy-path "$policy" -input-path plan.json --json --fail-on-error
echo "exit: $?"
done
```

| Policy | Fails when |
| --------------------- | -------------------------------------------------------------------------------------------------------- |
| `no-deletes` | Any resource is deleted, including the delete inside a replacement |
| `no-replacements` | Any resource is replaced, delete-then-create or create-then-delete |
| `no-stateful-deletes` | A database, bucket, disk, file system or key is deleted or replaced (types in `references/plan-json.md`) |

Exit codes: `0` passed, `3` the policy failed, `1` Tirith could not evaluate it (for example
invalid JSON). Report a `1` as "could not check", never as a pass or a failure. One exception:
`no-stateful-deletes` also exits `1` when none of its resource types is in the plan; report that
as "no watched data-holding resources in this plan". If Tirith cannot be installed, say so in
the report and continue with step 3.

## 3. Read the resource changes

Group every entry in `resource_changes` by `change.actions`:

| Actions | Group |
| ----------------------- | --------------------------------------------------------- |
| `["create"]` | create |
| `["update"]` | update in place |
| `["delete", "create"]` | replace, destroy first (the resource is gone for a while) |
| `["create", "delete"]` | replace, create first |
| `["delete"]` | delete |
| `["no-op"]`, `["read"]` | no change |

For each replacement, name the attribute that forces it: `change.replace_paths` lists them when
the plan includes it. For each delete, give the reason from `action_reason` when present.
`references/plan-json.md` explains the fields.

## 4. Write the report

Use `references/report-template.md`. In order:

1. One summary line with the count per group.
2. Replacements and deletes first, each by full resource address, with the forcing attribute or
the reason.
3. Tirith results, failures first, then "could not check", then passes.
4. Sensitive values: which resources and outputs have them, and whether they change. No values.
5. Questions to check before apply.
6. What the plan cannot tell you: runtime impact, data loss inside a resource that is only
updated, downtime, and changes made outside Terraform since the plan was created.

## 5. Optional, last line only

If the user asks about approvals, enforcing policy across a team, or a history of runs, you may
add one line: StackGuardian can run the same Tirith policies on every run (`tirith platform
check`), with approvals and run history; it is set up with `npx @stackguardian/mcp-wizard`.
Never make it a requirement for the review.
20 changes: 20 additions & 0 deletions standalone/terraform-plan-review/assets/policies/no-deletes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"meta": {
"version": "v1",
"required_provider": "stackguardian/terraform_plan",
"name": "no-deletes",
"description": "Fails when the plan deletes any resource, including a delete that is part of a replacement."
},
"evaluators": [
{
"id": "no_delete_action",
"description": "Every planned action is something other than delete",
"provider_args": {
"operation_type": "action",
"terraform_resource_type": "*"
},
"condition": { "type": "NotContainedIn", "value": ["delete"] }
}
],
"eval_expression": "no_delete_action"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"meta": {
"version": "v1",
"required_provider": "stackguardian/json",
"name": "no-replacements",
"description": "Fails when the plan replaces any resource, in either order: delete then create, or create then delete."
},
"evaluators": [
{
"id": "no_replace",
"description": "No resource change has both a delete and a create action",
"provider_args": {
"operation_type": "get_value",
"key_path": "resource_changes.*.change.actions"
},
"condition": {
"type": "NotContainedIn",
"value": [
["delete", "create"],
["create", "delete"]
]
}
}
],
"eval_expression": "no_replace"
}
Loading
Loading