Cursor Background Agents: CI Gates That Catch Hallucinated AWS Changes

Background Cursor agents will open a PR while you sleep. That is leverage until the diff invents iam.ManagedPolicy.fromAwsManagedPolicyName("AdministratorAccess"), a nonexistent L2 construct, or an S3 bucket policy that trusts *. Reviewers cannot outread agents at volume. The unfair advantage is a cheap, mandatory CI gauntlet tuned for AI-shaped AWS mistakes: synth, policy lint, scoped terraform/CDK plan, and path-based owners — before merge, not after the incident.

⚡ TL;DR: Treat agent PRs as untrusted input. Require cdk synth / tsc for touched packages, run IAM policy lint (no * on Action/Resource in app roles), block unknown CloudFormation resource types, and require human approval for infra/** plus identity policies. Encode the same rules in Cursor rules for AWS CDK so the agent fails locally first. Pair with Cursor rules for TS monorepos and AI code review bots.

Threat model: what agents invent

Typical hallucinated AWS changes:

  • Wildcard IAM (Action: "*", Resource: "*") on Lambda/task roles
  • Fake CDK APIs (bucket.grantMagic(...))
  • Hard-coded account ids / regions from training data
  • Security group 0.0.0.0/0 “for debugging”
  • New AWS managed policies attached “to unblock”
  • cdk.Context or secrets inlined into source
// ci/detect-iam-wildcards.ts — fail the job on app-role wildcards
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";

const BAD = [
  /actions?\s*:\s*\[\s*["']\*["']\s*\]/i,
  /resources?\s*:\s*\[\s*["']\*["']\s*\]/i,
  /ManagedPolicyName\(["']AdministratorAccess["']\)/,
  /aws_iam_policy_document[\s\S]{0,200}actions\s*=\s*\["\*"/i,
];

export function scan(dir: string): string[] {
  const hits: string[] = [];
  for (const name of readdirSync(dir)) {
    const p = join(dir, name);
    if (statSync(p).isDirectory()) {
      if (name === "node_modules" || name === "cdk.out") continue;
      hits.push(...scan(p));
      continue;
    }
    if (!/\.(ts|js|json|yaml|yml|tf)$/.test(name)) continue;
    const text = readFileSync(p, "utf8");
    for (const re of BAD) {
      if (re.test(text)) hits.push(`${p} :: ${re}`);
    }
  }
  return hits;
}

const hits = scan(process.argv[2] ?? "infra");
if (hits.length) {
  console.error("iam_wildcard_hits\n" + hits.join("\n"));
  // ❌ Allowing merge with “we will tighten later”
  process.exit(1);
}

Gate 1: package-local typecheck + cdk synth

Agents love cross-package edits that typecheck in the IDE cache and fail in CI. Mirror monorepo agent rules: affected-only turbo tasks, plus synth for any PR touching infra/.

# .github/workflows/agent-pr-gates.yml (illustrative)
name: agent-pr-gates
on:
  pull_request:
    paths:
      - "**"
jobs:
  cheap-gates:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Detect infra touch
        id: infra
        run: |
          if git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '^(infra|cdk|terraform)/'; then
            echo "touched=true" >> $GITHUB_OUTPUT
          else
            echo "touched=false" >> $GITHUB_OUTPUT
          fi
      - name: Typecheck affected
        run: pnpm turbo run typecheck --filter=...[origin/${{ github.base_ref }}]
      - name: IAM wildcard scan
        run: pnpm tsx ci/detect-iam-wildcards.ts infra
      - name: CDK synth
        if: steps.infra.outputs.touched == 'true'
        run: pnpm --filter @acme/infra exec cdk synth --strict
        env:
          # ✅ Fake context — never real credentials in PR CI for deploy
          CDK_DEFAULT_ACCOUNT: "123456789012"
          CDK_DEFAULT_REGION: "us-east-1"

cdk synth --strict catches many invented props before a human opens the files. It will not catch semantic IAM overbreadth — that is why the regex/AST lint stays.

Gate 2: policy lint with allowlisted exceptions

Move beyond regex when you can: parse CloudFormation from cdk.out and flag Effect: Allow statements where Action or Resource is *, unless the statement Sid is in a reviewed allowlist (e.g. CloudWatch PutMetricData on *).

// ci/lint-cfn-iam.ts
import { readFileSync } from "node:fs";

type Statement = {
  Sid?: string;
  Effect?: string;
  Action?: string | string[];
  Resource?: string | string[];
};

const ALLOWLIST_SIDS = new Set(["CloudWatchPutMetrics", "XRayWrite"]);

export function lintTemplate(path: string) {
  const tpl = JSON.parse(readFileSync(path, "utf8"));
  const errors: string[] = [];
  const resources = tpl.Resources ?? {};
  for (const [logicalId, res] of Object.entries(resources) as [string, any][]) {
    if (res.Type !== "AWS::IAM::Policy" && res.Type !== "AWS::IAM::ManagedPolicy") continue;
    const stmts: Statement[] = res.Properties?.PolicyDocument?.Statement ?? [];
    for (const s of stmts) {
      if (s.Effect !== "Allow") continue;
      if (s.Sid && ALLOWLIST_SIDS.has(s.Sid)) continue;
      const actions = Arr(s.Action);
      const resourcesArr = Arr(s.Resource);
      if (actions.includes("*") || resourcesArr.includes("*")) {
        errors.push(`${logicalId} sid=${s.Sid ?? "?"} wildcard`);
      }
    }
  }
  return errors;
}

function Arr(v?: string | string[]) {
  return v == null ? [] : Array.isArray(v) ? v : [v];
}

Gate 3: scoped plan / diff, not blind apply

For Terraform or CDK diff in advanced setups, run plan with read-only OIDC into a sandbox account — never apply from the agent PR workflow. Publish the plan artifact for reviewers.

# illustrative — sandbox only
aws sts get-caller-identity
cdk diff "StackName" -c env=sandbox || true

❌ Long-lived AKIA keys in GitHub secrets for agent workflows.
✅ OIDC, sandbox account, deny iam:CreateUser / * on production ARNs via SCP.

Gate 4: CODEOWNERS + required checks for infra

# CODEOWNERS
/infra/          @platform-team
/**/iam*.ts      @platform-team
/**/*.tf         @platform-team

Require status checks: cheap-gates, iam-lint, cdk-synth. Disable admins bypassing for infra/** if your org allows. Background agent PRs should still wait on platform review — automate the boring catches, not the merge.

Teach the agent the same gates locally

Put non-negotiables in .cursor/rules so background agents do not propose AdministratorAccess in the first place — details in Cursor Rules for AWS CDK. CI remains the backstop when rules are ignored.

<!-- .cursor/rules/aws-iam.mdc excerpt -->
- Never use Action:"*" or Resource:"*" on application roles
- Prefer grant* methods with least privilege; no AdministratorAccess
- No 0.0.0.0/0 on SSH; no inline secrets
- After infra edits, run: pnpm --filter @acme/infra exec cdk synth --strict

For review bots that comment on agent PRs, keep their IAM minimal per AI Code Review Bots. Prod-touching agent tools still need HITL dual control.

Checklist

  • [ ] PR CI: affected typecheck + IAM wildcard scan on every agent PR
  • [ ] cdk synth --strict (or terraform validate + plan) when infra paths change
  • [ ] CFN/IAM lint with small Sid allowlist; no silent *
  • [ ] No production apply credentials on agent workflows; sandbox OIDC only
  • [ ] CODEOWNERS for infra/** and IAM files; required checks enforced
  • [ ] Cursor rules mirror CI (wildcards, synth command, no fake constructs)
  • [ ] Review bot / humans treat agent AWS diffs as hostile until gates pass
  • [ ] Alerts when synth/lint start failing on main after rule drift

Related reading

Last updated on September 11, 2026


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply