{"slug": "show-hn-applygate-stop-coding-agents-from-running-terraform-destroy-in-prod", "title": "Show HN: Applygate, stop coding agents from running Terraform destroy in prod", "summary": "Flurit released applygate, a pre-1.0 hook for macOS and Linux that intercepts commands from AI coding agents such as Claude Code and Codex and returns allow, ask or block before execution, covering Terraform, OpenTofu, Terragrunt and the aws, gcloud, az, kubectl and helm CLIs. The tool judges each command by the environment it targets — protected, standard or disposable — blocking `terraform destroy`, `apply -auto-approve`, state changes and cloud delete commands in protected environments, and it starts in observe mode that logs would-be blocks without enforcing them. applygate uses fixed rules with no model involved by default and keeps data on the local machine, but Flurit states it is not a sandbox: an agent determined to bypass it and holding write credentials can still do damage.", "body_md": "**Stop coding agents from running `terraform destroy` in production.**\n\n- **What it does:** before an AI coding agent runs a command, applygate checks it and answers**allow** ,**ask** or**block** , with a reason the agent can act on. It covers Terraform, OpenTofu and Terragrunt, and the aws, gcloud, az, kubectl and helm CLIs.\n- **Who it is for:** teams that let Claude Code, Codex or another coding agent work in infrastructure repositories.\n- **How:** it runs on your machine as the agent's pre-tool-call hook. Fixed rules decide, with no model involved by default, and nothing leaves your machine.\n- **What it is not:** a sandbox. It catches mistakes and most injected instructions, but an agent that is set on getting around it and holds credentials that can write can still do damage.[What it does not protect against](#what-it-does-not-protect-against) .\n\napplygate is pre-1.0, for macOS and Linux.\n\n```\nbrew install Flurit/tap/applygate     # other ways to install: docs/install.md\ncd your-infra-repo\napplygate init                        # .applygate.yaml for this repository, hooks for Claude Code and Codex, a user config if you have none\napplygate check \"terraform -chdir=envs/prod destroy\"\n```\n\n- **Using Codex?** Open it and run`/hooks` once to trust the new hook. Until you do, Codex skips it without saying so.`applygate doctor` tells you whether it will run.\n- **Two config files.**`.applygate.yaml` lists the repository's environments; commit it so your team shares it. It can only add protection, and after every edit a person runs`applygate trust` , so an agent cannot quietly relabel production. Your own settings, such as observe or enforce, live in`~/.config/applygate/config.yaml` .\n- **It starts in observe mode.** It logs what it would block and blocks nothing. That includes an agent editing or removing applygate itself, so try it where that is acceptable.\n- **Then turn it on.** After a few days,`applygate report` shows what it would have stopped and`applygate suggest` proposes environments and tiers. Set`mode: enforce` in your user config and run`applygate doctor` .\n\nEvery command is judged by the environment it targets. You map directories, workspaces, backends, AWS profiles, Kubernetes contexts, GCP projects and Azure subscriptions to a tier in `.applygate.yaml` ([config](https://github.com/Flurit/applygate/blob/main/docs/config.md)): **protected** for production, **standard** or **disposable**. Anything applygate cannot place counts as protected.\n\n| Command | Protected | Standard | Disposable | \n|---|---|---|---|\n| Read-only: `plan` ,`show` ,`validate` ,`state list` | allow | allow | allow | \n| Apply a saved plan: `terraform apply tfplan` | plan check | plan check | allow | \n| Apply without a saved plan: `apply -auto-approve` | block | ask | allow | \n| Destroy: `destroy` ,`apply -destroy` ,`-replace` , a plan that deletes | block | ask | allow | \n| State changes: `state rm/mv/push` ,`force-unlock` ,`taint` | block | ask | ask | \n| Cloud deletes: `aws … delete-*` ,`s3 rb` ,`kubectl delete` ,`helm uninstall` | block | ask | ask | \n| Turning applygate off: skip-permission flags, editing hooks or applygate's files | block | block | block | \n| Git commands that rewrite files in a repository holding agent hook files (such as `.claude/settings.json` ):`checkout BRANCH` ,`pull` ,`merge` | ask | ask | ask | \n| Commands it cannot resolve: `curl … \\| bash` ,`eval \"$(curl …)\"` | ask | ask | allow | \n| Anything else that is not a plain, known command: loops, `bash -c` ,`npx` , unknown programs | ask | allow | allow | \n\n- **Plan check:** for`apply <planfile>` it reads the plan and rates what it finds as critical or high: destroyed or replaced stateful resources, removed deletion protection, internet exposure, IAM wildcards, state-backend changes. In a protected environment critical findings block and high ones ask; in a standard one critical findings ask. You can mark your own resources as critical ([policy](https://github.com/Flurit/applygate/blob/main/docs/policy.md) ).\n- **Turning applygate off** is blocked in every tier, but only a managed install makes that a guarantee ([below](#what-it-does-not-protect-against) ).\n- **Outside protected environments,** applygate still asks about a command that can run code it cannot follow if the same repository holds a protected environment.\n\napplygate reads commands the way the shell does. It follows `cd`, variables, `bash -c`, `eval`, `sudo`, `xargs`, Makefiles, scripts and base64 payloads to find what actually runs, so `grep \"terraform destroy\"` is just a grep and a destroy hidden in base64 is still a destroy. In a protected environment it also asks about anything that is not a plain command, because only plain commands are fully understood.\n\nOn 17 dangerous Terraform scenarios (our own set, so a demonstration, not a measure of coverage):\n\n| Guard | Caught | \n|---|---|\n| Best existing open-source guard we tried (default config) | 5 / 17 | \n| A competent hand-written Rego/regex policy | 14 / 17 | \n| **applygate** (protected tier) | **17 / 17** | \n\nThe hand-written policy also blocked harmless text like `git commit -m \"never run terraform destroy\"`. The scenarios are in [harness/](https://github.com/Flurit/applygate/blob/main/harness), and `applygate selftest` runs all 163 scenarios against your binary.\n\n| Agent | Status | Can show you an ask prompt | If applygate cannot run | \n|---|---|---|---|\n| Claude Code | Tested live | yes (except in `acceptEdits` ,`plan` and`dontAsk` modes) | the call is blocked (Claude Code 2.1.295 or later) | \n| Codex | Tested live | no: an ask is a block | the call runs | \n| GitHub Copilot CLI | Preview | yes | blocked; runs on a timeout | \n| Cursor | Preview | no: an ask is a block | blocked | \n| Gemini CLI | Preview | no: an ask is a block | the call runs | \n| Windsurf (Cascade) | Preview | no: an ask is a block | the call runs | \n| Devin (shell commands only) | Preview | no: an ask is a block | the call runs | \n| OpenCode | Preview | no: an ask is a block | blocked; runs if the plugin fails to load | \n| Cline | Preview | no: an ask is a block (it cancels the task) | blocked; runs on a timeout or a missing hook script | \n\n- **`applygate init`** installs the hooks for Claude Code and Codex.\n- **Preview** hooks follow each agent's documentation but have not been tested inside the agent. Install one with`applygate hook install --agent NAME` . To check it works, ask the agent to run`terraform destroy -auto-approve` in an empty directory: it should be blocked.\n- **On agents that cannot ask,** a person can allow one stopped call with`applygate approve ID` . This is off by default, because an agent on the same machine can reach your terminal too ([policy](https://github.com/Flurit/applygate/blob/main/docs/policy.md) ).\n- **On agents where the call runs if applygate cannot start,** keep the agent's own sandbox and approval settings on as a second line.\n- **MCP tools:** in Claude Code and Codex, calls to known infrastructure MCP servers (AWS, HCP Terraform, Kubernetes, gcloud, az, shell servers) are judged like the equivalent command. This has not yet been tested against the running servers ([mcp](https://github.com/Flurit/applygate/blob/main/docs/mcp.md) ).\n\nPer-agent details: [docs/hosts.md](https://github.com/Flurit/applygate/blob/main/docs/hosts.md), or `applygate hosts`.\n\n- **A check that fails** (a bad config, unreadable input, an internal error) blocks, unless every directory in the repository is configured as standard or disposable.`on_error` in your user config changes this.\n- **A decision that takes longer than 25 seconds** is blocked.\n- **If applygate cannot start at all,** the agent decides: see the table above.`applygate doctor` checks that the hook is in place.\n- **To stop it blocking,** set`mode: observe` in your user config.**To remove it,** run`applygate hook remove --agent NAME` for each agent, then`brew uninstall applygate` .\n\nA hook cannot promise that a command is never allowed. What protects you when it is wrong is a boundary around the agent: credentials that can only plan, writes behind MFA, a versioned and locked state bucket, a host sandbox and a reviewed apply path.\n\n- `applygate doctor --boundary` checks the ones it can and reports an assurance level from L0 (the hook only) to L3 (all of these in place) ([boundary](https://github.com/Flurit/applygate/blob/main/docs/boundary.md) ).\n- `applygate doctor --creds` asks your cloud accounts whether the credentials on this machine can change production without a person ([credentials](https://github.com/Flurit/applygate/blob/main/docs/credentials.md) ).\n- Stopping an agent from turning applygate off is guaranteed only with a managed, root-owned install ([managed](https://github.com/Flurit/applygate/blob/main/docs/managed.md) ); without one it is best-effort.\n\nKnown gaps are in [docs/limits.md](https://github.com/Flurit/applygate/blob/main/docs/limits.md), and what has been tested live is in [docs/capabilities.md](https://github.com/Flurit/applygate/blob/main/docs/capabilities.md).\n\n**From your terminal,** `applygate apply tfplan` judges a saved plan the way the hooks do, then runs `terraform apply` (or `tofu`). On an ask you confirm by typing the environment's name.\n\n**In CI,** the same plan check runs between `plan -out` and `apply`, as a GitHub Action or a Terragrunt `before_hook` ([docs/ci.md](https://github.com/Flurit/applygate/blob/main/docs/ci.md)):\n\n```\n- uses: Flurit/applygate@v0.6.0\n  with:\n    plan: tfplan\n    working-directory: envs/prod\n```\n\nNo model is needed. If you want a second opinion on calls the rules do not recognize (Pulumi, database clients, unknown scripts), turn on `judge:` in your user config with a model you choose. It can only make a verdict stricter, never looser ([docs/judge.md](https://github.com/Flurit/applygate/blob/main/docs/judge.md)).\n\napplygate makes no network calls by default, and logs decisions locally with secrets redacted. Three opt-in features do make network calls: the model judge, anonymous usage counts ([telemetry](https://github.com/Flurit/applygate/blob/main/docs/telemetry.md)), and live checks of your AWS identity and remote state.\n\n- [Install](https://github.com/Flurit/applygate/blob/main/docs/install.md) : installer, Homebrew, Go, signature checks, the Claude Code plugin\n- [Configuration](https://github.com/Flurit/applygate/blob/main/docs/config.md) and[policy rules](https://github.com/Flurit/applygate/blob/main/docs/policy.md)\n- [Commands](https://github.com/Flurit/applygate/blob/main/docs/commands.md)\n- [Agents](https://github.com/Flurit/applygate/blob/main/docs/hosts.md) ,[MCP servers](https://github.com/Flurit/applygate/blob/main/docs/mcp.md) ,[CI](https://github.com/Flurit/applygate/blob/main/docs/ci.md)\n- [Limits](https://github.com/Flurit/applygate/blob/main/docs/limits.md) ,[threat model](https://github.com/Flurit/applygate/blob/main/docs/threat-model.md) ,[what has been tested](https://github.com/Flurit/applygate/blob/main/docs/capabilities.md)\n- [Upgrading from an earlier release](https://github.com/Flurit/applygate/blob/main/docs/upgrading.md) ,[changelog](https://github.com/Flurit/applygate/blob/main/CHANGELOG.md) ,[roadmap](https://github.com/Flurit/applygate/blob/main/docs/roadmap.md)\n\nThe easiest way in is a scenario: a command or plan that applygate gets wrong, added to the pack with the verdict it should get ([CONTRIBUTING.md](https://github.com/Flurit/applygate/blob/main/CONTRIBUTING.md)). Questions go in [Discussions](https://github.com/Flurit/applygate/discussions). If you find a way around the guard, report it privately as [SECURITY.md](https://github.com/Flurit/applygate/blob/main/SECURITY.md) describes, not in a public issue.\n\nApache-2.0. See [LICENSE](https://github.com/Flurit/applygate/blob/main/LICENSE). Maintained by Flurit.", "url": "https://wpnews.pro/news/show-hn-applygate-stop-coding-agents-from-running-terraform-destroy-in-prod", "canonical_source": "https://github.com/Flurit/applygate", "published_at": "2026-11-12 14:08:47+00:00", "updated_at": "2026-11-12 14:22:38.133298+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "ai-safety", "developer-tools", "ai-infrastructure"], "entities": ["applygate", "Flurit", "Claude Code", "Codex", "Terraform", "OpenTofu", "Terragrunt", "Kubernetes"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/show-hn-applygate-stop-coding-agents-from-running-terraform-destroy-in-prod", "markdown": "https://wpnews.pro/news/show-hn-applygate-stop-coding-agents-from-running-terraform-destroy-in-prod.md", "text": "https://wpnews.pro/news/show-hn-applygate-stop-coding-agents-from-running-terraform-destroy-in-prod.txt", "jsonld": "https://wpnews.pro/news/show-hn-applygate-stop-coding-agents-from-running-terraform-destroy-in-prod.jsonld"}}