- wrapping AWS with clk
- setting default parameters
- managing parameters
- per-project configuration
- switching environments with extensions
- environment variable parameters
- preserving environment variables when no option is given
- completing the buckets of the account I am on
I use AWS a lot at work. The AWS CLI is powerful but verbose. Every command needs --profile to specify which account I’m targeting, and often --region too. I find myself typing things like:
aws --profile company-prod --region eu-west-1 s3 ls s3://some-bucket
aws --profile company-prod --region eu-west-1 ec2 describe-instances
aws --profile company-staging --region eu-west-1 s3 cp file.txt s3://staging-bucket/
This gets old fast. I wanted something where I could set my usual profile once and forget about it, but still be able to switch when needed.
wrapping AWS with clk
I created a clk wrapper around AWS. The idea is simple: a group command that handles the --profile and --region options, then passes them to subcommands via environment variables.
Here’s a simplified version (in real life, the subcommands would call the actual aws CLI).
First, create the group:
clk command create python aws --group --description "AWS CLI wrapper"
Then edit it to add the options. The code looks like this:
from clk.config import config
from clk.decorators import group, option
@group()
@option("--profile", "-p", default="default", help="The AWS profile to use")
@option("--region", "-r", default="us-east-1", help="The AWS region")
def aws(profile, region):
"AWS CLI wrapper with persistent configuration"
config.override_env["AWS_PROFILE"] = profile
config.override_env["AWS_REGION"] = region
config.init()
@aws.group()
def s3():
"S3 operations"
The subcommands are bash scripts that use the environment variables set by the parent group. We can declare arguments directly on the command line:
clk command create bash aws.s3.ls --description "List S3 buckets or objects" \
--argument 'path:str:S3 path to list:{"required": false}' \
--body 'echo "[${AWS_PROFILE}/${AWS_REGION}] aws s3 ls ${CLK___PATH:-}"'
clk command create bash aws.s3.cp --description "Copy files to/from S3" \
--argument 'source:str:Source path' \
--argument 'destination:str:Destination path' \
--body 'echo "[${AWS_PROFILE}/${AWS_REGION}] aws s3 cp ${CLK___SOURCE} ${CLK___DESTINATION}"'
clk command create bash aws.ec2 --description "EC2 operations" \
--argument 'args:str:EC2 command arguments:{"nargs": -1}' \
--body 'echo "[${AWS_PROFILE}/${AWS_REGION}] aws ec2 ${CLK___ARGS}"'
Now I can use it like this:
clk aws --profile company-prod --region eu-west-1 s3 ls s3://prod-bucket
[company-prod/eu-west-1] aws s3 ls s3://prod-bucket
That’s better, but I still have to type the profile every time.
setting default parameters
Here’s where clk shines. I can persist options so they become the default.
clk parameter set aws --profile company-prod --region eu-west-1
New global parameters for aws: --profile company-prod --region eu-west-1
Now I don’t need to specify them anymore:
clk aws s3 ls s3://prod-bucket
clk aws s3 cp backup.sql s3://prod-bucket/backups/
clk aws ec2 describe-instances
[company-prod/eu-west-1] aws s3 ls s3://prod-bucket
[company-prod/eu-west-1] aws s3 cp backup.sql s3://prod-bucket/backups/
[company-prod/eu-west-1] aws ec2 describe-instances
Much cleaner! And when I need to work with staging, I just override:
clk aws --profile company-staging s3 ls s3://staging-bucket
[company-staging/eu-west-1] aws s3 ls s3://staging-bucket
The region is still eu-west-1 from my persisted parameters, only the profile changed.
managing parameters
I don’t have to remember the name of the command I want to configure. The completion offers the ones I have, and ends with a dot those that hold others.
clk parameter set aw<TAB>
aws
aws.
Typing that dot shows what is inside, so I can set the parameters of a subcommand as easily as those of the group.
clk parameter set aws.<TAB>
aws.ec2
aws.s3
aws.s3.
I can check what’s currently set:
clk parameter show aws
aws --profile company-prod --region eu-west-1 -------------- Legend: global
And remove options I no longer want persisted:
clk parameter remove aws --region eu-west-1
clk parameter show aws
Erasing aws parameters --region eu-west-1 from global settings aws --profile company-prod -------------- Legend: global
Or clear everything:
clk parameter unset aws
clk aws s3 ls
Erasing global parameters of aws (was: --profile company-prod)
[default/us-east-1] aws s3 ls
When the line gets long, I would rather open it in my editor than type it all again.
clk parameter edit aws
clk aws s3 ls
New global parameters for aws: --profile company-prod --region eu-west-1
[company-prod/eu-west-1] aws s3 ls
Open it again and quit without touching anything, and clk writes nothing.
clk parameter edit aws
Nothing changed
Empty the file and it gives up rather than clearing them.
clk parameter edit aws
Aboooooort !!
Parameters work for all the commands, parameter itself included, so I can shoot myself in the foot.
clk parameter set parameter set
New global parameters for parameter: set
Every command under parameter has become parameter set, unset the first of them, so I cannot take it back.
clk parameter unset parameter 2>&1
warning: Failed to get the command unset: Command unset not found
Usage: clk parameter set [OPTIONS] CMD [PARAMS]...
error: Invalid value for 'CMD': invalid choice: unset. (choose from alias, aws, command, completion, describe, echo, exec, extension, flowdep, fork, help, log, parameter, pip, plugin, python, secret, update, value)
--no-parameter runs the command I ask for with none of my parameters, those of parameter included.
clk --no-parameter parameter unset parameter
Erasing global parameters of parameter (was: set)
I almost always use ec2 to list the instances, so I set that as its default.
clk parameter set aws.ec2 describe-instances
clk aws ec2
New global parameters for aws.ec2: describe-instances
[default/us-east-1] aws ec2 describe-instances
A few months later, we moved our machines elsewhere and I removed the command. The parameters I had set for it stayed in my profile: they belong to me, not to the command. clk reminds me of them the next time I look.
clk command remove aws.ec2 --force
clk parameter show aws.ec2 2>&1
warning: Failed to get the command aws.ec2: Command aws.ec2 not found warning: You should know that the command aws.ec2 does not exist aws.ec2 describe-instances -------------- Legend: global
I unset them, and nothing is left of that command.
clk parameter unset aws.ec2
Erasing global parameters of aws.ec2 (was: describe-instances)
per-project configuration
Different projects often use different AWS accounts. I can set parameters at the project level so they only apply when I’m in that directory.
mkdir -p webapp-project
cd webapp-project
mkdir .clk
Note that simply creating the .clk dir make webapp-project a project in clk point of view.
clk --project . parameter set aws --profile webapp-prod --region ap-southeast-1
clk parameter show aws
New local parameters for aws: --profile webapp-prod --region ap-southeast-1 aws --profile webapp-prod --region ap-southeast-1 ------------- Legend: local
clk aws s3 ls s3://webapp-assets
[webapp-prod/ap-southeast-1] aws s3 ls s3://webapp-assets
When I leave the project, my global defaults (or lack thereof) take over again:
cd ..
clk aws s3 ls
[default/us-east-1] aws s3 ls
This way, I never accidentally run a command against the wrong account just because I forgot to switch profiles.
switching environments with extensions
Per-project parameters are great when the environment is tied to a directory. But sometimes I want to switch my whole CLI context regardless of where I am — for example, temporarily pointing everything at staging.
Extensions whose name starts with config- are treated specially: their parameters override the global profile instead of providing defaults. This is similar to how hostname-named extensions work, but you control when they are active.
I first set my usual production defaults globally:
clk parameter set aws --profile company-prod --region eu-west-1
New global parameters for aws: --profile company-prod --region eu-west-1
Then I create a config-staging extension that overrides just the profile:
clk extension create config-staging
clk extension enable config-staging
clk parameter --extension config-staging set aws --profile company-staging
New global/config-staging parameters for aws: --profile company-staging
With the extension enabled, the staging profile overrides the global one. The region stays eu-west-1 because the extension doesn’t touch it:
clk aws s3 ls s3://staging-bucket
[company-staging/eu-west-1] aws s3 ls s3://staging-bucket
Command-line flags still win over everything:
clk aws --profile company-dev s3 ls s3://dev-bucket
[company-dev/eu-west-1] aws s3 ls s3://dev-bucket
When I’m done with staging, I disable the extension and the global defaults come back:
clk extension disable config-staging
clk aws s3 ls s3://prod-bucket
[company-prod/eu-west-1] aws s3 ls s3://prod-bucket
This makes it trivial to switch contexts: clk extension enable config-staging and clk extension disable config-staging is all it takes.
environment variable parameters
Sometimes you don’t want to persist parameters in a configuration file. For example, in a CI/CD pipeline or when quickly testing with different settings. You can set parameters through environment variables using the CLK_P_ prefix.
The naming convention is simple: CLK_P_ followed by the command name in uppercase. Dots in command names become underscores, hyphens become double underscores.
Let’s go back to the root directory and try it:
export CLK_P_AWS="--profile env-prod --region us-west-2"
clk aws s3 ls s3://env-bucket
[env-prod/us-west-2] aws s3 ls s3://env-bucket
These parameters are also visible in the command help:
clk aws --help 2>&1 | grep "current parameters"
The current parameters set for this command are: --profile env-prod --region us-west-2
And they show up in parameter show as well:
clk parameter show aws
aws --profile env-prod --region us-west-2 ----------- Legend: env
When you unset the environment variable, the parameters are gone:
unset CLK_P_AWS
clk aws s3 ls
[default/us-east-1] aws s3 ls
preserving environment variables when no option is given
In some setups, AWS_PROFILE and AWS_REGION are already set in the environment (e.g. by a CI pipeline or a .envrc file). I want my wrapper to use those values when I don’t pass the corresponding options, instead of overwriting them with defaults.
To do this, I simply remove the defaults from both options:
from clk.config import config
from clk.decorators import group, option
class AwsProfile:
pass
@group()
@option("--profile", "-p", expose_class=AwsProfile, help="The AWS profile to use")
@option("--region", "-r", help="The AWS region")
def aws(region):
"AWS CLI wrapper with persistent configuration"
config.override_env["AWS_PROFILE"] = config.awsprofile.profile
config.override_env["AWS_REGION"] = region
config.init()
@aws.group()
def s3():
"S3 operations"
Notice that the code still unconditionally assigns both config.override_env["AWS_PROFILE"] and config.override_env["AWS_REGION"]. Yet when those options are not provided, clk knows the values were not given and won’t set the corresponding environment variables. This means the code stays simple — no need for guards — and the existing environment is preserved:
export AWS_PROFILE=from-ci
export AWS_REGION=eu-west-1
clk aws s3 ls
[from-ci/eu-west-1] aws s3 ls
When the options are explicitly passed, they override the environment as expected:
clk aws --profile company-prod --region ap-southeast-1 s3 ls
[company-prod/ap-southeast-1] aws s3 ls
completing the buckets of the account I am on
Every account has its own buckets, every bucket its own objects, and I remember none of them. clk knows the profile, and a callback tells it the bucket I just typed.
from clk.decorators import argument
from clk.types import DynamicChoice
def list_buckets():
"Stands for: aws s3api list-buckets"
return {
"company-prod": ["prod-assets", "prod-logs"],
"company-staging": ["staging-assets"],
}.get(config.awsprofile.profile, [])
def list_objects():
"Stands for: aws s3api list-objects"
return {
"prod-assets": ["logo.png", "style.css"],
"prod-logs": ["2026-09-18.log", "2026-09-19.log"],
"staging-assets": ["logo.png"],
}.get(config.awsprofile.bucket, [])
class Bucket(DynamicChoice):
def choices(self):
return list_buckets()
def remember_bucket(ctx, attr, value):
config.awsprofile.bucket = value
return value
class S3Key(DynamicChoice):
def choices(self):
return list_objects()
@s3.command()
@argument("bucket", type=Bucket(), callback=remember_bucket, help="The bucket to download from")
@argument("key", type=S3Key(), help="The object to download")
def get(bucket, key):
"Download an object"
print(f"[{config.awsprofile.profile}] aws s3 cp s3://{bucket}/{key} .")
clk aws --profile company-prod s3 get <TAB>
prod-assets
prod-logs
clk aws --profile company-prod s3 get prod-logs <TAB>
2026-09-18.log
2026-09-19.log
clk aws --profile company-staging s3 get <TAB>
staging-assets
clk aws --profile company-prod s3 get prod-logs 2026-09-19.log
[company-prod] aws s3 cp s3://prod-logs/2026-09-19.log .