DEV Community

Cover image for A request-and-approve AWS permission system for Claude Code
Máté Borbély
Máté Borbély

Posted on • Originally published at Medium

A request-and-approve AWS permission system for Claude Code

AI coding tools like Claude Code have sped up how we build and run software, and infrastructure work is no exception. Tasks that used to take hours, like provisioning resources or chasing down configuration issues, can now be done in minutes. The largest gains come when these tools can take on bigger units of work autonomously, in a closed feedback loop. The agent acts, reads the result, adjusts, and tries again until the work actually holds together.

But handing an agent complete, unrestricted autonomy over a cloud account carries risks that are simply not acceptable in many environments. There are already ways to mitigate those risks, but each comes with its own drawbacks:

  1. Approve every command by hand. This is tedious and far from efficient: you are signing off on everything, including trivial local steps that carry no AWS risk at all. And even careful review has limits. A cryptic command or the name of a complex script rarely makes it obvious what you are actually permitting.

  2. Run the agent from an already-restricted environment, such as an EC2 instance carrying only limited credentials. This shrinks the blast radius, but setting it up is slow and fiddly enough that people often do not bother. And for that same reason, the credentials rarely end up scoped to least privilege. Rather than work out exactly what a task needs, people reach for a broad-enough umbrella that will not get in the way.

If we plot these approaches, together with the unrestricted access we started with, on a simple chart of ease of use against the risk of unwanted actions, the picture looks something like this. Unrestricted access sits in the simple-but-high-risk corner, while the two lower-risk options trade ease of use away to get there.

Ease of use vs. risk of unwanted actions: unrestricted access sits in the high-risk, simple corner, while manual approval and a restricted environment are lower risk but harder to use. The low-risk, simple corner is empty.

The solution: letting the agent ask for what it needs

Due to these shortcomings, I set out to build something that combines ease of use with proper control. The idea is simple: let Claude Code generate the IAM policies it needs, the same way it already generates commands. This has two useful consequences:

  1. Generating the permissions is quick and automatic, and it puts zero burden on you.
  2. Because the agent produces them in context, it knows exactly what the task in front of it needs, so it can generate least-privilege policies.

But generating the policies is only part of the solution. We obviously cannot let Claude Code grant itself the access it asks for. So we need an architecture where the agent can generate and request these policies, but has no ability to approve them. There are a couple of alternative ways to enforce that separation. The presented solution follows a Docker-based approach.

In practice, this means running Claude Code inside a Docker container that starts with no AWS permissions at all. Alongside the project files, we mount in some utility scripts the agent uses to interact with the broker, and a shared folder that supports those interactions. Whenever the agent needs extra permissions, it runs one of these scripts to write the policy it wants into that folder.

On the host, outside the container, a small credential broker watches that folder. It has access to the AWS credentials of the host machine, which means it can use them to grant the permissions the container requests. The broker holds the only door to the account, and a human stands at it.

Requesting and granting AWS access: Claude Code writes a requested IAM policy into the shared directory; the broker detects it, notifies the user, and on approval mints credentials into the grants folder for the agent to read.

Because AWS policies are rich, supporting this through a web UI felt like the most elegant choice, so the broker runs a small web application. That is where the human stands at the door: every request the agent makes surfaces there for a person to review and approve.

What this looks like in practice

To make this concrete, let us follow a single task through the broker. Let's suppose that we have a user registration API, and it's failing for some reason. Let's say the API runs on Lambda and DynamoDB: new sign-ups arrive at a Lambda function that writes them to a DynamoDB table. We ask Claude Code to find out what is going wrong.

To investigate, the agent needs to read the Lambda's logs and inspect the table. So before it starts, it requests exactly that access. The screenshot below shows how that request surfaces in the broker's web UI. On the left, each session appears under the human-readable name it has in Claude Code. The one for the current session is flagged with a waiting request. The main panel shows that request in detail.

The broker showing a pending request: read-only access to the registration Lambda's CloudWatch Logs and the DynamoDB table, with each action listed for review.

The request is scoped to the task in front of the agent: read-only access to the specific log group and table it named, and nothing else. Every action is listed explicitly, so you can see exactly what you are about to permit. If something looks off, you can untick individual actions before approving, or simply tell Claude to rework its request and submit a tighter one.

Once you approve, the broker mints short-lived credentials for precisely those permissions and hands them to the container. The same view now shows them as granted, and the agent can get to work.

The same session after approval: the requested permissions now appear under 'currently granted', kept alive for the duration of the task.

From the agent's side, none of this required a standing key or a broad role. It asked for a narrow slice of access, a human approved it, and the credentials exist only for as long as the task needs them.

How the credentials are minted

You are probably curious how the permissions are actually granted on the AWS side. It is worth a short explanation, especially since the mechanism has evolved as the project matured.

The initial solution relied on the quickest path AWS offers. When a request was approved, the broker called STS to produce a short-lived token and attached the approved permissions directly to it, as a session policy. This is fast and leaves nothing behind: no roles to create, no cleanup. The catch is that a session policy is capped at 2048 characters, which is a tight budget. As soon as the agent asks for a handful of services at once, each with its own actions and resource patterns, the policy runs past the limit. Worse, with tokens minted this way the agent cannot call IAM or STS, which rules out a whole class of tasks.

So the broker now mints differently by default. For each session it creates a small, dedicated IAM role whose own policy is exactly the approved union of permissions, and then it hands the container credentials for that role. Creating a real role per session is slightly slower than issuing a bare token, but it buys a lot of room: the permissions live in the role's inline policy, which allows far more than a session policy. If a session ever asked for so much that even that filled up, the design falls back to spilling the remainder into separate attached policies. This is a rare occurrence, so it has not been battle tested, and the implementation could also be optimized for speed.

The mode is configurable, and the older token-based path is still available for setups that want it. But the per-session role is the default, because the small cost of creating it is easily worth escaping the limits of the quick approach.

Where this is today

If you would like to try it, the solution is publicly available on GitHub at https://github.com/nexentize/aws-genai-credential-broker. So far we have tested it on Ubuntu Linux and Windows. The broker core is written in plain Python and should run anywhere. Supporting other operating systems mostly comes down to a little plumbing, making sure the scripts can be launched from anywhere and that features like voice dictation keep working inside the container.

It has already served me well in my everyday work, but it is far from perfect, and it has real limitations. The most important one is that the least-privilege guarantee is not completely airtight: if you grant broad enough IAM permissions, the agent can technically use them to escalate its own privileges beyond the task at hand. It is worth keeping in mind, though, that an agent is not a malicious attacker. It is trying to do the job you gave it, so this is a caveat to be aware of rather than a dealbreaker. Apart from that, plenty of edge cases could be handled more gracefully, and there is natural room to grow: other agents beyond Claude Code, and integration with the secrets tooling that many teams already run.

Most of all, though, I would love to hear what you think. If you work with coding agents in environments where unrestricted access is not an option, how do you handle it now? Do you think your workflow could benefit from this solution? Please let me know in the comments.

Top comments (0)