aaws - AI-Assisted AWS CLI
Natural language in, AWS command out.
pip install aaws -- pypi.org/project/aaws
Stop context-switching to AWS docs. Describe what you want in plain English and aaws generates, explains, and safely executes the correct AWS CLI command.
$ aaws "list my S3 buckets"
Command: aws s3api list-buckets --output json
Lists all S3 buckets in your account.
Name CreationDate
my-app-assets 2024-03-15T10:22:00+00:00
my-logs-bucket 2024-06-01T08:00:00+00:00
staging-uploads 2025-01-10T14:30:00+00:00
3 result(s)
Table of Contents
- How It Works
- Using with Claude Code (MCP)
- Value Stream: User-Initiated vs Agentic Actions
- Installation
- Quick Start
- Configuration
- Commands Reference
- End-to-End Flows with Examples
- Safety Model
- LLM Providers
- Output Formatting
- Error Recovery
- Architecture
- UX and Agent Experience Assessment
- Development
- License
How It Works
Every aaws invocation follows a six-stage pipeline. Some stages are user-initiated (you trigger them), and others are agentic (the system handles them autonomously). This separation is the core design principle.
YOU aaws (AGENT)
─── ────────────
Type natural language ──────────> LLM translates to AWS CLI command
Agent classifies risk tier (0-3)
Agent selects safety gate
Review command + confirm ───────> Agent executes via subprocess
Agent detects output shape
Agent formats as table/card/JSON
Read formatted results <────────
(on error) Agent classifies error
(on error) Agent generates recovery advice
Read error + suggestion <───────
Using with Claude Code (MCP)
Use aaws with your Claude Code subscription — no API keys, no LLM config, zero extra cost.
Instead of aaws calling an LLM directly, Claude Code becomes the LLM. The aaws MCP server provides safety classification, command execution, and output formatting as tools that Claude Code calls.
Standalone CLI: MCP Mode:
User -> aaws -> LLM (you pay) User -> Claude Code (subscription) -> aaws MCP tools
-> AWS CLI -> AWS CLI
Setup
Prerequisite: AWS CLI v2 must be installed and configured (see Installation above).
# Install with MCP support
pip install aaws[mcp]
# Register with Claude Code (one-time)
claude mcp add --scope user aaws -- python -m aaws.mcp_server
Or add a project-scoped .mcp.json (version-controlled, shared with team):
{
"mcpServers": {
"aaws": {
"command": "python",
"args": ["-m", "aaws.mcp_server"]
}
}
}
Verify with /mcp inside Claude Code to see the tools listed.
Available MCP Tools
| Tool | Purpose | LLM Needed? |
|---|---|---|
classify_aws_command |
Risk tier classification (0-3) for any AWS CLI command | No (static table) |
execute_aws_command |
Safe subprocess execution with profile/region injection | No |
format_aws_output |
JSON shape detection -> plain-text tables/cards | No |
list_safety_tiers |
Browse known command risk tiers by service | No |
check_aws_environment |
Verify AWS CLI, active profile, region | No |
Example Conversation in Claude Code
You: List my S3 buckets in us-west-2
Claude Code:
1. Calls check_aws_environment() -> {aws_cli_available: true, active_profile: "default"}
2. Calls classify_aws_command("aws s3api list-buckets --output json")
-> {tier: 0, tier_label: "Read-only", should_confirm: false}
3. Calls execute_aws_command("aws s3api list-buckets --output json", region="us-west-2")
-> {stdout: '{"Buckets": [...]}', success: true}
4. Calls format_aws_output(stdout)
-> Formatted table with bucket names and dates
You: Now delete the one named old-logs
Claude Code:
1. Calls classify_aws_command("aws s3 rb s3://old-logs --force")
-> {tier: 2, tier_label: "Destructive", should_confirm: true}
2. Asks: "This is a destructive operation (tier 2). Delete bucket old-logs?"
3. You confirm
4. Calls execute_aws_command(...)
What Changes vs Standalone CLI
| Aspect | Standalone CLI | MCP Mode |
|---|---|---|
| LLM provider | You configure (Bedrock/OpenAI) | Claude Code subscription (free) |
| NL translation | aaws translator.py | Claude Code LLM |
| Session memory | In-process, 10-turn limit | Claude Code built-in (full context) |
| Multi-step workflows | One command at a time | Claude Code orchestrates multiple |
| Error interpretation | LLM call per error | Claude Code reasons over stderr |
| Configuration | aaws config init required |
Just register MCP server |
AWS Cloud Engineering Lifecycle (MCP)
| Lifecycle Stage | Standalone CLI | + MCP with Claude Code |
|---|---|---|
| Discovery | Query-based | + Autonomous inventory, cross-service |
| Provisioning | Single-command | + Multi-step with dependency ordering |
| Monitoring | Snapshot queries | + Conversational drill-down |
| Troubleshooting | Hardcoded + LLM errors | + Autonomous log/metric investigation |
| Maintenance | Manual delete/resize | + Agent finds waste, suggests optimization |
| Security | CLI pass-through | + Permission auditing |
| Disaster Recovery | Single-command backup | + Orchestrated DR workflows |
Value Stream: User-Initiated vs Agentic Actions
The following maps every activity in the development and usage lifecycle to who owns it: the user (manual, intentional) or the agent (autonomous, zero-touch).
Setup Phase
| # | Activity | Owner | Description |
|---|---|---|---|
| 1 | Install aaws |
User | pip install aaws |
| 2 | Run config wizard | User | aaws config init — choose provider, model, profile, region |
| 3 | Detect missing config | Agent | If no config exists, prints actionable message and exits |
| 4 | Validate config schema | Agent | Pydantic validates all fields, rejects bad values |
| 5 | Resolve ${ENV_VAR} in config |
Agent | Substitutes environment variable references in YAML values |
| 6 | Apply AAWS_* env overrides |
Agent | Environment variables override file-based config (CI/CD friendly) |
| 7 | Detect AWS CLI presence | Agent | Checks aws in PATH on startup; fails fast with install link |
One-Shot Command Flow
| # | Activity | Owner | Description |
|---|---|---|---|
| 8 | Write natural language request | User | aaws "show my running EC2 instances" |
| 9 | Resolve AWS profile + region | Agent | Merges --profile/--region flags > config > boto3 session > fallback |
| 10 | Build LLM prompt with context | Agent | Injects system prompt + profile/region context + user request |
| 11 | Call LLM via tool/function calling | Agent | Sends structured tool schema, forces tool use (no free-text) |
| 12 | Validate command starts with aws |
Agent | Rejects hallucinated non-AWS output |
| 13 | Auto-retry on invalid command | Agent | Sends corrective instruction, retries once, then fails with clear error |
| 14 | Return clarification if ambiguous | Agent | If request is vague, asks ONE clarifying question instead of guessing |
| 15 | Classify risk tier (static table) | Agent | Longest-prefix match against 100+ known command patterns |
| 16 | Fallback to LLM-assigned tier | Agent | Unknown commands use the LLM's risk assessment |
| 17 | Check protected profile | Agent | Blocks all writes on prod-* or user-defined glob patterns |
| 18 | Show command + explanation | Agent | Displays the generated command with plain-English explanation |
| 19 | Confirm or cancel execution | User | Tier 0: auto-run. Tier 1: y/n. Tier 2: type "yes". Tier 3: refused. --yes auto-confirms 1-2 |
| 20 | Offer --dry-run for EC2 |
Agent | For destructive EC2 commands, offers to validate with --dry-run first |
| 21 | Execute via subprocess | Agent | shlex.split() + subprocess.run(shell=False) — no injection possible |
| 22 | Detect output shape | Agent | Inspects JSON: list -> table, dict -> card, empty -> "No results." |
| 23 | Render formatted output | Agent | Rich tables with column hints per resource type, or syntax-highlighted JSON |
| 24 | Classify error on failure | Agent | Regex matches for credential, permission, resource errors |
| 25 | Provide hardcoded fix for auth | Agent | Expired token -> aws sso login. No creds -> aws configure |
| 26 | LLM-interpret resource errors | Agent | Sends failed command + stderr to LLM for plain-English recovery steps |
Interactive Session Flow
| # | Activity | Owner | Description |
|---|---|---|---|
| 27 | Start session | User | aaws session [--profile X] [--region Y] |
| 28 | Display session header | Agent | Shows active profile, region, exit instructions |
| 29 | Type follow-up requests | User | Conversational input referencing prior context |
| 30 | Maintain conversation history | Agent | Appends each exchange, bounded to last 10 for LLM context |
| 31 | Translate with history context | Agent | LLM sees prior conversation for multi-turn refinement |
| 32 | Full safety pipeline per turn | Agent | Every command goes through classify -> gate -> execute -> format |
| 33 | Exit session | User | Type exit/quit or Ctrl+C |
| 34 | Handle Ctrl+C gracefully | Agent | Catches KeyboardInterrupt, prints "Goodbye.", no stack trace |
Utility Flows
| # | Activity | Owner | Description |
|---|---|---|---|
| 35 | Explain existing command | User | aaws explain "aws ec2 describe-instances --filters ..." |
| 36 | LLM generates explanation | Agent | Describes what the command does, each flag, and safety caveats |
| 37 | View resolved config | User | aaws config show — effective config with secrets masked |
| 38 | Use --raw for scripting |
User | aaws --raw "list my buckets" | jq '.Buckets[].Name' |
| 39 | Use --dry-run to preview |
User | Shows generated command without executing |
| 40 | Override tier-3 refusal | User | aaws --i-accept-responsibility "delete all IAM users" |
CI/CD and Automation
| # | Activity | Owner | Description |
|---|---|---|---|
| 41 | Configure via env vars only | User | Set AAWS_LLM_PROVIDER, AAWS_AWS_REGION, etc. — no config file needed |
| 42 | Pipe raw output to tools | User | aaws --raw "..." | jq ... for scripted consumption |
| 43 | Tests on push (GitHub Actions) | Agent | Lint (ruff) + type check (mypy) + pytest across Python 3.11-3.13 |
| 44 | Publish to PyPI on tag | Agent | hatch build + trusted publishing on v* tags |
Installation
Step 1: Install AWS CLI v2
aaws requires the AWS CLI to be installed and in your PATH. It delegates all AWS operations to the aws command.
macOS:
brew install awscli
Windows:
Download and run the installer from https://awscli.amazonaws.com/AWSCLIV2.msi
Or via winget:
winget install Amazon.AWSCLI
Linux (x86_64):
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
Verify:
aws --version
# aws-cli/2.x.x Python/3.x.x ...
Step 2: Configure AWS Credentials
You need at least one AWS profile configured with valid credentials.
Option A: IAM Access Keys (simplest)
aws configure
You'll be prompted for:
AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE
AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
Default region name [None]: us-east-1
Default output format [None]: json
Option B: AWS SSO (recommended for organizations)
aws configure sso
Follow the browser login flow. Then activate the session:
aws sso login --profile your-profile-name
Option C: Environment Variables (CI/CD)
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
export AWS_DEFAULT_REGION=us-east-1
Verify credentials work:
aws sts get-caller-identity
# Should return your account ID, ARN, and user ID
Step 3: Install aaws
Requirements: Python 3.11+, AWS CLI v2 (configured above)
The package is published at pypi.org/project/aaws.
Standalone CLI (needs an LLM provider — Bedrock or OpenAI):
pip install aaws
With Claude Code MCP support (use your Anthropic subscription, no API key needed):
pip install aaws[mcp]
claude mcp add --scope user aaws -- python -m aaws.mcp_server
Verify the installation:
aaws --help
aws sts get-caller-identity # confirm AWS creds work
Upgrade to latest version
pip install --upgrade aaws
Install from source (development)
git clone https://github.com/farmountain/ai_aws_cli.git
cd ai_aws_cli
pip install -e ".[dev,mcp]"
Quick Start
1. Configure
aaws config init
The wizard walks you through:
aaws configuration wizard
LLM provider [bedrock/openai] (bedrock):
Bedrock model ID (anthropic.claude-3-5-haiku-20241022-v1:0):
Default AWS profile (default):
Default AWS region (us-east-1):
Configuration saved to ~/.config/aaws/config.yaml
Run aaws "list my S3 buckets" to test.
2. Run your first command
aaws "list my S3 buckets"
3. Try more commands
# Read-only (auto-executes, no confirmation)
aaws "show my running EC2 instances in us-west-2"
aaws "how many Lambda functions do I have"
aaws "get the details of my RDS database named prod-db"
# Write operations (asks y/n)
aaws "create an S3 bucket named my-new-bucket in us-east-1"
aaws "tag instance i-abc123 with Environment=staging"
# Preview without executing
aaws --dry-run "terminate instance i-abc123"
# Explain an existing command
aaws explain "aws iam attach-role-policy --role-name MyRole --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess"
# Raw output for scripting
aaws --raw "list my S3 buckets" | jq '.Buckets[].Name'
Configuration
Config file location
| OS | Path |
|---|---|
| Linux/macOS | ~/.config/aaws/config.yaml |
| Windows | %APPDATA%\aaws\config.yaml |
Full config reference
llm:
provider: bedrock # "bedrock" or "openai"
model: anthropic.claude-3-5-haiku-20241022-v1:0
api_key: ${OPENAI_API_KEY} # Only for OpenAI; supports ${ENV_VAR} syntax
temperature: 0.1 # Low for deterministic output
timeout: 30 # Seconds
aws:
default_profile: default
default_region: us-east-1
safety:
auto_execute_tier: 0 # Auto-run commands at or below this tier (0=read-only)
protected_profiles: # Glob patterns — all writes blocked on these profiles
- "prod-*"
- "production"
output:
format: auto # "auto" detects tables/cards/JSON
raw: false # true = always output raw JSON
color: true
Environment variable overrides
Every config field can be overridden with AAWS_-prefixed env vars. Useful for CI/CD where file config is impractical.
| Env Var | Config Field |
|---|---|
AAWS_LLM_PROVIDER |
llm.provider |
AAWS_LLM_MODEL |
llm.model |
AAWS_LLM_API_KEY |
llm.api_key |
AAWS_LLM_TEMPERATURE |
llm.temperature |
AAWS_LLM_TIMEOUT |
llm.timeout |
AAWS_AWS_PROFILE |
aws.default_profile |
AAWS_AWS_REGION |
aws.default_region |
AAWS_SAFETY_AUTO_EXECUTE_TIER |
safety.auto_execute_tier |
AAWS_OUTPUT_FORMAT |
output.format |
AAWS_OUTPUT_RAW |
output.raw |
AAWS_OUTPUT_COLOR |
output.color |
Commands Reference
aaws "<request>"
Translate natural language to an AWS CLI command and run it.
aaws [OPTIONS] "<natural language request>"
| Flag | Description |
|---|---|
--profile, -p |
Override AWS profile for this invocation |
--region, -r |
Override AWS region for this invocation |
--raw |
Output raw JSON (no tables, no formatting) |
--dry-run |
Show the generated command without executing |
--yes, -y |
Auto-confirm tier 1-2 commands (skip interactive prompts) |
--i-accept-responsibility |
Override tier-3 catastrophic operation refusal |
aaws explain "<command>"
Explain what an existing AWS CLI command does.
$ aaws explain "aws s3 rm s3://my-bucket --recursive"
Command: aws s3 rm s3://my-bucket --recursive
This command deletes all objects in the S3 bucket "my-bucket" recursively.
The --recursive flag means it will delete everything inside the bucket, not
just a single object. This is a destructive operation that cannot be undone.
Ensure you have backups before running this command.
aaws session
Start an interactive REPL with conversation context.
$ aaws session --profile dev --region eu-west-1
aaws interactive session
Profile: dev Region: eu-west-1
Type 'exit' or 'quit' to end the session. Ctrl+C to abort.
[aaws]> show my EC2 instances
...table output...
[aaws]> now stop the one named web-server
# LLM sees the prior context and knows which instance you mean
[aaws]> what's the status now?
# Multi-turn conversation continues
[aaws]> exit
Goodbye.
aaws config init
Interactive first-time setup wizard.
aaws config show
Print the resolved effective config with secrets masked.
$ aaws config show
{
"llm": {
"provider": "bedrock",
"model": "anthropic.claude-3-5-haiku-20241022-v1:0",
"api_key": "***",
"temperature": 0.1,
"timeout": 30
},
...
}
End-to-End Flows with Examples
Flow 1: Read-Only Query (Tier 0 - Fully Agentic)
Zero friction. You type, agent does everything.
$ aaws "what EC2 instances are running in us-east-1"
Pipeline:
[User] Types request
[Agent] Translates -> "aws ec2 describe-instances --filters Name=instance-state-name,Values=running --region us-east-1 --output json"
[Agent] Classifies -> tier 0 (aws ec2 describe = read-only)
[Agent] Safety gate -> auto-execute (tier 0, no prompt)
[Agent] Executes subprocess
[Agent] Detects JSON shape -> Reservations -> flattens Instances -> table
[Agent] Renders Rich table
[User] Reads formatted output
InstanceId InstanceType State PublicIpAddress LaunchTime
i-0abc123def t3.micro running 54.123.45.67 2025-03-01T...
i-0def456ghi t3.large running 54.234.56.78 2025-03-15T...
2 result(s)
User actions: 1 (type request). Agent actions: 6 (translate, classify, gate, execute, detect, render).
Flow 2: Write Operation (Tier 1 - User Confirms)
$ aaws "create an S3 bucket named analytics-2025 in us-west-2"
Pipeline:
[User] Types request
[Agent] Translates -> "aws s3api create-bucket --bucket analytics-2025 --region us-west-2 --create-bucket-configuration LocationConstraint=us-west-2"
[Agent] Classifies -> tier 1 (aws s3api create-bucket = write)
[Agent] Shows command + explanation
[User] Confirms y/n
[Agent] Executes, formats output
Command: aws s3api create-bucket --bucket analytics-2025 --region us-west-2 --create-bucket-configuration LocationConstraint=us-west-2
Creates a new S3 bucket named "analytics-2025" in us-west-2.
Run this command? [y/n]: y
Location: http://analytics-2025.s3.amazonaws.com/
User actions: 2 (type request, confirm). Agent actions: 5.
Flow 3: Destructive Operation (Tier 2 - Warning + Explicit Confirm)
$ aaws "terminate instance i-0abc123def"
Pipeline:
[User] Types request
[Agent] Translates -> "aws ec2 terminate-instances --instance-ids i-0abc123def"
[Agent] Classifies -> tier 2 (aws ec2 terminate-instances = destructive)
[Agent] Shows command + explanation
[Agent] Shows WARNING panel
[Agent] Offers --dry-run validation (EC2 commands support it)
[User] Chooses dry-run or types "yes" to confirm
[Agent] Executes
Command: aws ec2 terminate-instances --instance-ids i-0abc123def
Terminates EC2 instance i-0abc123def. This permanently destroys the instance.
+---------- Destructive Operation ----------+
| WARNING This operation is irreversible |
| - it cannot be undone. |
+--------------------------------------------+
Validate first with --dry-run (no changes will be made)? [y/n]: y
Running: aws ec2 terminate-instances --instance-ids i-0abc123def --dry-run
Dry run succeeded - no changes were made.
User actions: 2 (type request, choose dry-run or confirm). Agent actions: 7 (translate, classify, gate, warn, offer dry-run, execute, render).
Flow 4: Catastrophic Operation (Tier 3 - Refused by Default)
$ aaws "delete all objects in s3://production-data recursively"
Pipeline:
[Agent] Translates -> "aws s3 rm s3://production-data --recursive"
[Agent] Classifies -> tier 3 (aws s3 rm + --recursive = catastrophic)
[Agent] REFUSES execution
+---------- Catastrophic Operation Blocked ----------+
| REFUSED This operation is catastrophic and may |
| permanently alter or destroy your AWS account. |
| |
| If you are absolutely certain, re-run with |
| --i-accept-responsibility. |
+-----------------------------------------------------+
To override (with full tier-2 confirmation flow):
$ aaws --i-accept-responsibility "delete all objects in s3://production-data recursively"
Flow 5: Ambiguous Request (Agent Asks for Clarification)
$ aaws "stop that server"
Pipeline:
[User] Types vague request
[Agent] Translates -> clarification needed (no resource ID)
[Agent] Returns clarification question
[User] Reads question, re-runs with specifics
? Which EC2 instance would you like to stop? Please provide the instance ID
(e.g., i-0abc123def) or a name tag to identify it.
In session mode, you can answer directly:
[aaws]> stop that server
? Which EC2 instance would you like to stop?
[aaws]> the one tagged Name=web-server
Command: aws ec2 stop-instances --instance-ids i-0abc123def
Run this command? [y/n]:
Flow 6: Error Recovery (Agent Diagnoses + Suggests Fix)
$ aaws "delete S3 bucket old-logs"
Pipeline:
[User] Confirms deletion
[Agent] Executes -> fails (BucketNotEmpty)
[Agent] Classifies error -> resource error
[Agent] Sends command + error to LLM for interpretation
[Agent] Renders error panel with suggestion
+---------- Error ----------+
| An error occurred |
| (BucketNotEmpty) when |
| calling DeleteBucket: |
| The bucket is not empty. |
| |
| Suggestion: The bucket |
| "old-logs" still contains |
| objects. Empty it first: |
| aws s3 rm s3://old-logs |
| --recursive |
| Then retry the delete. |
+----------------------------+
Flow 7: Credential Error (Agent Provides Hardcoded Fix)
+---------- Error ----------+
| ExpiredTokenException: |
| The security token in the |
| request has expired. |
| |
| Suggestion: Your AWS |
| session has expired. |
| Refresh it with: |
| aws sso login |
| --profile default |
+----------------------------+
No LLM call needed for credential errors -- instant, deterministic fix.
Flow 8: Protected Profile (Agent Blocks Writes)
# config.yaml
safety:
protected_profiles:
- "prod-*"
- "production"
$ aaws --profile prod-us-east-1 "delete instance i-abc123"
Blocked: Profile 'prod-us-east-1' is protected (read-only). Switch profiles to make changes.
Read operations still work:
$ aaws --profile prod-us-east-1 "list my EC2 instances"
# Executes normally (tier 0, read-only)
Flow 9: Interactive Multi-Turn Session
$ aaws session --profile dev
aaws interactive session
Profile: dev Region: us-east-1
Type 'exit' or 'quit' to end the session. Ctrl+C to abort.
[aaws]> show my security groups
GroupId GroupName Description VpcId
sg-abc123 web-sg Web servers vpc-111
sg-def456 db-sg Database vpc-111
2 result(s)
[aaws]> what are the inbound rules for the web one?
# Agent knows "the web one" = sg-abc123 from conversation history
Command: aws ec2 describe-security-group-rules --filters Name=group-id,Values=sg-abc123 --output json
...rules table...
[aaws]> add port 443 to it
Command: aws ec2 authorize-security-group-ingress --group-id sg-abc123 --protocol tcp --port 443 --cidr 0.0.0.0/0
Run this command? [y/n]: y
...success...
[aaws]> exit
Goodbye.
History is bounded to the last 10 exchanges. Session state is in-memory only (never written to disk).
Flow 10: Scripting and Piping (Raw Mode)
# Get instance IDs as plain text for scripting
INSTANCES=$(aaws --raw "list running EC2 instances" | jq -r '.Reservations[].Instances[].InstanceId')
# Use in a loop
for id in $INSTANCES; do
echo "Processing $id..."
done
# Combine with other tools
aaws --raw "show my IAM users" | jq '.Users[] | select(.UserName | startswith("temp-"))' | wc -l
Flow 11: CI/CD Automation with --yes Flag
# In a CI pipeline, skip interactive confirmations for tier 1-2 commands
aaws --yes "tag instance i-abc123 with Environment=staging"
# Combine with --raw for machine-readable output
aaws --yes --raw "create an S3 bucket named build-artifacts-$(date +%s)" | jq .
# Configure entirely via environment variables (no config file needed)
export AAWS_LLM_PROVIDER=bedrock
export AAWS_AWS_REGION=us-east-1
aaws --yes "create a CloudWatch alarm for high CPU on instance i-abc123"
Note: --yes auto-confirms tier 1 (write) and tier 2 (destructive) commands.It does NOT bypass tier 3 (catastrophic) refusal -- that still requires --i-accept-responsibility.
Safety Model
Risk Tiers
| Tier | Label | Examples | Gate | User Action |
|---|---|---|---|---|
| 0 | Read-only | describe, list, get, head |
Auto-execute | None |
| 1 | Write | create, put, update, start, tag |
Show + y/n | Press y (or --yes) |
| 2 | Destructive | delete, terminate, detach, rm |
Warn + type "yes" + dry-run offer | Type yes (or --yes) |
| 3 | Catastrophic | Bulk delete, org-level, IAM nuke | Refused | --i-accept-responsibility (not --yes) |
Classification Strategy
- Tier 3 substring check -- patterns like
aws s3 rm+--recursivealways match catastrophic - Static tier table -- 100+ command prefix-to-tier mappings (longest match wins). Deterministic, zero LLM cost, cannot be overridden by a confused model
- LLM fallback -- for unknown/novel commands, the LLM's own
risk_tierassessment is used
Protected Profiles
Configure glob patterns in safety.protected_profiles. Any write operation (tier > 0) against a matching profile is blocked with a clear error. Case-insensitive matching via fnmatch.
Shell Injection Prevention
Commands are never passed to a shell. shlex.split() tokenizes the command string and subprocess.run() receives a list with shell=False. This is enforced at the architecture level, not by convention.
LLM Providers
AWS Bedrock (default)
Uses your existing AWS credentials. No extra API key, no data leaving your AWS account.
llm:
provider: bedrock
model: anthropic.claude-3-5-haiku-20241022-v1:0
Requires Bedrock model access to be enabled in the AWS Console. If not, aaws detects the AccessDeniedException and tells you exactly where to enable it.
OpenAI
llm:
provider: openai
model: gpt-4o-mini
api_key: ${OPENAI_API_KEY}
Or set OPENAI_API_KEY as an environment variable.
Structured Output
Both providers use tool/function calling to guarantee structured responses (command, explanation, risk_tier, clarification). This eliminates JSON parsing failures from free-text prompting. A fallback JSON parser handles edge cases where tool calling is unavailable.
Output Formatting
| JSON Shape | Rendered As | Example |
|---|---|---|
| Top-level list of dicts | Rich table | EC2 instances, S3 buckets, IAM users |
| Single dict | Key-value card panel | Bucket details, instance metadata |
| Empty list/dict | "No results." | |
| Non-JSON text | Plain text passthrough | aws s3 ls text output |
Any (with --raw) |
Raw stdout | For piping to jq, grep, etc. |
Column hints are built in for 25+ AWS resource types (EC2 Instances, S3 Buckets, IAM Users/Roles/Policies, Lambda Functions, CloudFormation Stacks, ECS Clusters, RDS instances, etc.), ensuring the most useful columns appear first.
Error Recovery
| Error Type | Detection | Recovery | LLM Call? |
|---|---|---|---|
| Expired token | Regex on stderr | aws sso login --profile <name> |
No |
| No credentials | Regex on stderr | aws configure |
No |
| Access denied | Regex on stderr | Shows denied action + profile switch advice | No |
| Bucket not empty | Regex on stderr | LLM explains + suggests aws s3 rm --recursive |
Yes |
| Resource not found | Regex on stderr | LLM interprets error + suggests fix | Yes |
| Resource conflict | Regex on stderr | LLM interprets error + suggests fix | Yes |
| Unknown error | Fallback | Raw stderr in styled error panel | No |
| LLM timeout | Exception catch | "Check your network connection and try again." | No |
Credential and permission errors use hardcoded messages (instant, free, deterministic). Resource errors are sent to the LLM for interpretation (contextual, actionable).
Architecture
src/aaws/
__init__.py # Package version
cli.py # Typer entry point: root command, explain, session, config
translator.py # NL -> AWS CLI via LLM (system prompt, validation, retry)
executor.py # subprocess.run(shlex.split(cmd), shell=False)
formatter.py # JSON shape detection -> Rich tables/cards/JSON
config.py # Pydantic models, YAML loader, env var resolution
errors.py # Error classification, credential messages, LLM interpretation
session.py # Interactive REPL with bounded conversation history
providers/
__init__.py # get_provider() factory
base.py # LLMProvider protocol, TOOL_SCHEMA, LLMResponse, Message
bedrock_provider.py # AWS Bedrock converse API with tool use
openai_provider.py # OpenAI chat completions with function calling
safety/
__init__.py # Package exports
classifier.py # classify(), apply_safety_gate(), is_protected_profile()
tier_table.py # Static dict: 100+ command prefix -> tier mappings
Design Decisions
| Decision | Choice | Rationale |
|---|---|---|
| Execution | subprocess.run over boto3 |
Auditable commands, no 300+ service API to model |
| LLM output | Tool/function calling over prompting | Guaranteed structured schema, no JSON parse failures |
| Safety | Static table + LLM fallback | Deterministic floor (90%+ commands) + flexible fallback |
| CLI framework | Typer + Rich | Type-annotated CLI + production-grade terminal UI |
| Config | Pydantic + YAML + env vars | Validation, file-based defaults, CI/CD-friendly overrides |
| Provider | Bedrock default | Same creds, same account, no extra API key |
| Python | 3.11+ minimum | Not EOL until 2027, match/case, wide platform availability |
UX and Agent Experience Assessment
What works well
- Zero-config for Bedrock users: If you have AWS creds, you have an LLM. No signup, no API key.
- Progressive disclosure of safety: Tier 0 is frictionless, each tier adds exactly one more confirmation step. The escalation is proportional to risk.
- Clarification over guessing: The agent asks rather than hallucinating resource IDs -- this is a critical safety decision.
- Deterministic safety floor: The static tier table means
aws ec2 terminate-instancesis always tier 2, regardless of what the LLM says. The LLM can escalate but never downgrade. - Error recovery is contextual: Credential errors get instant hardcoded fixes (no LLM latency/cost). Resource errors get LLM interpretation. This is the right split.
- Session mode preserves context: "stop the one tagged web-server" works because the agent remembers the prior listing.
- Raw mode for composability:
--rawmakesaawsa good Unix citizen that pipes intojqand scripts.
Areas for improvement
- No command history persistence: Session history lives in memory only. Restarting loses all context. A
~/.aaws/history.jsonwith last N sessions would help. - No streaming output: Long-running commands (e.g.,
aws s3 sync) show nothing until completion. Streaming stdout would improve perceived latency. - No command validation against AWS help: The agent trusts the LLM to produce valid flags. A post-generation check against
aws <service> helpoutput would catch hallucinated flags. - Protected profile patterns are config-only: No
aaws safety add-profile "prod-*"command to manage them interactively. - Single-command-at-a-time: No agentic multi-step workflows (acknowledged as v2 scope).
Agent autonomy balance
The current split is well-calibrated for a CLI tool that runs real infrastructure commands:
- High autonomy for read-only operations and output formatting (no friction for safe actions)
- Shared control for writes and destructive operations (agent prepares, human decides)
- Human-only for catastrophic operations (agent refuses, human must explicitly override)
- Full autonomy for error recovery (agent diagnoses and suggests without asking)
This is the right balance. A more autonomous agent (auto-executing writes) would be dangerous for AWS operations. A less autonomous one (confirming reads) would be annoying.
Development
Setup
git clone https://github.com/farmountain/ai_aws_cli.git
cd ai_aws_cli
pip install -e ".[dev]"
Run tests
pytest --tb=short -q
Lint and type check
ruff check src/ tests/
mypy src/
CI/CD
- Push to
mainor PR: runs tests on Python 3.11, 3.12, 3.13 + lint + type check - Push
v*tag: builds and publishes to PyPI via trusted publishing
Publish a release
git tag v0.1.0
git push origin v0.1.0
# GitHub Actions builds and publishes to PyPI
License
Apache License 2.0 - see LICENSE for details.