Setup guide
Everything you must provision before this skill can run โ the exact credentials, permissions, and where to click. Links go to the official consoles and docs, which stay current.
AWS CLI v2 installed and a named profile - IAM Identity Center, an assumable role, or static keys - that resolves to credentials `aws sts get-caller-identity` accepts for the account and region you intend to operate on.
You need to already have
- An AWS account you are authorised to operate, and its 12-digit account id
- AWS CLI v2.13 or later (`aws --version` must report aws-cli/2.x; v1 lacks sso-session and configure export-credentials)
- Python 3.9+ with boto3 1.34+ and botocore, for anything scripted
- `jq` for the bash examples, or use `--query` with `--output text` instead
- An administrator to create the read-only role or IAM Identity Center permission set, and a separate write role if the task will mutate
- For IAM Identity Center: the AWS access portal start URL (https://d-xxxxxxxxxx.awsapps.com/start) and the region the Identity Center instance lives in
- A browser on the same machine for the `aws sso login` device-authorisation flow, or use --no-browser and paste the code
How the pieces connect
Credentials this skill needs
Set these as environment variables. Never paste secrets into a chat or commit them.
| Variable | What it is | Where to get it |
|---|---|---|
AWS_PROFILE | Named profile in ~/.aws/config format: Profile name matching a `[profile <name>]` block, e.g. acme-staging-ro | Create it with `aws configure sso` (Identity Center) or by adding a `[profile x]` block with role_arn + source_profile to ~/.aws/config. List what exists with `aws configure list-profiles`. |
AWS_REGION | Default region for regional API calls format: Region code such as us-east-1 or eu-west-1 (AWS_DEFAULT_REGION is the older alias and loses to AWS_REGION) | Pick from `aws ec2 describe-regions --all-regions --query 'Regions[].RegionName' --output text`, or read the `region` key of the profile in ~/.aws/config. |
AWS_ACCESS_KEY_IDsecretoptional | Access key id (only when Identity Center and role assumption are unavailable) format: 20-character string starting AKIA (long-lived user key) or ASIA (temporary session credential) | IAM console > Users > <user> > Security credentials > Access keys > Create access key. Prefer IAM Identity Center or a role - long-lived AKIA keys are the last resort. |
AWS_SECRET_ACCESS_KEYsecretoptional | Secret access key paired with the access key id format: 40-character base64-style string | Shown once, at creation time, on the same IAM console screen as the access key id; it cannot be retrieved later, only rotated. |
AWS_SESSION_TOKENsecretoptional | Session token for temporary credentials format: Long opaque string; mandatory whenever the key id begins with ASIA | Returned in `Credentials.SessionToken` by `aws sts assume-role`, or emitted by `aws configure export-credentials --format env`. |
AWS_PAGERoptional | Pager override - set to an empty string for non-interactive runs format: Empty string (export AWS_PAGER=""); equivalent to passing --no-cli-pager on every command | Not obtained from AWS - set it in the shell that runs the agent. CLI v2 pipes output through a pager by default, which blocks any non-tty session. |
Permissions to grant
sts:GetCallerIdentityIAM policyThe mandatory pre-flight probe that proves which account, principal and partition the credentials resolve to before any read or mutation.
โณ Requires no IAM permission at all and cannot be denied by an identity policy, so it is safe to call from any principal.
arn:aws:iam::aws:policy/ReadOnlyAccessIAM policyadmin consent requiredBaseline for every investigation: describe/list/get across services so the skill can enumerate resources, resolve blast radius and verify post-state.
โณ arn:aws:iam::aws:policy/job-function/ViewOnlyAccess is narrower (it omits data-plane reads such as s3:GetObject and dynamodb:GetItem); narrower still is a hand-written policy listing only the describe/list actions the task needs.
sts:AssumeRoleIAM policyadmin consent requiredSwitches into the target account, or into the elevated write role named by a profile's role_arn.
โณ Scope Resource to the exact role ARNs the agent may assume, and require sts:ExternalId plus aws:MultiFactorAuthPresent in the role's trust policy.
ec2:DescribeRegionsIAM policyEnumerates which regions are enabled for the account so multi-region sweeps never guess and OptInRequired is diagnosed correctly.
โณ Included in ReadOnlyAccess; account:ListRegions is an alternative that also reports opt-in status.
ec2:DescribeInstancesIAM policyPrimary inventory read used to resolve tag and state filters into a concrete instance id list before any change.
โณ Cannot be resource-scoped; constrain it with a Condition on ec2:Region or aws:RequestedRegion instead.
iam:SimulatePrincipalPolicyIAM policyadmin consent requiredTests whether the caller is allowed to perform a specific action on a specific resource ARN before the mutation is attempted.
โณ Scope Resource to the caller's own role ARN so the agent can simulate itself but not audit other principals; note it does not evaluate service control policies.
sts:DecodeAuthorizationMessageIAM policyDecodes the encoded blob returned with UnauthorizedOperation so a denial can be reported as a specific action and resource rather than a dead end.
โณ Cannot be resource-scoped; it is read-only and safe to grant broadly, but the decoded output contains policy detail so keep it out of shared logs.
ec2:StopInstancesIAM policyadmin consent requiredThe reversible-mutation example: gated behind --dry-run plus explicit human confirmation, with start-instances as the documented rollback.
โณ Grant only in the write role, scoped with Condition ec2:ResourceTag/Environment matching non-production values; keep ec2:TerminateInstances out of the policy entirely unless the task demands it.
cloudformation:CreateChangeSetIAM policyadmin consent requiredProduces the reviewable plan for stack changes so the skill never applies an unexamined template.
โณ Pair with cloudformation:DescribeChangeSet and cloudformation:DescribeStacks but withhold cloudformation:ExecuteChangeSet until a change is approved; scope Resource to specific stack ARNs.
cloudformation:ExecuteChangeSetIAM policyadmin consent requiredApplies a change set only after its Changes list, including any Replacement=True rows, has been shown and confirmed.
โณ Scope Resource to the named stack ARNs and add a Condition on cloudformation:ChangeSetName so ad-hoc change sets cannot be executed.
tag:GetResourcesIAM policyCross-service, tag-scoped inventory used to resolve 'everything tagged Environment=x' into explicit ARNs before a change is proposed.
โณ Read-only and included in ReadOnlyAccess; no resource-level scoping is available for this action.
cloudtrail:LookupEventsIAM policyadmin consent requiredAttributes an unexpected change to a principal and timestamp when reconciling drift or investigating a failed call.
โณ Read-only; grant only when investigation is in scope, and prefer a dedicated audit role if CloudTrail data is sensitive.
Step by step
- 1
Install or upgrade to AWS CLI v2
macOS: `brew install awscli` or the signed .pkg. Linux: `curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip && unzip -q awscliv2.zip && sudo ./aws/install --update`. Windows: the AWSCLIV2.msi installer. Confirm with `aws --version` - it must print aws-cli/2.x.
Open in the console / docs โ - 2
Make the shell safe for non-interactive use
Add to the environment the agent runs in: export AWS_PAGER="", export AWS_RETRY_MODE=adaptive, export AWS_MAX_ATTEMPTS=10, export AWS_CLI_AUTO_PROMPT=off. Without the first, CLI v2 pipes output into a pager and every command appears to hang.
Open in the console / docs โ - 3
Preferred: configure an IAM Identity Center profile
Run `aws configure sso`, supply the SSO session name, the access portal start URL and the Identity Center region, then choose the account and permission set. This writes an [sso-session <name>] block plus [profile <name>] with sso_account_id, sso_role_name, region and output. Refresh later with `aws sso login --sso-session <name>`; these sessions are short-lived by design.
Open in the console / docs โ - 4
Alternative: configure a role-assumption profile
Add to ~/.aws/config: [profile acme-staging-ro] role_arn = arn:aws:iam::111122223333:role/AgentReadOnly source_profile = base external_id = <shared secret> mfa_serial = arn:aws:iam::444455556666:mfa/alice duration_seconds = 3600 region = us-east-1 The SDK then refreshes the session automatically - far safer than exporting AWS_SESSION_TOKEN once.
Open in the console / docs โ - 5
Create the read-only role or permission set
IAM console > Roles > Create role > Custom trust policy (Principal = the agent's identity or the Identity Center permission set), attach arn:aws:iam::aws:policy/ReadOnlyAccess, name it AgentReadOnly. Add an inline policy granting sts:DecodeAuthorizationMessage and iam:SimulatePrincipalPolicy scoped to this role's own ARN.
Open in the console / docs โ - 6
Create a separate write role, never a combined one
Repeat the previous step with a policy listing only the mutating actions the work requires - for example ec2:StopInstances, ec2:CreateTags, cloudformation:CreateChangeSet, cloudformation:DescribeChangeSet, cloudformation:ExecuteChangeSet - scoped to resource ARNs and, where supported, Condition ec2:ResourceTag/Environment. Expose it as a second profile so the read profile can never mutate.
Open in the console / docs โ - 7
Prove the credentials and pin the account
Run `aws sts get-caller-identity --profile <p> --query '[Account,Arn]' --output text` and `aws iam list-account-aliases --profile <p>`. Record the 12-digit account id as EXPECTED_ACCOUNT in the agent's environment so the preflight guard refuses to run anywhere else.
Open in the console / docs โ - 8
Smoke-test pagination, filtering and a dry run
`aws ec2 describe-regions --all-regions --query 'Regions[0:3].RegionName' --output text`, then `aws ec2 describe-instances --page-size 20 --query 'length(Reservations[].Instances[])'`, then a harmless `aws ec2 stop-instances --instance-ids <id> --dry-run` - the expected result is the DryRunOperation error, which exits 254.
Open in the console / docs โ
Check it worked
aws sts get-caller-identity --profile "$AWS_PROFILE" --query '[Account,Arn]' --output text
If you hit an error
Unable to locate credentials. You can configure credentials by running "aws configure".Cause: No credential source resolved: the profile has neither static keys nor a live SSO/role session, or AWS_PROFILE names a profile that only exists in ~/.aws/credentials while the config lives elsewhere.
Fix: Run `aws configure list --profile <p>` to see where each value comes from, then `aws sso login --sso-session <name>` or set the profile's source_profile. Never invent keys.
The config profile (acme-staging) could not be foundCause: AWS_PROFILE names a profile that is not in ~/.aws/config, or the block is written as [acme-staging] in config where it must be [profile acme-staging].
Fix: Run `aws configure list-profiles`, fix the block header (only ~/.aws/credentials uses the bare [name] form), or point AWS_CONFIG_FILE at the right file.
An error occurred (ExpiredToken) when calling the DescribeInstances operation: The security token included in the request is expiredCause: The temporary session (IAM Identity Center or assume-role) passed its duration - typically one hour - mid-task.
Fix: Re-run `aws sso login --sso-session <name>` or re-assume the role. Structurally, use a profile with role_arn/sso_session so the SDK refreshes automatically instead of exporting AWS_SESSION_TOKEN once.
Error loading SSO Token: Token for https://d-xxxxxxxxxx.awsapps.com/start does not existCause: No cached IAM Identity Center token for this start URL, or the ~/.aws/sso/cache entry was cleared or belongs to a different sso-session name.
Fix: Run `aws sso login --sso-session <name>` (or `aws sso login --profile <p>`) and complete the browser device flow; add --no-browser on a headless host.
An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:iam::444455556666:user/alice is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::111122223333:role/AgentReadOnlyCause: Either the caller's identity policy lacks sts:AssumeRole for that role ARN, or the role's trust policy does not name the caller - or it requires an ExternalId or MFA that was not supplied.
Fix: Fix both sides: allow sts:AssumeRole on the exact role ARN in the caller's policy, and add the caller as a Principal in the role's trust policy. Pass --external-id and --serial-number/--token-code when the trust policy conditions on them.
An error occurred (UnauthorizedOperation) when calling the TerminateInstances operation: You are not authorized to perform this operation. Encoded authorization failure message: <blob>Cause: EC2's authorization failure for a mutating call - an identity policy, permission boundary, or SCP denied it.
Fix: Run `aws sts decode-authorization-message --encoded-message <blob> --query DecodedMessage --output text` to see the exact action, resource and matched statement, then request that specific permission rather than a broader policy.
An error occurred (AccessDenied) when calling the PutObject operation: User: arn:aws:sts::111122223333:assumed-role/AgentReadOnly/session is not authorized to perform: s3:PutObject on resource: arn:aws:s3:::bucket/key with an explicit deny in a service control policyCause: An AWS Organizations SCP denies the action for the whole account, so no identity policy can grant it - and iam:SimulatePrincipalPolicy will not have predicted it.
Fix: Stop and escalate to the organization administrator; do not attempt workarounds, alternate regions, or a different principal in the same account.
An error occurred (DryRunOperation) when calling the StopInstances operation: Request would have succeeded, but DryRun flag is set.Cause: Not a failure - this is the success signal for a --dry-run pre-check, but the CLI still exits 254 and will abort a `set -e` script.
Fix: Capture the output (out=$(aws ... --dry-run 2>&1) || true) and match on DryRunOperation instead of relying on the exit status.
An error occurred (OptInRequired) when calling the DescribeInstances operation: You are not subscribed to this serviceCause: The target region is an opt-in region this account has not enabled, or the service is not activated for the account.
Fix: Enable the region in Account settings (or via account:EnableRegion) and set sts_regional_endpoints = regional so STS works there; otherwise pick an enabled region from `aws ec2 describe-regions`.
An error occurred (RequestLimitExceeded) when calling the DescribeInstances operation: Request limit exceeded.Cause: Too many API calls per second, usually from looping single-resource describes or from client-side --query filtering that pulls every page.
Fix: Set AWS_RETRY_MODE=adaptive and AWS_MAX_ATTEMPTS=10, use paginators with --page-size, and push predicates into server-side --filters instead of --query.
You must specify a region. You can also configure your region by running "aws configure".Cause: No region resolved from --region, AWS_REGION, AWS_DEFAULT_REGION, the profile's region key, or instance metadata.
Fix: Pass --region explicitly on every scripted command, or set the region key on the profile; never rely on an ambient default when mutating.
An error occurred (InvalidClientTokenId) when calling the GetCallerIdentity operation: The security token included in the request is invalidCause: The access key was deleted, deactivated or rotated, the secret does not match the key id, or the credentials belong to a different partition (aws-us-gov, aws-cn).
Fix: Re-issue or re-export the credentials, confirm the key id and secret are from the same pair, and check the endpoint partition matches the account.
Official documentation: https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html โ
API operations this skill uses (20)
Every call the skill makes, linked to its official reference page.
| Operation | Call | Permission | Ref |
|---|---|---|---|
| Prove which account and principal the credentials resolve to Mandatory pre-flight before every read sequence and every mutation; the only reliable proof of account, principal and partition. | CLI aws sts get-caller-identity --profile <p> --query '[Account,Arn]' --output text | sts:GetCallerIdentity | docs โ |
| Resolve the account's human-readable alias Turns a bare 12-digit id into something a human can sanity-check ('acme-prod') when confirming a change. | CLI aws iam list-account-aliases --query 'AccountAliases[0]' --output text | iam:ListAccountAliases | docs โ |
| Refresh an IAM Identity Center session The fix for ExpiredToken and 'Error loading SSO Token'; re-mints the short-lived credentials every other call depends on. | CLI aws sso login --sso-session <session-name> | sso:GetRoleCredentials (granted by the permission set, not by an inline identity policy) | docs โ |
| Assume a role in another account or a higher-privilege role Crosses the account boundary deliberately; the returned Credentials block feeds AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_SESSION_TOKEN. | CLI aws sts assume-role --role-arn arn:aws:iam::111122223333:role/Ops --role-session-name agent-task --duration-seconds 3600 --external-id <id> | sts:AssumeRole | docs โ |
| Export resolved credentials for a child process Hands SDK-resolved, auto-refreshing credentials to tools that cannot read ~/.aws/config - terraform, a container, a test harness - without copying long-lived keys. | CLI aws configure export-credentials --profile <p> --format env-no-export | None (reads local config and refreshes the profile's own credentials) | docs โ |
| Enumerate regions enabled for the account Drives multi-region sweeps from fact rather than assumption, and distinguishes a disabled region (OptInRequired) from a credential problem. | CLI aws ec2 describe-regions --all-regions --query 'Regions[?OptInStatus!=`not-opted-in`].RegionName' --output text | ec2:DescribeRegions | docs โ |
| Paginated, server-side-filtered instance inventory The canonical read: server-side --filters for selection, --page-size for safe pagination, --query only for output shape. | CLI aws ec2 describe-instances --filters Name=instance-state-name,Values=running Name=tag:Environment,Values=staging --page-size 100 --query 'Reservations[].Instances[].[InstanceId,InstanceType,State.Name]' --output text | ec2:DescribeInstances | docs โ |
| Tag-scoped inventory across services Resolves 'everything tagged X' into explicit ARNs in one call, so blast radius is enumerated before anything is proposed. | CLI aws resourcegroupstaggingapi get-resources --tag-filters Key=Environment,Values=prod --resources-per-page 100 --query 'ResourceTagMappingList[].ResourceARN' | tag:GetResources | docs โ |
| Simulate an action before attempting it Cheap authorization pre-check that avoids half-completed changes; note it does not evaluate service control policies. | CLI aws iam simulate-principal-policy --policy-source-arn <caller-arn> --action-names ec2:TerminateInstances --resource-arns <resource-arn> --query 'EvaluationResults[].[EvalActionName,EvalDecision]' --output text | iam:SimulatePrincipalPolicy | docs โ |
| Dry-run a destructive EC2 call Proves the call would succeed without performing it: DryRunOperation means allowed (exit 254), UnauthorizedOperation means denied. | CLI aws ec2 terminate-instances --instance-ids i-0abc123 --dry-run | ec2:TerminateInstances | docs โ |
| Execute a gated, reversible mutation The preferred shape for change: reversible, dry-run verified, executed only after explicit confirmation, with start-instances as the stated rollback. | CLI aws ec2 stop-instances --instance-ids i-0abc123 --query 'StoppingInstances[].[InstanceId,CurrentState.Name]' --output table | ec2:StopInstances | docs โ |
| Decode an authorization failure blob Converts an opaque UnauthorizedOperation into the exact action, resource and matched statement so the right permission can be requested. | CLI aws sts decode-authorization-message --encoded-message <blob> --query DecodedMessage --output text | sts:DecodeAuthorizationMessage | docs โ |
| Create a CloudFormation change set without executing it The dry-run equivalent for stacks; `aws cloudformation deploy --no-execute-changeset` is the one-command form of the same gate. | CLI aws cloudformation create-change-set --stack-name app --change-set-name review-1 --template-body file://template.yaml --capabilities CAPABILITY_NAMED_IAM | cloudformation:CreateChangeSet | docs โ |
| Review the pending stack changes Surfaces Replacement=True rows - the resources that will be destroyed and recreated - which must be shown to a human before execution. | CLI aws cloudformation describe-change-set --stack-name app --change-set-name review-1 --query 'Changes[].ResourceChange.[Action,LogicalResourceId,ResourceType,Replacement]' --output table | cloudformation:DescribeChangeSet | docs โ |
| Execute the change set after confirmation Applies exactly the reviewed plan, never a freshly generated one, so what was approved is what runs. | CLI aws cloudformation execute-change-set --stack-name app --change-set-name review-1 | cloudformation:ExecuteChangeSet | docs โ |
| Block until a stack operation settles Correct polling instead of sleep loops; on failure follow with describe-stack-events to find the first failed resource. | CLI aws cloudformation wait stack-update-complete --stack-name app | cloudformation:DescribeStacks | docs โ |
| Block until an instance reaches a state Verifies the post-state of a mutation before success is reported; fails fast on terminal states rather than spinning. | CLI aws ec2 wait instance-stopped --instance-ids i-0abc123 | ec2:DescribeInstances | docs โ |
| List S3 objects under a prefix Enumerates exactly what a prefix contains, and how large it is, before any delete or lifecycle change is proposed. | CLI aws s3api list-objects-v2 --bucket my-bucket --prefix logs/2026/ --page-size 1000 --query 'Contents[].[Key,Size]' --output text | s3:ListBucket | docs โ |
| Preview a recursive S3 delete The s3 high-level commands spell the flag --dryrun (one word); it prints every key that would be deleted, which is the confirmation artefact. | CLI aws s3 rm s3://my-bucket/logs/2026/ --recursive --dryrun | s3:DeleteObject (plus s3:ListBucket) | docs โ |
| Attribute an unexpected change to a principal Answers 'who did this, and when' when reconciling drift or investigating a resource that vanished. | CLI aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=TerminateInstances --max-results 10 --query 'Events[].[EventTime,Username,EventName]' --output table | cloudtrail:LookupEvents | docs โ |