brolly
brolly — British informal for umbrella. One login covers every profile under an AWS SSO session.
A small, pure-Python CLI for AWS IAM Identity Center (SSO): log in once per session, switch accounts and roles
in place, and keep your profiles fresh — without ever touching $AWS_PROFILE.
brolly drives AWS SSO the way the modern [sso-session] config was meant to be used: authenticate once against
a session and every profile that references it is usable. It refreshes and verifies credentials cheaply (the
default), forces a fresh login when you want one, repoints the current profile to a different account/role, or
adds a new profile under an existing session — and it ships a freshness-aware shell-prompt pill so you can see
your credential state at a glance.
brolly verify/refresh the current profile (same as `brolly refresh`)
brolly login [-s <session>] force a fresh device-code login for a session
brolly switch repoint the current profile's account/role
brolly refresh [<profile>] [-s <session>]
brolly add <profile> [-s <session>]
brolly secure enable|disable [-s <session>] opt-in: keep a session's token in your OS keychain
Install
$ uv tool install brolly # recommended
$ pipx install brolly
$ pip install brolly
Opt-in secure mode (tokens in the OS keychain) is built in — no extra to install.
Prerequisites:
- AWS CLI v2 on your
$PATH. brolly shells out toaws sso loginandaws configure set; everything else (listing accounts/roles, resolving tokens) goes through boto3. The AWS CLI is not a pip dependency — install it separately: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html. - Python 3.14+.
- A Unix-like terminal. The arrow-key picker uses
termios/tty, so brolly's interactive menus need a POSIX TTY (Linux, macOS, WSL). Off a TTY it falls back to numeric selection.
Mental model
Modern AWS SSO config in ~/.aws/config has two layers:
[sso-session <name>]— the session. Holdssso_start_url,sso_region, etc. Logging in against a session caches ONE token, keyed by SHA1 of the session name (~/.aws/sso/cache/<sha1>.json).[profile <name>]— a profile references a session viasso_session = <name>and adds its ownsso_account_id/sso_role_name. Any number of profiles can point at the same session.
Because the token is cached per-session, not per-profile, every profile under the same session shares one login — authenticate once and all sibling profiles are usable. (One login covering many profiles is where the name comes from.)
$AWS_PROFILE is a fixed handle you set yourself in each shell. brolly never touches it — it only changes
what a profile resolves to (switch), creates new profiles (add), or verifies a profile's credentials
(refresh, which targets the aws CLI with --profile rather than mutating the ambient env). To actually use
a different profile you still export AWS_PROFILE=<name> yourself. There is no shell wrapper and nothing that
rewrites your environment behind your back.
Commands
brolly
Bare brolly (no subcommand) is shorthand for brolly refresh on the current $AWS_PROFILE — cheap, no
browser unless the session is actually dead.
$ brolly
✔ corp-dev live → arn:aws:sts::111111111111:assumed-role/AdministratorAccess/alex
brolly login
Forces a fresh device-code login for an sso-session, unconditionally — no token check first. Session
defaults to the current $AWS_PROFILE's sso_session; -s/--session targets a different session. It's
session-scoped, not profile-scoped (no profile argument).
This is the rare escape hatch for "I'm still valid but I want a new login anyway" — refresh (and therefore
bare brolly) already logs in automatically whenever a session is actually dead, so day to day you shouldn't
need this.
$ brolly login
$ brolly login -s corp
brolly switch
Interactively repoints the CURRENT $AWS_PROFILE to a different account/role under its own session. Arrow-key
picker (↑/↓ or j/k, enter to select, q/esc/Ctrl-C to quit); shows the account list, then the role
list (skipped if the account has exactly one role). Current account/role are pre-selected. sso_account_id,
sso_role_name, and sso_account_name of that profile are rewritten in place — everything else about the
profile is untouched. Recording sso_account_name is what lets the shell prompt show a friendly account name
instead of the raw ID.
$ export AWS_PROFILE=corp-prod
$ brolly switch
select account ↑/↓ move · enter select · q quit
ACCOUNT NAME
111111111111 corp-prod ← current
▶ 222222222222 corp-staging
select role ↑/↓ move · enter select · q quit
ROLE
▶ AdministratorAccess
✔ corp-prod → 222222222222 (corp-staging) / AdministratorAccess
brolly refresh
The cheap, no-browser daily-driver check. Takes an optional <profile> positional (default: $AWS_PROFILE)
and an optional -s/--session <name> that asserts which sso-session you're operating in (default: the
sso_session of the current $AWS_PROFILE). The target profile must actually belong to the asserted session,
or the command fails loudly — this makes crossing sessions always deliberate:
$ AWS_PROFILE=corp-dev brolly refresh corp-prod
✔ corp-prod live → arn:aws:sts::222222222222:assumed-role/AdministratorAccess/alex
$ AWS_PROFILE=customer-admin brolly refresh corp-prod
profile 'corp-prod' is under session 'corp', not 'customer' — use -s corp to target it
$ AWS_PROFILE=customer-admin brolly refresh corp-prod -s corp
✔ corp-prod live → arn:aws:sts::222222222222:assumed-role/AdministratorAccess/alex
None of this ever touches $AWS_PROFILE: refresh runs aws sts get-caller-identity --profile <target> —
overriding the ambient env var for just that one call — and logs in with --sso-session <session> if needed.
Your shell's $AWS_PROFILE is exactly what it was before; that's the whole point of -s — it lets you check on
(or log into) a profile in another session without poisoning your shell.
Under the hood: get-caller-identity forces credential resolution and lets botocore refresh the hourly SSO
token as a side effect — but only when the token is already lapsed or within ~15 min of expiry (botocore's own
refresh window). If it still has plenty of time left, refresh just confirms you're authenticated without
resetting the clock — that's the intended cheap behavior. Contrast with brolly login, which always does a full
aws sso login unconditionally.
If credentials can't be resolved at all (the 7-day session is dead), it prints a notice, falls through to a
device-code aws sso login, and retries. It also opportunistically backfills sso_account_name on the target
profile if that key is missing (one list_accounts call, made only when absent) — so an existing profile's
prompt name heals itself the first time you refresh it.
brolly add <profile> [-s <session>]
Creates a NEW profile under an existing sso-session, walks the same account/role picker, and leaves it
authenticated and ready to use. -s/--session picks the session (default: the sso_session of the current
$AWS_PROFILE) — there's no cross-session guard here like refresh has, since the profile being written is
new:
$ brolly add corp-qa # session inferred from current $AWS_PROFILE's sso_session
$ brolly add customer-admin -s customer # explicit session
What it does:
- Refuses if
<profile>already exists (tells you to usebrolly switchinstead), or if<session>isn't a knownsso-session(lists the available ones). - Writes a profile skeleton:
sso_session = <session>, plusregion/outputcopied from a sibling profile on the same session if one exists, otherwise the session'ssso_regionandjson. - Ensures a valid token for the session, logging in if needed.
- Runs the account/role picker, then writes
sso_account_id/sso_role_name/sso_account_name.
It does not change $AWS_PROFILE. To use the new profile: export AWS_PROFILE=<name>.
Recovery: if you Ctrl-C out of the picker mid-add, the profile skeleton (step 2) is already written to
~/.aws/config, so re-running brolly add will hit the "already exists" guard. Finish it instead:
$ export AWS_PROFILE=new-account
$ brolly switch # or `brolly` first if the token also expired
Common tasks
| Situation | Command |
|---|---|
| Verify/refresh current creds | brolly |
| Force a fresh login (session healthy but you want a new one) | brolly login |
| Wrong account or role for the current profile | brolly switch |
| Need a new profile under the current session | brolly add <name> then export AWS_PROFILE=<name> |
Need a new profile under a different session, e.g. customer |
brolly add <name> -s customer |
| Refresh a profile in a different session from this shell | brolly refresh <profile> -s <session> |
Interrupted a brolly add mid-picker |
export AWS_PROFILE=<name> then brolly switch |
| Keep a session's token out of plaintext | brolly secure enable -s <session> |
Secure mode (OS keychain)
By default brolly is a thin layer over the stock ~/.aws/sso/cache — the same plaintext token cache the aws
CLI uses. Secure mode is an opt-in that moves the SSO token off disk and into your OS keychain (macOS
Keychain, GNOME Keyring / KWallet, or pass + gpg-agent on a desktop-less Linux box — see
Choosing a backend), then registers brolly as each profile's credential_process so every
SDK and the aws CLI keep working with nothing but $AWS_PROFILE — no shell wrapper, no plaintext token.
It's built in (the keyring library ships as a dependency); it just needs a
keychain backend, which macOS and desktop Linux already have.
brolly secure enable [-s <session>]
Logs the session in (a device-code login brolly runs itself, storing the token in your keychain) and rewrites
every profile under that session to use credential_process. Once the token is safely in the keychain it also
deletes the now-redundant plaintext token from ~/.aws/sso/cache/, so nothing sensitive is left on disk.
Idempotent — re-run it after brolly add to pull new profiles into secure mode (and to clean up any leftover
plaintext token from a session you secured before this behavior existed).
$ brolly secure enable -s corp
To authorize brolly for session 'corp', open:
https://device.sso.us-east-1.amazonaws.com/?user_code=WXYZ-1234
and confirm the code: WXYZ-1234
✓ authorized — SSO token stored in your OS keychain
✓ removed plaintext token cache for session 'corp'
✓ secure mode on for session 'corp' — 3 profile(s) now use the OS keychain
Nothing else about your workflow changes: export AWS_PROFILE=corp-prod and every SDK resolves credentials
through brolly, which pulls the keychain token and vends short-lived role credentials on demand. brolly,
brolly refresh, and brolly switch all keep working and stay in secure mode.
brolly secure login [-s <session>]
Re-authorizes a secured session, refreshing its keychain token in place. brolly normally refreshes silently
using the stored refresh token; reach for this only if the 7-day session has fully lapsed (bare brolly also
triggers it automatically when it finds a dead secured session).
brolly secure disable [-s <session>]
Reverts every secured profile under the session back to a stock plaintext-cache SSO profile and deletes the
token from your keychain — a clean, complete undo of enable.
How it works
- The token (with its refresh token) lives in the keychain under service
brolly-sso, keyed the way botocore keys its own cache (SHA1 of the session name). brolly plugs a keychain-backed cache into botocore's token provider, so silent hourly refresh still happens — no reimplementation, just a different vault. - A secured profile keeps
sso_session(needed to refresh the token) but movessso_account_id/sso_role_nameunderbrolly_sso_*and addscredential_process. That combination deactivates botocore's built-in SSO credential provider so resolution flows through brolly — otherwise botocore would find the (now-absent) plaintext token and fail instead of falling through. - The prompt pill reads a small non-secret expiry sidecar (
<aws-config-dir>/brolly/<sha1>.json) instead of the plaintext cache, so it stays a cheap filesystem check — no keychain access, no secret on disk. - No environment variable to keep exported.
secure enablewrites the chosen backend to~/.aws/brolly/config.json(alongside the sidecars), and everycredential-processcall re-selects it itself — so credential resolution works from any venv, a cron job, or an IDE withoutPYTHON_KEYRING_BACKENDset. (It does run thebrollycommand, so keep brolly on yourPATH.)
Choosing a backend
keyring needs a real backend, and the one that matters for credential_process is one that stays unlocked
for your session — because brolly's credential-process is spawned fresh and non-interactively on every cold
credential resolution, so it can't stop to prompt. macOS Keychain and desktop Linux's gnome-keyring / KWallet
already work that way (unlocked at login by a session daemon). If keyring can't find a backend, brolly says so
and stops rather than failing obscurely.
secure enable auto-detects the backend: it uses a real OS keychain if one is active, otherwise it falls back
to pass when its store is set up. So on most machines you don't name a backend at all — and whatever it picks is
saved to ~/.aws/brolly/config.json and re-applied on every later call. Override with --backend <dotted.path>
when you want a specific one.
Backends live in brolly's own environment — because credential_process runs the brolly executable, which
uses brolly's venv, not the venv of whatever triggered the credential lookup. The pass backend
(keyring_pass) ships with brolly; other backends (e.g. 1Password) you install once alongside brolly
(uv tool install brolly --with <pkg>, or pipx inject brolly <pkg>).
Linux without a desktop (no gnome-keyring / KWallet): use pass + gpg-agent. This is the recommended path —
pass stores each secret gpg-encrypted, and gpg-agent is the session daemon that keeps your key unlocked, so
reads are silent once it's warm. (Encrypting a write needs no passphrase at all, so token refreshes never
prompt.) keyring_pass is bundled, so you only need the pass CLI itself and an initialized store — then
secure enable finds it automatically:
$ sudo apt install pass # the pass CLI (system-wide)
$ pass init <your-gpg-key-id> # initialize the store (brolly then auto-detects it)
$ brolly secure enable -s corp # picks pass, saves it to ~/.aws/brolly/config.json
Unlock gpg-agent once per session. Because credential-process has no TTY, gpg-agent must already be warm
when it runs — a cold cache would fail. Two ways:
- Keep the agent unlocked all session by raising the cache TTL in
~/.gnupg/gpg-agent.conf(max-cache-ttl 34560000), then do onepass show(orbrolly secure enable) in a terminal at login; the first decrypt prompts once (pinentry-curses, no X11 needed) and the agent caches it. - For zero-touch warming at login, preset the passphrase with
gpg-preset-passphrase(addallow-preset-passphrasetogpg-agent.conf) — the same pattern used by borg-backup'sborg-backup-passphrase.
1Password / other vaults. Anything with a keyring backend works — install it into brolly's env and pass its
dotted path to --backend. For example onepassword-keyring with a 1Password service-account token
(OP_SERVICE_ACCOUNT_TOKEN) for non-interactive reads. Note the trade: without the desktop app there's no
biometric unlock, so you're trusting a long-lived service-account token in your environment.
Encrypted-file fallback (last resort). keyrings.alt's EncryptedKeyring needs no daemon, but that's the
problem: it has nothing to keep it unlocked, so it prompts for the master passphrase in every new process —
meaning a prompt on roughly every aws call and a hard failure anywhere non-interactive. Only viable for
occasional interactive use; otherwise the plaintext-cache default is the more honest choice.
$ uv tool install brolly --with keyrings.alt --with pycryptodome
$ brolly secure enable -s corp --backend keyrings.alt.file.EncryptedKeyring
See the keyring docs for the full backend list.
Shell prompt integration
brolly ps1 renders a colored AWS_PROFILE pill for your prompt, reflecting the local, filesystem-only state
of the session token — no network call, no keychain access, no aws/boto3 invocation:
- live (bright orange) — token still valid.
- idle (grey, clock glyph) — cached but lapsed; refreshes automatically on next use.
- gone (red, cross glyph) — no cached token; run
brolly. - plain (neutral grey) — profile has no
sso_session(not an SSO profile).
Add it to your PS1. It needs a Nerd Font for the powerline separators and
glyphs:
export PS1='$(brolly ps1)\u@\h:\w\$ '
It reads whichever store the profile actually uses — the non-secret expiry sidecar for
secure-mode profiles, or the stock plaintext cache otherwise — so it stays accurate
either way, with no configuration. A dead 7-day session can't be detected locally, so it reads as idle rather
than gone. The pill shows <profile> · <account>, preferring the friendly sso_account_name and falling back
to the raw account ID (names get populated by switch, add, and refresh).
Cost is ~10ms per prompt: one short-lived process that reads two local files and deliberately never imports boto3.
How it compares
No single existing tool combines what brolly does. Its niche is the combination: native to the sso-session block
- in-place
switch/add+ never mutating$AWS_PROFILE+ a shipped freshness-aware prompt pill + pure-Python/pip.
- aws-sso-util (Ben Kehoe) is the closest sibling — pure-Python,
config-native, and non-invasive, in the same spirit as brolly. But it has no prompt integration, no in-place
single-profile repoint, and predates the
sso-sessionblock. - granted and aws-sso-cli are excellent tools, but they're Go binaries that install a shell wrapper mutating your shell and generate bulk static profiles. brolly is deliberately thinner: no wrapper, no bulk generation, in-place edits only.
- awsume targets classic IAM role assumption, not Identity Center.
This is positioning, not disparagement. brolly's plaintext default is deliberately thin; when you want tokens in the OS keychain, that's an opt-in secure mode rather than the always-on model of aws-vault, granted, or aws-sso-cli.
Design notes & caveats
Two things to own up front:
switchrewrites~/.aws/configglobally. A profile lives in one shared config file, so switching the account/role of a profile name silently retargets any other shell pinned to the same profile name on its next command. Safe use = one distinct profile name per concurrent context. If you keep two shells on the same account simultaneously, give them different profile names. This is the one real footgun — stated plainly.- Tokens live in the stock plaintext
~/.aws/sso/cache/by default — the very same cache theawsCLI uses. That's the thin-by-design default. When you want tokens off disk, opt into secure mode (brolly secure enable), which stores them in your OS keychain and vends credentials viacredential_process.
Roadmap
- Windows support. The interactive picker uses
termios/tty(POSIX only). Amsvcrt-based key reader would let the menus run natively on Windows. - Passphrase-backend ergonomics for secure mode. Smoother first-run setup for the headless-Linux encrypted- file keyring backend.
Shipped: secure mode (opt-in OS-keychain token storage via credential_process),
a test suite, CI, and publication to the fduplex PyPI org.
License
Apache-2.0 © 2026 Full Duplex Media
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file brolly-0.2.0.tar.gz.
File metadata
- Download URL: brolly-0.2.0.tar.gz
- Upload date:
- Size: 24.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d5df29f884c05129bec44036ad2983bca7934e84a47df384963beda73a48785d
|
|
| MD5 |
9652f488da70b64e3cc75165425a663d
|
|
| BLAKE2b-256 |
1cbef74d69a43aea1c28b3325286f2dbc59b370be64e712b4337153b5f250807
|
Provenance
The following attestation bundles were made for brolly-0.2.0.tar.gz:
Publisher:
release.yml on fduplex/brolly
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
brolly-0.2.0.tar.gz -
Subject digest:
d5df29f884c05129bec44036ad2983bca7934e84a47df384963beda73a48785d - Sigstore transparency entry: 2229088164
- Sigstore integration time:
-
Permalink:
fduplex/brolly@e778ef7fab6e9bdda455fe6c4a22336506fca226 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/fduplex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e778ef7fab6e9bdda455fe6c4a22336506fca226 -
Trigger Event:
release
-
Statement type:
File details
Details for the file brolly-0.2.0-py3-none-any.whl.
File metadata
- Download URL: brolly-0.2.0-py3-none-any.whl
- Upload date:
- Size: 26.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5fff7ee974c31055931386bf515f145750823acc633d6e39089eacd90774eaee
|
|
| MD5 |
e586e5942064918c5b65e49f26230c4c
|
|
| BLAKE2b-256 |
ad2cda56a9ba3c2676dec8cb5d271bd8ccddf7520b9792f9767cee9047d3f53e
|
Provenance
The following attestation bundles were made for brolly-0.2.0-py3-none-any.whl:
Publisher:
release.yml on fduplex/brolly
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
brolly-0.2.0-py3-none-any.whl -
Subject digest:
5fff7ee974c31055931386bf515f145750823acc633d6e39089eacd90774eaee - Sigstore transparency entry: 2229088489
- Sigstore integration time:
-
Permalink:
fduplex/brolly@e778ef7fab6e9bdda455fe6c4a22336506fca226 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/fduplex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@e778ef7fab6e9bdda455fe6c4a22336506fca226 -
Trigger Event:
release
-
Statement type: