# My agent guardrail only lived on the laptop. So I compiled it into AWS.

> Source: <https://dev.to/moneytool/my-agent-guardrail-only-lived-on-the-laptop-so-i-compiled-it-into-aws-j1d>
> Published: 2026-10-01 19:08:30+00:00

For the past few weeks I've been building [Aegis-DevOps](https://github.com/moneytool/aegis-devops), an open-source check that sits in front of the shell commands an AI coding agent runs. Before Claude Code, Codex, Copilot, Cursor or Gemini CLI runs `kubectl`, `terraform` or `aws`, a hook hands the command to Aegis, and Aegis blocks it if your signed policy says no. It works. It also has a hole I knew about from the start, and version 0.3.0 is my first attempt at closing it.

*Disclosure: I drafted this article with help from an AI assistant and reviewed it myself. The commands and output below are from running aegis-devops 0.3.0 against its example policy.*

A pre-execution hook sees the command string the agent asks its shell tool to run. It doesn't see:

`boto3` directly;
In all three cases the destructive call reaches the AWS API without ever looking like `aws s3 rb`. Blocking on the client is the right first layer, because it's fast and it can explain itself to the model. It isn't the last layer.

AWS already has a control that sits above everything an identity inside an account can do: a **Service Control Policy**. An SCP attached to an account in an AWS Organization caps what any role in that account may do, and an agent with IAM rights inside the account can't detach it.

So 0.3.0 adds a compiler. It takes the same policy the hook enforces and writes it out as SCPs:

``` bash
$ aegis compile aws --account 111122223333 --out build/aegis-aws
aegis: wrote 1 SCP(s) for account 111122223333 to build/aegis-aws
       (exact=2, not-applicable=27, partial=1)
aegis: agents.yaml is report-only: the SCPs are under report-only/ and are not for attaching;
       review coverage.md, then set enforcement: enforce
```

The rule "never delete an S3 bucket or an RDS database" becomes an explicit `Deny`:

```
{
  "Effect": "Deny",
  "Action": ["rds:DeleteDBInstance", "rds:DeleteDBCluster", "rds:DeleteDBSnapshot",
             "rds:DeleteDBClusterSnapshot", "s3:DeleteBucket", "s3:DeleteObject",
             "s3:DeleteObjectVersion"],
  "Resource": ["*"],
  "Condition": {
    "ArnNotLike": { "aws:PrincipalArn": ["arn:aws:iam::111122223333:role/BreakGlass",
                                         "arn:aws:iam::111122223333:role/Deploy"] },
    "StringNotEqualsIfExists": { "aws:SourceIdentity": ["alice@example.com"] }
  }
}
```

Now it doesn't matter whether the agent typed `aws s3 rb`, wrote a boto3 script, or found a key. If the call is made as an agent identity, AWS refuses it.

An API call carries an identity, not a hint about who was typing. So the compiler needs an identity model, in a signed `agents.yaml`. The default is **deny-by-default**: you list the identities you trust (people, your deploy role, CI bound to a specific workflow), plus at least one break-glass role, and everything else is treated as an agent.

I went with deny-by-default because the other way round, listing the agents, fails open. Forget one, or let an agent mint a new role, and the new one isn't restricted.

If the only thing between an agent and `DeleteDBInstance` is "is this call from the Deploy role?", the obvious move is to become the Deploy role. So every compile also adds statements that stop exactly that:

`sts:AssumeRole` into a trusted or break-glass role, and no `iam:PassRole` of one;`organizations:LeaveOrganization`, since an account that leaves the organization sheds its SCPs.
A guardrail that looks stronger than it is does more damage than no guardrail, because people stop checking. So every compile writes a coverage report, rule by rule:

```
### block-s3-bucket-delete: exact, mapping verified
- enforced: s3/bucket delete (s3:DeleteBucket, s3:DeleteObject, s3:DeleteObjectVersion)
- same effect, not covered: s3:PutLifecycleConfiguration: an expiration rule deletes every object
- same effect, not covered: s3:PutBucketPolicy: a deny-all bucket policy locks everyone out

### block-rds-delete: partial, mapping verified
- not enforced: 'rds/*' also matches resource types the action map does not know
- same effect, not covered: rds:ModifyDBInstance: BackupRetentionPeriod=0 deletes the automated backups
```

"Same effect, not covered" is the list of other ways to reach the same outcome that the rule, as written, doesn't block. You can widen the rule, or accept the gap knowingly.

A few more choices along the same lines:

`report-only/` and tells you not to attach them. You switch `agents.yaml` to `enforce` (a signed change) once you've reviewed who would be restricted.
It's a preview. AWS is in the 0.3.0 release; a Kubernetes target (ValidatingAdmissionPolicies scoped to agent identities) is merged and waiting for the next release, and GCP and Azure are designed but not built. SCPs don't apply to an organization's management account, so agents shouldn't run there at all. Rules with a time window or a rate limit stay client-side, because an SCP can't express them. And it's a young project: I'd much rather hear "this mapping is wrong" now than after someone relies on it.

```
pip install aegis-devops
aegis init .aegis
aegis compile aws --account <your-account-id> --out build/aegis-aws
```

The client hook installs in one command for each agent (`aegis install claude|codex|copilot|cursor|gemini|opencode`), and there's a [GitHub Action](https://github.com/marketplace/actions/aegis-devops-plan-check) for Terraform and OpenTofu plans. Details on the compiler are in [docs/server-side.md](https://github.com/moneytool/aegis-devops/blob/main/docs/server-side.md).

If you run agents with real AWS access, I'd like to know how you scope them today. Issues are open at [github.com/moneytool/aegis-devops](https://github.com/moneytool/aegis-devops).
