AWS CLI Operations — Multi-Account
This skill manages multiple AWS accounts (static IAM keys, native IAM Identity Center, and SAML SSO) with safe switching. An account registry at ~/.aws/account-registry.json tracks all configured accounts and which one is active.
CRITICAL: Every aws command MUST be prefixed with AWS_PROFILE=<profile> using the active profile from the registry. Shell env vars do not persist between tool calls.
Active Account Check (MANDATORY — run on EVERY invocation)
Before doing anything else, determine the active account and verify credentials:
python -c "
import json, pathlib
reg = json.loads(pathlib.Path.home().joinpath('.aws/account-registry.json').read_text())
acct = reg['accounts'][reg['active']]
print(f\"Active: {reg['active']} | {acct.get('description','')} | Account ID: {acct.get('account_id','unknown')} | Region: {acct.get('region','us-east-1')} | Auth: {acct['auth_method']}\")
print(f\"AWS_PROFILE={acct['aws_profile']}\")
"
Then verify credentials:
AWS_PROFILE=<profile> aws sts get-caller-identity --output json
- If creds work → show the banner to the user and proceed.
- If creds fail → run the Credential Refresh procedure for the account's
auth_method(see the Credential Refresh section).
If ~/.aws/account-registry.json does not exist → run First-Time Setup (the First-Time Setup section).
Account Management
List accounts
python -c "
import json, pathlib
reg = json.loads(pathlib.Path.home().joinpath('.aws/account-registry.json').read_text())
for name, acct in reg['accounts'].items():
marker = ' (active)' if name == reg['active'] else ''
print(f\" {name}{marker} — {acct.get('description','')} [{acct['auth_method']}]\")
"
Register a new account — static keys
- Ask the user for: friendly name, description, AWS profile name, region, access key ID, secret access key.
- Run
aws configure --profile <profile>(or write to~/.aws/credentialsdirectly). - Verify with
AWS_PROFILE=<profile> aws sts get-caller-identity. - Capture the account ID from the response.
- Add to registry:
python -c "
import json, pathlib
p = pathlib.Path.home() / '.aws/account-registry.json'
reg = json.loads(p.read_text())
reg['accounts']['<name>'] = {
'description': '<desc>',
'account_id': '<from_sts>',
'aws_profile': '<profile>',
'region': '<region>',
'auth_method': 'static_keys'
}
p.write_text(json.dumps(reg, indent=2))
print('Added.')
"
Register a new account — IAM Identity Center
Prerequisite: the named profile must already exist in ~/.aws/config and reference a configured sso_session, sso_account_id, and sso_role_name.
- Ask the user for: friendly name, description, AWS profile name, and default region.
- Authenticate and verify the profile:
AWS_PROFILE=<aws_profile> aws sso login --use-device-code
AWS_PROFILE=<aws_profile> aws sts get-caller-identity
- Capture the verified account ID and add it to the registry:
python -c "
import json, pathlib
p = pathlib.Path.home() / '.aws/account-registry.json'
reg = json.loads(p.read_text())
reg['accounts']['<name>'] = {
'description': '<desc>',
'account_id': '<from_sts>',
'aws_profile': '<aws_profile>',
'region': '<region>',
'auth_method': 'sso'
}
p.write_text(json.dumps(reg, indent=2))
print('Added.')
"
Identity Center session, start URL, account, and role configuration stay in ~/.aws/config — do not duplicate them into the registry.
Register a new account — SAML SSO (saml2aws)
Prerequisites: saml2aws must be installed. Check with saml2aws --version. If missing, tell the user to install it: choco install saml2aws -y (Windows, elevated PowerShell) or brew install saml2aws (macOS). The agent cannot elevate privileges.
- Ask the user for: friendly name, description, SAML IDP URL, AWS profile name, IAM role ARN, region, session duration (default 3600).
- Create the storage state directory:
mkdir -p ~/.aws/saml2aws
- Configure saml2aws. Always use the Browser provider — programmatic providers (KeyCloak, Okta, etc.) cannot handle MFA flows:
saml2aws configure \
--idp-account=<name> \
--idp-provider=Browser \
--url="<idp_url>" \
--username="<user_email>" \
--profile=<aws_profile> \
--role="<role_arn>" \
--session-duration=<duration> \
--mfa=Auto \
--skip-prompt
- First login — this opens a Chromium browser for the user to complete login + MFA:
saml2aws login --idp-account=<name> --profile=<aws_profile> --download-browser-driver --skip-prompt
The --download-browser-driver flag auto-installs Playwright Chromium on first use. The browser opens for the user to authenticate (enter credentials, complete MFA, select role if needed). After successful auth, saml2aws captures the SAML assertion and writes temporary credentials to ~/.aws/credentials under the named profile.
- Verify:
AWS_PROFILE=<aws_profile> aws sts get-caller-identity. - Capture account ID and add to registry:
python -c "
import json, pathlib
p = pathlib.Path.home() / '.aws/account-registry.json'
reg = json.loads(p.read_text())
reg['accounts']['<name>'] = {
'description': '<desc>',
'account_id': '<from_sts>',
'aws_profile': '<aws_profile>',
'region': '<region>',
'auth_method': 'saml2aws',
'saml2aws_idp_account': '<name>',
'idp_url': '<url>',
'idp_provider': 'Browser',
'role_arn': '<role_arn>',
'session_duration': <duration>
}
p.write_text(json.dumps(reg, indent=2))
print('Added.')
"
Remove an account
Remove from registry only. Do NOT delete AWS profiles or credentials files.
python -c "
import json, pathlib
p = pathlib.Path.home() / '.aws/account-registry.json'
reg = json.loads(p.read_text())
if '<name>' == reg['active']:
print('ERROR: Cannot remove the active account. Switch first.')
else:
del reg['accounts']['<name>']
p.write_text(json.dumps(reg, indent=2))
print('Removed.')
"
Account Switching
When the user asks to switch accounts (e.g., "switch to dnn-dev", "use my personal account"):
- Verify the target account exists in the registry.
- Test credentials:
AWS_PROFILE=<target_profile> aws sts get-caller-identity. - If expired → run the Credential Refresh procedure for the account's
auth_method(see the Credential Refresh section). - Update registry:
python -c "
import json, pathlib
p = pathlib.Path.home() / '.aws/account-registry.json'
reg = json.loads(p.read_text())
reg['active'] = '<target_name>'
p.write_text(json.dumps(reg, indent=2))
print('Switched to <target_name>.')
"
- Confirm by running
AWS_PROFILE=<target_profile> aws sts get-caller-identity.
Safety Rules
Destructive operations require confirmation
Before running any of these, describe what will happen, state which account is affected, and ask the user to confirm:
terminate-instances,delete-*,remove-*,deregister-*drop,destroy,purge- Modifying security groups to open
0.0.0.0/0 - Deleting IAM users, roles, or policies
- Emptying or deleting S3 buckets
Cost-incurring operations require a warning
Before creating resources that cost money, state the expected cost impact and the target account:
- EC2 instances (mention instance type pricing)
- RDS instances, NAT Gateways, ELBs
- Data transfer, EBS volumes
- Example: "This will create a
t3.microinstance (~$8.50/month in us-east-1) on account personal (851725607183). Proceed?"
Cross-account safety
- Always include the active account name and ID in destructive/cost confirmations.
- If the user's request seems to target a different account than the active one (e.g., they mention "dev" resources but the active account is "personal"), warn them and ask if they want to switch first.
Never expose secrets
Do not print access keys, secret keys, passwords, or tokens. Use --query to filter them out, or redact them.
Credential Refresh
Principle: When credentials expire or fail, refresh them using the same method that was originally used to obtain them. This applies at invocation start (the mandatory Active Account Check) AND mid-operation if any aws command returns ExpiredTokenException, ExpiredToken, RequestExpired, or an InvalidIdentityToken error.
Refresh by auth method
Read the account's auth_method from the registry and follow the matching procedure:
sso (IAM Identity Center)
Re-authenticate the existing AWS CLI profile through its configured Identity Center session:
AWS_PROFILE=<aws_profile> aws sso login --use-device-code
Have the user complete the device authorization and MFA flow, then verify with AWS_PROFILE=<aws_profile> aws sts get-caller-identity before retrying the failed command. Do not copy Identity Center role credentials into ~/.aws/credentials; the CLI obtains and caches temporary credentials through the configured SSO session.
saml2aws
Re-run the same login command used during initial registration — this opens the browser for the user to re-authenticate, exactly as they did the first time:
saml2aws login --idp-account=<saml2aws_idp_account> --profile=<aws_profile> --download-browser-driver --skip-prompt
After successful login, retry the command that failed. If saml2aws is not installed, tell the user to install it: choco install saml2aws -y (Windows, elevated PowerShell) or brew install saml2aws (macOS).
static_keys
Static IAM keys do not expire on a timer, but they can be rotated or revoked. When sts get-caller-identity fails for a static-keys account, re-run the same configuration command used during initial setup:
aws configure --profile <aws_profile> # or write to ~/.aws/credentials directly
Prompt the user to enter their new Access Key ID and Secret Access Key, then verify with AWS_PROFILE=<profile> aws sts get-caller-identity.
Mid-operation expiry
If any aws command fails with a credential-expiry error during a multi-step operation:
- Pause the operation.
- Run the refresh procedure for the active account's
auth_method(above). - Verify with
AWS_PROFILE=<profile> aws sts get-caller-identity. - Retry the failed command and continue the operation.
First-Time Setup
Auto-detected when ~/.aws/account-registry.json does not exist:
- Check if
~/.aws/credentialshas a[default]section. - If yes → run
aws sts get-caller-identityto get the account ID. - Create the registry:
python -c "
import json, pathlib
p = pathlib.Path.home() / '.aws/account-registry.json'
p.write_text(json.dumps({
'active': 'personal',
'accounts': {
'personal': {
'description': 'Personal AWS account',
'account_id': '<from_sts>',
'aws_profile': 'default',
'region': '<from aws configure get region>',
'auth_method': 'static_keys'
}
}
}, indent=2))
print('Registry created with personal account.')
"
- Ask if the user wants to add any SSO accounts.
Output and Filtering
Default output is JSON. Use --output table for human-readable display, or --query for JMESPath filtering:
AWS_PROFILE=<profile> aws ec2 describe-instances --query 'Reservations[*].Instances[*].[InstanceId,State.Name,InstanceType]' --output table
Use --no-cli-pager when output is long and you want it inline.
Common Workflows
All commands below must be prefixed with AWS_PROFILE=<active_profile> from the registry.
EC2
# List running instances
aws ec2 describe-instances --filters "Name=instance-state-name,Values=running" --query 'Reservations[*].Instances[*].[InstanceId,InstanceType,PublicIpAddress,Tags[?Key==`Name`].Value|[0]]' --output table
# Launch instance (confirm cost + account first)
aws ec2 run-instances --image-id ami-xxxxx --instance-type t3.micro --key-name MyKey --security-group-ids sg-xxxxx --subnet-id subnet-xxxxx --count 1
# Stop / start / terminate (confirm destructive ops + account)
aws ec2 stop-instances --instance-ids i-xxxxx
aws ec2 start-instances --instance-ids i-xxxxx
aws ec2 terminate-instances --instance-ids i-xxxxx
S3
# List buckets
aws s3 ls
# Sync local directory to bucket
aws s3 sync ./local-dir s3://bucket-name/prefix
# Copy file
aws s3 cp file.txt s3://bucket-name/
# Presigned URL (1 hour)
aws s3 presign s3://bucket-name/file.txt --expires-in 3600
IAM
# List users
aws iam list-users --output table
# List attached policies for a user
aws iam list-attached-user-policies --user-name <username>
# Create a new role
aws iam create-role --role-name MyRole --assume-role-policy-document file://trust-policy.json
Lambda
# List functions
aws lambda list-functions --query 'Functions[*].[FunctionName,Runtime,LastModified]' --output table
# Invoke function
aws lambda invoke --function-name my-function --payload '{"key":"value"}' response.json
# Update function code
aws lambda update-function-code --function-name my-function --zip-file fileb://function.zip
CloudFormation
# List stacks
aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE --output table
# Deploy stack
aws cloudformation deploy --template-file template.yaml --stack-name my-stack --capabilities CAPABILITY_IAM
# Delete stack (confirm first)
aws cloudformation delete-stack --stack-name my-stack
RDS
# List DB instances
aws rds describe-db-instances --query 'DBInstances[*].[DBInstanceIdentifier,Engine,DBInstanceStatus,Endpoint.Address]' --output table
Route53
# List hosted zones
aws route53 list-hosted-zones --output table
# List records in a zone
aws route53 list-resource-record-sets --hosted-zone-id Z1234567890
CloudWatch
# List alarms
aws cloudwatch describe-alarms --state-value ALARM --output table
# Get metrics
aws cloudwatch get-metric-statistics --namespace AWS/EC2 --metric-name CPUUtilization --dimensions Name=InstanceId,Value=i-xxxxx --start-time 2024-01-01T00:00:00Z --end-time 2024-01-02T00:00:00Z --period 3600 --statistics Average
Lightsail
# List instances
aws lightsail get-instances --query 'instances[*].[name,state.name,publicIpAddress,blueprintId]' --output table
# Get instance details
aws lightsail get-instance --instance-name MyInstance
ECS
# List clusters
aws ecs list-clusters
# List services in a cluster
aws ecs list-services --cluster my-cluster
# Describe service
aws ecs describe-services --cluster my-cluster --services my-service
Secrets Manager
# List secrets
aws secretsmanager list-secrets --query 'SecretList[*].[Name,LastChangedDate]' --output table
# Get secret value (be careful with output)
aws secretsmanager get-secret-value --secret-id my-secret --query 'SecretString' --output text
Error Handling
| Error | Cause | Fix |
|---|---|---|
AccessDenied / UnauthorizedAccess |
Missing IAM permission | Check policies via aws iam list-attached-user-policies or aws iam list-attached-role-policies |
ExpiredTokenException / ExpiredToken / RequestExpired / InvalidIdentityToken |
Credentials expired | Run the Credential Refresh procedure for the account's auth_method (see the Credential Refresh section), then retry the command |
ThrottlingException |
API rate limit hit | Wait and retry with exponential backoff |
ResourceNotFoundException |
Resource doesn't exist or wrong region | Verify region with --region flag |
InvalidParameterValue |
Bad input | Check AWS docs for the correct parameter format |
Multi-Region Operations
Use the region from the active account in the registry. For resources in other regions, pass --region:
AWS_PROFILE=<profile> aws ec2 describe-instances --region eu-west-1
For global services (IAM, Route53, CloudFront, S3 bucket creation), region doesn't matter.