Skip to content
What Claude Code wrote of our infrastructure, and what we refused to merge
← ← Back to Thinking AI

What Claude Code wrote of our infrastructure, and what we refused to merge

We use Claude Code for infrastructure work daily, on a platform we run for a US client: CDK stacks, deploy scripts, IAM policies, alarms, runbooks. About a third of the infrastructure code committed in the last six months was first drafted by it. This is an honest accounting: what it wrote that we merged as-is, what it wrote that we fixed, and the four proposals we refused, with the reasons, because the refusals are more instructive than the successes.

The shape of the work

Infrastructure changes on this platform go through the same pull request review as application code, and the agent's output goes through it too. It runs in a sandbox with read access to the repository and a scoped AWS role that can cdk diff against staging and read CloudWatch, but cannot deploy. Every change it proposes is a branch and a PR. A human reads the diff, reads the cdk diff output the agent pastes into the PR, and merges or does not. Deploys are the same script as always, run by a human.

That setup is the reason the rest of this article is calm. The agent can propose anything; it can execute nothing. Every one of the refusals below was caught at the PR.

Claude Code · sandboxrepo: read + branchAWS: cdk diff, CloudWatch readno deploy · no write to AWS pull requestthe code changecdk diff output pasted inreasoning in the description human reviewmerge · fix · refusesame bar as app codeall four refusals caught here human deploythe usual scriptstaging, then prodagent not involved The agent can propose anything and execute nothing. That is what makes the rest of this article calm.

Merged as written

CloudWatch alarms and dashboards. The best category. Given "alarm on the email DLQ, the App Runner 5xx rate, and Aurora ACU at max for 10 minutes", it produced correct CDK with sensible thresholds, the right statistics (p99 not average for latency), treatMissingData set appropriately, and an SNS topic wired to the existing on-call channel. Alarms are tedious, well-documented, and have an obvious right answer; that is the sweet spot.

The restore drill script. The script that re-applies PITR, TTL, streams and tags to a restored DynamoDB table, reading the intended settings from the CDK definitions. Around 120 lines of TypeScript with the AWS SDK, correct on the first PR, including the pagination that humans forget.

Runbooks. Given an incident's log excerpt and the fix that was applied, it writes a clear runbook page in the house format. Its runbooks are better than ours, because it does not skip the steps it considers obvious.

The OIDC deploy role trust policy. Correct sub condition, correct audience, scoped to the repository and branch. It also, unprompted, added a comment explaining why the sub claim must be matched with StringLike for the branch pattern, which is the thing people get wrong.

Cost-tagging propagation. A cross-cutting change to add CostCenter and Owner tags to every stack via an aspect. Mechanical, touched 40 files, zero mistakes.

Merged after fixes

The VPC endpoint stack. It added the endpoints correctly but put the S3 endpoint as an interface endpoint rather than a gateway endpoint, which works but costs $7 a month for something that is free. Caught by reading the cdk diff. The kind of mistake a human who had not done it before would make, too.

A Lambda concurrency change. It set reserved concurrency on a function to protect a downstream API, which was the ask, but chose a number by reading the downstream's documented rate limit and dividing by the average invocation duration. The arithmetic was right; the assumption that invocations are uniformly spread was not. We halved it. The reasoning was in the PR description, which is what made the fix a one-minute conversation.

Security group tightening. Asked to reduce the Aurora security group's ingress to only the App Runner VPC connector and the Lambdas, it did, and also removed the rule for the bastion host that the runbook still references for emergency access. The bastion is off 99 % of the time, so cdk diff in staging showed nothing breaking. A human who knew about the bastion put the rule back. The agent could not have known, because the bastion's role was in a person's head, not in the repository. It is in the repository now.

Refused

1. "Add cdk deploy to the agent's role so I can verify changes end to end." Proposed in a PR description as an efficiency improvement, with a well-argued case: diff is not the same as deploy, some errors only appear at deploy time, staging is isolated. All true. Refused, because the whole model rests on the agent not being able to change AWS state, and "just staging" is how that erodes. Staging shares a CDK codebase with production; a deploy bug in staging is a preview of one in production, and we want a human to see the preview.

2. A RemovalPolicy.DESTROY on the staging Aurora cluster "to make teardown faster". Correct in isolation; staging is disposable. Refused because the same construct is instantiated for production with an environment flag, and we have lost resources to exactly this shape of change before. The rule in this codebase is that removal policies are RETAIN everywhere and teardown is a manual, named operation. The agent had no way to know the history; the CLAUDE.md now says it.

3. A wildcard IAM statement to "unblock the search indexer". The indexer was failing with an access denied on a new DynamoDB stream ARN. The proposed fix was dynamodb:* on *, with a comment "tighten later". Refused, obviously, and the interesting part is that when asked for the minimal fix instead, it produced the exact stream ARN and the four actions the consumer needs in under a minute. The wildcard was not a capability limit; it was the path of least resistance, and the review is what made the other path be taken.

4. Moving secrets from Secrets Manager to SSM to save $8 a month. A real saving, correctly computed, and an argument we have made ourselves. Refused for the two secrets in question because they are rotated by Secrets Manager's rotation Lambda, which SSM does not have, and the PR did not account for rotation. Half of it was merged: the three static secrets moved, the two rotated ones stayed.

merged as written alarms and dashboardsrestore drill settings scriptrunbooks from incident logsOIDC trust policy, with the commentcost tags across 40 files tedious · documented · one right answerthe sweet spot merged after fixes S3 endpoint: interface, not gatewayconcurrency: right maths, wrong modelsecurity group: removed the bastion each fix was a one-minute conversationbecause the reasoning was in the PRthe bastion lived in a head, not the repo refused deploy permission for the agentDESTROY on a shared constructdynamodb:* on * "tighten later"rotated secrets → SSM (half merged) all about blast radius or historynone about capabilityall caught at the PR Roughly a third of infra commits in six months were agent-drafted. Zero were agent-deployed.

What the pattern says

The merges are mechanical, well-documented work with one right answer. The fixes are places where the right answer depended on something not in the repository: the bastion, the traffic shape. The refusals are all about blast radius or history, and none of them are about capability: in every case the agent could produce the safe version the moment it was asked to.

So the work is not "can the agent write infrastructure code". It can. The work is making the repository contain the things that used to live in heads: the removal-policy rule, the bastion's purpose, why the two secrets rotate. Every refusal above became a line in the CLAUDE.md or a comment in the construct, and the same proposal has not come back.

And the sandbox boundary is not negotiable, however good the efficiency argument. The agent drafts; the human deploys. That is the whole reason a third of the infrastructure can be agent-drafted without anyone losing sleep.

If you want to use an agent on your infrastructure and are trying to decide where the line goes, we have drawn it once and will help you draw yours.