clk

A very opinionated framework to ease the creation of command line interfaces

View on GitHub

I have an MCP server running on Amazon Bedrock AgentCore. It sits behind a Cognito user pool, so before calling one of its tools I need an access token, and to get one I log in as a test user, demo-buyer, with its password.

The token lasts an hour. Every hour, I read the password from a plain file with $(cat ...), hand it to aws cognito-idp initiate-auth and paste the token where it goes. Let’s make a command out of it, and get that password out of the file on the way.

a command to get my token

Writing the password on the command line would leave it in my shell history, so the command prompts for it, hiding what I type and asking twice, the click way. In real life, token calls aws cognito-idp initiate-auth. Here, it only says how it logs in.

from clk.config import config
from clk.decorators import group, option


class AgentcoreConfig:
    pass


@group()
@option("--user", default="demo-buyer", help="The Cognito user to log in as", expose_class=AgentcoreConfig)
@option(
    "--password",
    help="The password of that user",
    prompt=True,
    confirmation_prompt=True,
    hide_input=True,
    expose_class=AgentcoreConfig,
)
def agentcore():
    "Talk to my MCP server on Bedrock AgentCore"


@agentcore.command()
def token():
    "Get a fresh access token from Cognito"
    print(f"Logging in as {config.agentcore.user} with password {config.agentcore.password}")

To know more about expose_class=AgentcoreConfig, see dynamic parameters.

not typing the password every hour

Being prompted every hour gets old fast. I could make the password a parameter.

clk parameter set agentcore --password mytoken
New global parameters for agentcore: --password mytoken
clk agentcore token
Logging in as demo-buyer with password mytoken

It works, but the password now sits in plain text in my clk parameters, no better than the file I started from.

Instead, the parameter can tell clk to look the password up in my password manager.

clk parameter set agentcore --password noeval:secret:demo-buyer-password
Removing global parameters of agentcore: --password mytoken
New global parameters for agentcore: --password secret:demo-buyer-password
  1. secret:demo-buyer-password tells clk to get the value from my password manager, under the key demo-buyer-password,
  2. noeval: keeps clk from looking it up right now, so that the parameter holds the reference rather than the password.

I have not stored it yet, so clk fails and says so1.

clk agentcore token
error: Could not find the secret for demo-buyer-password

on a machine where I have not stored it

On a colleague’s laptop, or on mine before I store it, --ask-secret makes clk ask me for the password rather than fail.

clk --ask-secret agentcore token
warning: Could not find the secret for demo-buyer-password
Please provide the secret demo-buyer-password:
Repeat for confirmation:
Logging in as demo-buyer with password test

The error became a warning, and I typed the password.

When I mistype it, clk asks again until both match.

warning: Could not find the secret for demo-buyer-password
Please provide the secret demo-buyer-password:
Repeat for confirmation:
Error: The two entered values do not match.
Please provide the secret demo-buyer-password:

The missing password does not get in the way of completing the command, with or without --ask-secret.

clk agentcore --us<TAB>
--user

--user

storing it in my password manager

clk stores secrets with keyring, hence in whatever password manager keyring is set up to use.

clk secret set demo-buyer-password

It prompts for the password, to keep it out of my shell history too. Let’s say I typed “mytoken”.

clk secret show demo-buyer-password
demo-buyer-password *****

That tells me it is there without putting it on my screen. When I do want to see it, I say so.

clk secret show demo-buyer-password --secret
demo-buyer-password mytoken

And to hand it to another program, the value alone.

clk secret show demo-buyer-password --secret --field secret
mytoken

Now my command gets its password on its own, and I can delete that plain file.

clk agentcore token
Logging in as demo-buyer with password mytoken

Calling a tool of the server is a bash command, whose password defaults to the secret.

clk command create bash mcp-call --description "Call a tool of my MCP server" \
    --option '--password:str:The password of demo-buyer:{"default": "secret:demo-buyer-password"}' \
    --argument 'tool:str:The tool to call' \
    --body 'echo "Calling ${CLK___TOOL} as demo-buyer with password ${CLK___PASSWORD}"'
clk mcp-call get_account_info
Calling get_account_info as demo-buyer with password mytoken

setting up a new laptop

My clk parameters and aliases follow me from laptop to laptop, my password manager does not always. One of those aliases gets the token of demo-buyer whatever the parameters say.

clk alias set buyer-token agentcore --user demo-buyer --password noeval:secret:demo-buyer-password token

To know which secrets to store again, clk lists the ones they refer to, and whether each is there.

clk secret list
key                  status    commands
-------------------  --------  ---------------------
demo-buyer-password  set       agentcore buyer-token
--------------
Legend: global

a check of my own in python

I also wrote a quick python command that logs in with its own http client. It reads the same password with get_secret.

from clk import get_secret

@command()
def dosomething():
    'Log in with my own client'
    click.echo(get_secret('demo-buyer-password'))
clk dosomething
mytoken

when the demo-buyer password is reset

When someone resets the demo-buyer password, I remove the old one.

clk secret unset demo-buyer-password

clk asks first. I hesitate, since the reset may not be done yet, and answer no.

This will definitely remove the secret for demo-buyer-password. Are you sure? [y/N]: Kept the secret for demo-buyer-password

It is still there.

clk secret show demo-buyer-password
demo-buyer-password *****

The reset is done, so this time I answer yes. In a script, --force skips the question.

clk secret unset demo-buyer-password

Asking for it now fails, so a script that reads it notices.

clk secret show demo-buyer-password
error: No secret set

Removing it again tells me there is nothing left to remove.

clk secret unset demo-buyer-password
error: No secret set

And the list reminds me to store the new one.

clk secret list
key                  status    commands
-------------------  --------  ---------------------
demo-buyer-password  missing   agentcore buyer-token
--------------
Legend: global

when an agent runs it for me

These days, a coding agent calls my MCP server as often as I do. It runs in a shell with no desktop session, where keyring has no backend to work with, and it cannot answer a prompt either. There, I keep the password in ~/.netrc: the secret name in machine, its value in password.

machine demo-buyer-password
password thevalue

Sometimes the agent runs in a container where clk was installed without the python library keyring. clk reads my netrc file all the same.

clk secret show demo-buyer-password --secret
demo-buyer-password thevalue
clk secret backend which
clk keeps your secrets in clk.netrc.Netrc, because the python library keyring is not installed.
clk secret backend show
backend          configuration      priority  status
---------------  ---------------  ----------  --------
clk.netrc.Netrc  Unset                     1  in use

Naming a keyring there makes no sense, and clk says so.

clk --keyring clk.keyrings.NetrcKeyring secret show demo-buyer-password
Usage: clk [OPTIONS] [COMMAND] [ARGS]...
error: You need to install keyring in order to manipulate secrets. Hint: python3 -m pip install keyring

Where keyring is installed but finds no password manager, clk falls back on netrc by itself.

clk secret show demo-buyer-password --secret
demo-buyer-password thevalue

When I wonder where a secret comes from, clk tells me where it keeps them, and why.

clk secret backend which
clk keeps your secrets in clk.keyrings.NetrcKeyring, because the python library keyring found no password manager on this machine.

And which keyrings it could use instead.

clk secret backend show
backend                                  configuration      priority  status
---------------------------------------  ---------------  ----------  --------
keyring.backends.chainer.ChainerBackend  Unset                    10
clk.keyrings.GpgKeyring                  Unset                     1
clk.keyrings.NetrcKeyring                Unset                     1  in use
keyring.backends.fail.Keyring            Unset                     0

I can also ask for netrc explicitly, with the NetrcKeyring of clk.keyrings.

clk --keyring clk.keyrings.NetrcKeyring secret show demo-buyer-password --secret
demo-buyer-password thevalue

A secret my netrc file does not hold is simply not there.

clk --keyring clk.keyrings.NetrcKeyring secret show other_token --secret
error: No secret set

It only reads: storing one is up to me, in the file itself.

clk --keyring clk.keyrings.NetrcKeyring secret set other_token
error: Could not save your secret.
Usage: clk secret set [OPTIONS] KEY
error: The netrc keyring only reads secrets. Write this one in your netrc file to use it.

Removing one is up to me as well.

clk --keyring clk.keyrings.NetrcKeyring secret unset demo-buyer-password
Usage: clk secret unset [OPTIONS] KEY
error: The netrc keyring only reads secrets. Remove this one from your netrc file to get rid of it.

when the team shares the password

The team now keeps the demo-buyer password in AWS Secrets Manager, and my role may read it, not write it. I wrote a keyring backend that reads from there. In real life, get_password calls aws secretsmanager get-secret-value.

import keyring.backend


class SecretsManagerKeyring(keyring.backend.KeyringBackend):
    priority = 6

    def get_password(self, service, username):
        return None

    def set_password(self, service, username, password):
        raise NotImplementedError

    def delete_password(self, service, username):
        raise NotImplementedError

Out of habit, I tried storing the new password after it was reset. clk says which keyring refused.

clk --keyring team_keyring.SecretsManagerKeyring secret set demo-buyer-password
error: The keyring team_keyring.SecretsManagerKeyring cannot store secrets. Store it with the tool of that password manager, or pick another keyring with --keyring.
clk --keyring team_keyring.SecretsManagerKeyring secret backend which
clk keeps your secrets in team_keyring.SecretsManagerKeyring, because --keyring names it.

Rather than typing --keyring every time, I tell clk to use it from now on.

clk secret backend use team_keyring.SecretsManagerKeyring
clk now keeps your secrets in team_keyring.SecretsManagerKeyring (global settings)
clk secret backend which
clk keeps your secrets in team_keyring.SecretsManagerKeyring, because the global settings name it.
clk secret backend show
backend                                  configuration      priority  status
---------------------------------------  ---------------  ----------  --------
team_keyring.SecretsManagerKeyring       global                    6  in use
keyring.backends.fail.Keyring            Unset                     0
keyring.backends.chainer.ChainerBackend  Unset                    -1
--------------
Legend: global

keeping it in the project

The demo-buyer password is a throwaway, not worth a place in a password manager. I keep it in the project of my MCP server instead, encrypted with my gpg key.

I give it a try first.

clk --keyring clk.keyrings.GpgKeyring secret set demo-buyer-password
error: Could not save your secret.
Usage: clk secret set [OPTIONS] KEY
error: Write in ./.clk/secrets/.gpg-id the gpg ids to encrypt the secrets to, one per line. gpg --list-secret-keys shows yours.
mkdir -p .clk/secrets
echo me@example.com > .clk/secrets/.gpg-id

Then I make it the keyring of the project. I don’t remember its name, tab does.

clk secret backend --local use clk.keyrings.<TAB>
clk.keyrings.GpgKeyring
clk.keyrings.NetrcKeyring
clk secret backend --local use clk.keyrings.GpgKeyring
clk now keeps your secrets in clk.keyrings.GpgKeyring (local settings)
clk secret set demo-buyer-password
ls .clk/secrets
demo-buyer-password.gpg
clk secret show demo-buyer-password --secret
demo-buyer-password mytoken

Removing it removes the file.

clk secret unset demo-buyer-password
ls .clk/secrets
clk secret show demo-buyer-password
error: No secret set

With no secret left in it, the project goes back to the keyring of my laptop.

clk secret backend --local unuse
clk no longer picks a keyring

On my desktop, the password manager of my session, behind the Secret Service interface, does just as well, and I don’t need to handle gpg ids.

clk secret backend --local use keyring.backends.SecretService.Keyring
clk now keeps your secrets in keyring.backends.SecretService.Keyring (local settings)
clk secret set demo-buyer-password
clk secret show demo-buyer-password --secret
clk secret backend which
demo-buyer-password mytoken
clk keeps your secrets in keyring.backends.SecretService.Keyring, because the local settings name it.

when the demo is over

The demo-buyer user is gone, and so are its parameter and its alias.

clk parameter unset agentcore
clk alias unset buyer-token

I no longer need the team keyring either.

clk secret backend unuse
clk no longer picks a keyring

Once is enough.

clk secret backend unuse
error: The global settings pick no keyring

Without a choice of clk, the python library keyring picks the backend. It first reads the environment variable PYTHON_KEYRING_BACKEND.

PYTHON_KEYRING_BACKEND=clk.keyrings.NetrcKeyring clk secret backend which
clk keeps your secrets in clk.keyrings.NetrcKeyring, because the environment variable PYTHON_KEYRING_BACKEND names it.

Then its configuration file.

mkdir -p "${XDG_CONFIG_HOME}/python_keyring"
cat <<EOF > "${XDG_CONFIG_HOME}/python_keyring/keyringrc.cfg"
[backend]
default-keyring=clk.keyrings.NetrcKeyring
EOF
clk secret backend which
clk keeps your secrets in clk.keyrings.NetrcKeyring, because ./config/python_keyring/keyringrc.cfg names it.

Without either, it takes the backend of highest priority, here the Secret Service of my session.

rm "${XDG_CONFIG_HOME}/python_keyring/keyringrc.cfg"
clk secret backend which
clk keeps your secrets in keyring.backends.SecretService.Keyring, because it has the highest priority of the backends the python library keyring found.

Once the team publishes its Secrets Manager keyring as a package, installing it is enough: keyring finds it beside the Secret Service, and chains the two, asking the team keyring first.

pip install team-keyring
clk secret backend which
clk keeps your secrets in keyring.backends.chainer.ChainerBackend, because it has the highest priority of the backends the python library keyring found.

No command of mine needs a secret any more.

clk secret list
No parameter or alias refers to a secret

Footnotes

1 so that a script calling clk learns that something went wrong