AI News HubLIVE
In-site rewrite6 min read

Block AI coding agents from shipping insecure/expensive Terraform

ops0 is a guardrail tool that sits between AI coding agents (like Claude Code, Codex, and Gemini CLI) and your infrastructure. It validates, lints, policy-checks, security-scans, and cost-estimates the entire working directory at the end of each agent turn, and blocks destructive commands before they run. This ensures only compliant and budget-friendly infrastructure changes proceed.

SourceHacker News AIAuthor: sureshpaulchamy

Notifications You must be signed in to change notification settings

Fork 3

Star 64

BranchesTags

Open more actions menu

Folders and files

NameName

Last commit message

Last commit date

Latest commit

History

122 Commits

122 Commits

.github

.github

cmd/ops0

cmd/ops0

internal

internal

.gitignore

.gitignore

.goreleaser.yaml

.goreleaser.yaml

LICENSE

LICENSE

README.md

README.md

go.mod

go.mod

go.sum

go.sum

install.sh

install.sh

Repository files navigation

Policy, lint, vulnerability, and cost guardrails for AI coding agents.

Sits in front of Claude Code, Codex and Gemini CLI. Every time the agent finishes writing IaC, the whole working directory gets validated, linted, policy-checked, security-scanned, and cost-estimated server-side. Failures come back as a failed tool call so the agent self-remediates. Destructive commands are blocked before they run.

curl -fsSL https://raw.githubusercontent.com/ops0-ai/ops0-cli/main/install.sh | sh

Quick start · What runs at end-of-turn · How agents trigger it · Monorepos · ops0-scan.md · Commands

Why this exists

When humans write infrastructure, policy gates fire on the PR. CI runs the scanner, someone gets paged, the PR sits in review for hours.

When an agent writes infrastructure, that loop is broken. The agent will happily generate a public S3 bucket, an open security group, or an oversized EC2 fleet. By the time CI catches it, the agent has moved on.

ops0 sits in front of the agent. It runs your organization's checks when the agent finishes a turn, blocks destructive commands before they execute, tells the agent how much its IaC will cost per month, and gates the change against the project's budget.

What it does

Validates at end-of-turn, not per file save A Stop hook fires when the agent finishes its turn. ops0 validate runs once against the complete working directory. Validating mid-construction was noisy on half-written modules; validating the finished module is clean.

Blocks destroy commands A PreToolUse hook intercepts terraform destroy, tofu destroy, oxid destroy and the -destroy variants — before they run. Override with OPS0_ALLOW_DESTROY=1.

Enforces project budgets If the cost estimate exceeds a project budget set in the ops0 dashboard, the gate fails. Agent gets told to optimize.

Writes a fresh report file ops0-scan.md is rewritten at the repo root after every run, so the agent reads one file to know the current state across all stages.

Speaks MCP ops0 mcp serve exposes list_policies and check_compliance to any MCP-compatible agent. Registered automatically with Claude Code on ops0 init.

Multi-project aware Walks up and down from the workspace root to find the nearest .ops0/config.json (up to 12 levels deep). One repo with many subprojects each maps to its own ops0 project.

Audit trail Every failed lint finding, policy violation, vulnerability, blocked destroy, and budget overrun is recorded against your API key in Settings → API Keys → Activity.

Works from any workspace root Hooks install at both project-level and user-level, so they fire no matter where Claude Code is opened.

Quick start

1. Install

curl -fsSL https://raw.githubusercontent.com/ops0-ai/ops0-cli/main/install.sh | sh

2. Auth: get a key at https://brew.ops0.ai/settings?tab=api-keys

ops0 login --api-base https://brew.ops0.ai

3. Bind a repo (or any subdir) to an ops0 project

cd ~/work/my-terraform-repo ops0 init --project=

4. Open Claude Code (or Codex / Gemini) anywhere in or above the repo.

Write Terraform freely. When the agent stops, the gate fires once

against the whole module. Try terraform destroy and watch it get

blocked before it runs.

Verify the wiring:

$ ops0 policies list NAME CATEGORY SEVERITY DESCRIPTION no-public-s3 security high S3 buckets must not be public require-encryption-at-rest security medium All storage must use customer-managed keys tag-required-cost-center tagging low Every resource must carry a cost-center tag

$ ops0 validate . ops0 validate . (4 files, 8.2s)

✓ Configuration is valid

tflint: 0 error(s), 2 warning(s), 0 notice(s) [WARNING] terraform_required_providers: Missing version constraint for provider "aws"

scan: 14 passed, 8 failed (0 parsing errors). Severity: 1C / 1H / 6M / 0L [CRITICAL] no-public-s3: S3 bucket has public read access [HIGH] require-encryption: S3 bucket is missing default encryption ...

cost: $284.50 / month across 6 resource(s) $148.92 aws_db_instance.app (aws_db_instance) $89.71 aws_instance.api (aws_instance) ...

budget: ✓ $284.50/mo within project limit of $500.00/mo.

What runs at end-of-turn

A single ops0 validate call fans out to five server-side stages, in this order. The CLI returns the merged result; failures gate the agent.

# Stage Catches Fails the gate when...

1 Syntax validation parse errors, undefined variables, wrong attribute types terraform validate returns invalid

2 Lint (provider-aware) wrong instance types, deprecated args, missing version constraints lint errors (warnings/notices report only)

3 Policies + vulnerabilities your org's compliance rules, security findings (public buckets, open SGs, IMDSv1, unencrypted volumes, missing tags, etc.) any finding at or above --scan-fail-on (default high)

4 Cost estimate monthly cost of all priced resources informational unless step 5 triggers

5 Project budget per-project monthly limit from the ops0 dashboard enabled AND exceeded AND Block Deployments on Exceed is on

All five run in one HTTPS call. With the server-side provider cache warm, end-of-turn validation is ~5-12s.

How agents trigger it

ops0 init writes hooks at both project- and user-level .claude/settings.json, so the gate fires regardless of which directory Claude Code is opened at.

┌──────────────────────────────────────────────┐ │ Claude Code / Codex / Gemini writes │ │ .tf / .tofu / .hcl / .tfvars files │ │ in any order. No mid-turn gate fires. │ └──────────────────────┬───────────────────────┘ │ │ Agent finishes its turn ▼ Stop hook (.claude/settings.json) │ ▼ ┌─────────────────────────────────────────┐ │ ops0 validate │ │ (walks up AND down from workspace │ │ root, up to 12 levels deep, to │ │ find the nearest .ops0/config.json) │ └─────────────────┬───────────────────────┘ │ HTTPS (API key) ▼ ┌─────────────────────────────────────────┐ │ ops0 platform │ │ - syntax validate │ │ - lint │ │ - policies + vulnerabilities │ │ - cost │ │ - project budget │ └─────────────────┬───────────────────────┘ │ ▼ ┌─────────────────────────────────────────┐ │ exit 0 → turn ends normally │ │ exit ≠ 0 → hook fails → │ │ agent gets stderr, takes another turn │ │ to remediate. │ │ │ │ Either way: ops0-scan.md is rewritten │ │ and Activity rows land in the │ │ dashboard. │ └─────────────────────────────────────────┘

The user never has to ask "did you run the scan?". The gate is mechanical.

Destructive command blocking

When the agent tries to run terraform destroy / tofu destroy / oxid destroy (or any -destroy variant) via Bash, the PreToolUse hook fires before the command runs:

agent calls Bash with terraform destroy -auto-approve │ ▼ PreToolUse hook reads the command ▼ matches the destroy pattern ▼ POSTs an audit row to ops0 ▼ prints the block message to stderr ▼ exit 2 → Claude Code aborts the Bash call

To intentionally tear something down (sandbox, dev env), prefix with the override:

OPS0_ALLOW_DESTROY=1 terraform destroy

The override is still logged to the audit trail.

This hook is separate from the validate pipeline — it's a runtime safety gate, not a write-time gate.

Monorepos

One repo, many subprojects, each with its own policies and budget? Open Claude Code anywhere in or above any of them. The Stop hook resolves the binding automatically:

my-monorepo/ ← Claude Code opened here ├── iaas/ │ └── terraform/ │ └── live/ │ └── env/ │ └── prod/ │ └── customer/ │ ├── alpha/ │ │ ├── .ops0/config.json ← projectId: alpha │ │ └── main.tf │ └── beta/ │ ├── .ops0/config.json ← projectId: beta │ └── main.tf └── shared/ ├── .ops0/config.json ← projectId: shared └── main.tf

How it finds the binding:

Walk up from the workspace's current directory looking for the nearest .ops0/config.json. If found, that's the target.

Otherwise walk down the workspace tree up to 12 levels deep and take the nearest descendant. Pruned automatically: node_modules, .git, .terraform, dist, build, .next, .venv, venv. The scan stays fast on workspaces with large vendored trees.

So you can open Claude Code at ~/my-monorepo/ and edit iaas/terraform/live/env/prod/customer/alpha/main.tf — the Stop hook walks down through 9 levels of directories, finds alpha/.ops0/config.json, and validates against the alpha project's policies + budget. Same edit in beta/ resolves to the beta project independently.

12-level depth covers any realistic IaC layout. If you need deeper, file an issue with your path shape and we'll bump it.

The ops0-scan.md report

After every ops0 validate run, the CLI rewrites a markdown file at the bound repo root:

/ops0-scan.md

It contains:

Generated timestamp + CLI version

Summary table (validate / lint / policies / cost / budget — one row each)

terraform validate errors, if any

Lint findings table

Failed policy + vulnerability findings table, ranked by severity

Cost breakdown, top 20 resources by monthly cost

Budget verdict (within limit / over by $X / blocked)

The file is overwritten on every run, so it's always the current truth. The agent reads it between turns to see findings without re-running validate. Don't hand-edit it (the next run will throw your changes away).

Disable with --no-report, or move it with --report path/to/file.md.

Integrations

Claude Code

ops0 init does the wiring for you:

ops0 init --project=

That single command:

Writes /.ops0/config.json to bind the directory to a project.

Installs PreToolUse (block destroys) and Stop (end-of-turn validate) hooks in /.claude/settings.json.

Installs the same hooks in ~/.claude/settings.json so they fire whatever directory Claude Code is opened at.

Appends a fenced governance section to CLAUDE.md so the agent knows the rules and how to read ops0-scan.md.

Runs claude mcp add ops0 ops0 mcp serve so the agent can call list_policies and check_compliance natively over MCP.

After upgrading the CLI, re-run ops0 init --force in the project directory to refresh the hooks, then restart your Claude Code session so it re-reads the hook config.

Codex / Gemini CLI / any MCP client

ops0 mcp serve

Run it as a stdio MCP server. Tools exposed: list_policies, check_compliance, whoami. Wire it up via your client's MCP config.

Commands

Command What it does

ops0 login Authenticate with an API key from the ops0 settings UI

ops0 init Bind the current directory to a project, install hooks, register MCP

ops0 policies list List policies in scope for the current directory's project

ops0 policies check [path] Lightweight scan (policies + vulnerabilities only, no init/lint/cost)

ops0 validate [path] Full pipeline: syntax + lint + policies + vulnerabilities + cost

ops0 mcp serve Run the MCP server over stdio

ops0 telemetry blocked-command Record a destroy attempt blocked by the PreToolUse hook

ops0 version Print version info

ops0 validate flags

Flag Default Purpose

--format pretty|json pretty Output format. JSON is for piping into other tools.

--iac-type terraform|opentofu|oxid terraform Which IaC flavor to dispatch to.

--cloud aws|gcp|azure|oracle (auto) Hint for the lint plugins.

--scan-fail-on critical|high|medium|low high Severity threshold for the policy/vulnerability gate.

--fail-on-warning false Also exit non-zero on lint warnings.

--report /ops0-scan.md Where to write the report.

--no-report false Skip writing the report file.

ops0 init flags

Flag Default Purpose

--project "" ops0 IaC project ID to bind this directory to. Get it from the dashboard.

-

[truncated for AI cost control]