TrademarkTrademark
Features
Documentation

Using Claude Code With Terraform: A Least-Privilege Setup

A concrete Claude Code configuration for Terraform and OpenTofu work: what to allow, how to block apply in layers, and why credentials are the boundary that actually holds.
Sebastian StadilJuly 21, 2026
Key takeaways
  • Let Claude Code read, edit, validate, and plan. Keep apply, destroy, and state mutation behind a human or pipeline approval, enforced in layers rather than by one setting.
  • Permission deny rules like Bash(terraform apply *) are the first layer, but Anthropic's own documentation calls argument-constraining Bash patterns fragile. Don't make them the only enforcement.
  • A PreToolUse hook that exits with code 2 blocks the tool call before it runs and feeds the reason back to the model, which makes it a stronger gate than a string matcher.
  • The layer that actually holds is credentials: give the session a backend identity that can plan but not apply, and the worst case of any bypass is a rejected API call.
  • Scope MCP servers per project, start registry-only, and delegate plan and log reading to a read-only subagent so large payloads never enter the main context.

Claude Code will happily run terraform apply if nothing tells it not to. The fix isn't one setting. It's a set of layers, each catching what the previous one misses, ending at a credential that can't apply no matter what the agent does.

This post is the concrete configuration companion to our primer on infrastructure as code with AI coding agents. Everything below was checked against Anthropic's live documentation as of July 21, 2026; Claude Code changes quickly, so re-verify the syntax before copying it into a team-wide config.

What Should Claude Code Be Allowed to Do?

Decide the policy before touching settings. A useful default for Terraform and OpenTofu work:

Allowed: read the repository, edit HCL, run fmt, validate, test on files a human has verified as plan-only, produce speculative plans, and query provider documentation through a registry MCP server.

Denied: apply, destroy, any state subcommand, import, taint, force-unlock, -auto-approve, -lock=false, and init -upgrade. Approval and apply stay with a human or a controlled pipeline.

That split matches how the tool itself is trending: as of July 2026, Claude Code's auto permission mode ships with a classifier that refuses terraform destroy and plans that destroy resources by default. Sensible, but a default someone else tunes isn't a policy. Write yours down.

How Do You Write the Permission Rules?

Permission rules live in .claude/settings.json (project, shared through Git), .claude/settings.local.json (personal), or ~/.claude/settings.json (user). Rules merge across scopes and a deny at any level wins, so a project file is a real team control rather than a suggestion. A starting point:

{
  "permissions": {
    "deny": [
      "Bash(terraform apply*)",
      "Bash(terraform destroy*)",
      "Bash(terraform state*)",
      "Bash(terraform import*)",
      "Bash(terraform taint*)",
      "Bash(terraform force-unlock*)",
      "Bash(tofu apply*)",
      "Bash(tofu destroy*)",
      "Bash(tofu state*)"
    ],
    "allow": [
      "Bash(terraform fmt*)",
      "Bash(terraform validate*)",
      "Bash(terraform plan*)",
      "Bash(tofu fmt*)",
      "Bash(tofu validate*)",
      "Bash(tofu plan*)"
    ]
  }
}

Two semantics worth knowing. Evaluation order is deny, then ask, then allow, with the first match winning, so a broad deny beats a narrow allow. And Claude Code splits compound commands on &&, ;, and pipes, matching each part separately, so terraform validate && terraform apply doesn't slip through on the strength of its first half.

Why Aren't Deny Rules Enough?

Because Anthropic says so, in the permissions documentation itself: Bash patterns that constrain command arguments are fragile. The docs' own example shows a curl restriction bypassed by option reordering and shell variables, and the same tricks apply to any command matcher. A permission rule evaluates the command string before execution; it can't see what the process actually does.

So treat the deny list as the polite layer. It stops the agent's ordinary behavior and documents your policy in a reviewable file. For enforcement, the documentation points to two stronger mechanisms: hooks and the sandbox.

How Do You Block Apply With a Hook?

A PreToolUse hook runs your script before a tool call executes. If the script exits with code 2, the call is blocked and your stderr message is fed back to the model, which then knows why and can route around it properly (for example, by handing you the plan for approval instead of retrying).

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.command' | grep -qE '(terraform|tofu) +(apply|destroy|state|import|taint|force-unlock)|-auto-approve|-lock=false' && { echo 'Blocked: apply/destroy/state operations require human approval' >&2; exit 2; } || exit 0"
          }
        ]
      }
    ]
  }
}

The hook receives the full command as JSON on stdin, after variable expansion has had its chance to fool a string matcher, and a blocking hook takes precedence over allow rules. It's still pattern matching, so don't expect miracles from it. What it adds over deny rules is a second, independently evaluated gate that you control completely.

What Does Sandboxing Add?

The sandbox is the only layer here the operating system enforces. Enabled with /sandbox or "sandbox": {"enabled": true} in settings, it confines bash commands and their child processes, explicitly including terraform, on macOS (Seatbelt) and Linux (bubblewrap).

Three defaults matter for IaC work. Writes are confined to the working directory and session temp, which protects the rest of the machine. Reads are not confined by default: the documentation specifically warns that ~/.aws/credentials and ~/.ssh remain readable unless you turn on sandbox.credentials protection, so do that. And network access goes through a proxy with per-domain approval, so an unexpected outbound request becomes a visible prompt instead of a silent exfiltration path.

One Terraform-specific gotcha from the docs: Go-based CLIs, Terraform among them, can fail TLS verification under the macOS sandbox. If plans start failing on certificate errors, the documented workaround is excluding that command rather than disabling the sandbox wholesale.

How Should MCP Servers Be Scoped?

Add servers at project scope so the configuration lives in .mcp.json, gets reviewed like code, and each teammate approves it once:

claude mcp add --transport http --scope project terraform-registry https://<registry-mcp-endpoint>

Start registry-only. Documentation lookup covers most authoring work and carries no control-plane access. When a task genuinely needs run history or workspace data, add a control-plane server with a read-only scope; the pillar post's access-class taxonomy is the checklist for what a given server can reach. Some platforms make the read-only choice for you. Scalr's MCP server, for instance, can't trigger runs or approve applies as of July 2026, so connecting it doesn't extend the blast radius to apply even if the token holder could apply in the UI.

Output size is handled for you up to a point: Claude Code warns when one MCP result exceeds 10,000 tokens and caps results at 25,000 by default. That's protection against a flooded context, not against a malicious payload; treat tool output as untrusted input either way.

How Do You Delegate Noisy Work to a Read-Only Subagent?

Plans, provider schemas, and run logs shouldn't sit in your main session. Define a read-only subagent in .claude/agents/plan-reader.md:

---
name: plan-reader
description: Reads Terraform plan JSON and run logs, returns a filtered summary
tools: Read, Grep, Glob
---
Summarize the plan or log at the path you are given. Return affected resource
addresses, action types, every delete or replacement, the first causal error
if any, and nothing else. Keep the summary under one page.

The tools field is an allowlist; omit it and the subagent inherits everything, including write tools, so don't omit it. The subagent burns its own context on the 200 KB plan file and returns a page, and the boundary doubles as containment: if a poisoned log tries to steer the model, the reader has no tools that mutate anything.

Which Layer Actually Holds?

Credentials. Every layer above evaluates intent; only the credential constrains outcome. Give the session a backend or control-plane identity that can read state, acquire locks, and create speculative plans, and cannot apply, approve, or write state. Then a bypass of every software layer still ends in a 403.

Platforms with run-level RBAC make this clean. In Scalr, the apply approval is a distinct permission, so a service account for agent sessions can create plan-only runs while approval stays with named humans; short-lived tokens via OIDC federation avoid parking a long-lived secret in the agent's environment. HCP Terraform, Spacelift, and env0 offer their own scoping models, and plain open-source Terraform can approximate it with a plan-only IAM role for the backend. The mechanism matters less than the property: the agent's identity can't complete the change alone. Once a plan exists, review it like any agent-authored change; our review checklist for AI-generated Terraform covers that half.

Where Should You Start?

Copy the deny list into a project .claude/settings.json, add the hook, and turn on the sandbox with credential protection. That's twenty minutes. Then do the part that outlasts any client configuration: issue the agent its own plan-only identity, and let every other layer be defense in depth rather than the thing standing between an autocompleted command and production.

Frequently asked questions

Can Claude Code run terraform apply?

Yes, if its permission rules and shell credentials allow it, which is why the safer default is layered denial: a deny rule on the apply command, a PreToolUse hook that blocks it, and, most importantly, a backend identity whose token cannot approve or apply runs. As of July 2026, Claude Code's auto permission mode also blocks terraform destroy and plans that destroy resources by default.

Are Claude Code permission rules enough to stop a destructive command?

Not on their own. Anthropic's documentation states that Bash permission patterns constraining command arguments are fragile and can be bypassed through option reordering or shell variables. It recommends hooks and OS-level sandboxing for real enforcement. Treat deny rules as guardrails for the common case and put the hard stop in hooks and credentials.

Should Claude Code have access to my cloud credentials?

By default a sandboxed Claude Code session can still read the whole filesystem, and Anthropic's sandboxing documentation specifically warns that ~/.aws/credentials and ~/.ssh are readable unless you enable credential protection. Use the sandbox.credentials setting, keep long-lived cloud keys out of the environment, and prefer short-lived credentials scoped to plan-only operations.

Which MCP server should a Terraform session use?

Start with a registry-only server for provider and module documentation, added at project scope so the whole team shares one reviewed configuration. Add a control-plane server with read scope only when the task needs run or workspace data, and keep write-capable servers out of routine sessions.
About the author
Sebastian StadilCEO at Scalr
Sebastian Stadil is the CEO at Scalr. He has over 15 years of devops experience, and started his career with AWS in 2004.