| name | aws-readonly-agent |
| description | Use whenever running AWS CLI commands on behalf of the user (checking IAM users/roles, scanning resources, debugging AWS config). Ensures Claude uses a scoped, least-privilege profile instead of the user's personal aws-login session. |
| license | MIT |
AWS read-only agent access
Never run AWS CLI commands under the user's default/aws login session —
that session has the user's own full permissions. Use a dedicated,
narrowly-scoped profile instead, set up by the bundled
setup-ai-agent-aws-access.sh script. It creates three things:
- A role (default name
ai-agent-claude-code-readonly) — read-only,
capped by a ReadOnlyAccess permissions boundary so it can never be
widened into a write permission by accident.
- A bootstrap user (default
ai-agent-claude-code-bootstrap) whose
only permission is assuming that one role. Its access key lives in
~/.aws/credentials, never in a chat transcript.
- A local CLI profile (default
ai-agent-claude-code, in
~/.aws/config) that auto-assumes the role via role_arn +
source_profile, with a fixed role_session_name so every action shows
up in CloudTrail as assumed-role/<role-name>/<session-name> —
distinguishable from the human's own session.
If the profile doesn't exist yet
Ask the user to run aws login (if not already), then run the bundled
setup-ai-agent-aws-access.sh from the project they want the agent to
access AWS in — it prompts for names and permission scope, and is safe to
re-run (reuses existing resources instead of erroring). Don't create IAM
resources by hand as a substitute; use the script so the setup stays
reproducible and auditable.
Rule: always use the scoped profile, never the default session
Every AWS CLI call must pass --profile <profile-name> (whatever name was
chosen at setup, default ai-agent-claude-code) or run with
AWS_PROFILE=<profile-name> exported.
aws --profile ai-agent-claude-code sts get-caller-identity
aws --profile ai-agent-claude-code iam list-users
If a command fails with AccessDenied, that is the least-privilege
boundary working as intended — do not fall back to the default profile to
get it to succeed. Tell the user what extra read action is needed and let
them decide whether to widen the role's policy themselves (it's written to
iam/<role-name>/policy.json in the project where the script was run).
Known gotchas (already hit and fixed once, don't re-derive)
aws login sessions cannot call sts:AssumeRole at all, by AWS design —
this is why the bootstrap-user indirection exists. Don't try to make the
default profile assume the role directly.
- The role's trust policy must NOT require a
sts:SourceIdentity condition.
The AWS CLI's role_arn/source_profile profile-chaining (what the
convenience --profile flag uses) has no way to set source identity, so
a mandatory condition on it makes the profile permanently unable to
satisfy its own trust policy — AccessDenied every time, no amount of
waiting fixes it. role_session_name is the distinguishing signal that
actually works through that chain. This looked identical to the
propagation-lag gotcha below and cost real time to tell apart.
Discriminator: aws iam simulate-principal-policy says allowed but the
real call still denies AND an explicit manual assume-role with
--source-identity set succeeds — that combination means it's the
condition, not propagation.
- Right after creating/updating an IAM role, user, or policy, AssumeRole
calls can fail with AccessDenied for up to ~60 seconds while IAM
propagates. Retry with backoff (see
retry_until in the setup script)
instead of concluding the policy is wrong. aws iam simulate-principal-policy
checks the policy logic independent of propagation timing — but it only
validates the identity-based policy, not trust-policy conditions like the
one above, so a clean simulator result doesn't rule out that problem.
- Never let a failed credential-extraction step (e.g. empty JSON from a
failed
assume-role call) silently fall through to real ambient
credentials — always check parsed values are non-empty, or better, use
--profile on every call so a broken profile fails loudly instead of
falling back to the default identity.