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>]
Install
$ uv tool install brolly # recommended
$ pipx install brolly
$ pip install brolly
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 |
Shell prompt integration
brolly ships a drop-in __aws_ps1 function for your ~/.bashrc that renders a colored AWS_PROFILE pill
reflecting the local, filesystem-only state of the session token — no network call, no aws/boto3
invocation on every prompt:
- live (bright orange) — hourly 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).
A dead 7-day session can't be detected locally, so it still reads as idle rather than gone. The pill also
shows the account: <profile> · <account>, where <account> is the friendly sso_account_name when the
profile has one, falling back to the raw sso_account_id if not. Names get populated by switch, add, and
refresh; until one of those runs against a given profile, the pill just falls back to the raw account ID.
Drop this into your ~/.bashrc and add $(__aws_ps1) to your PS1. It needs a Nerd Font
for the powerline separators and glyphs.
__aws_ps1() {
[[ -z $AWS_PROFILE ]] && return
# Cheap, filesystem-only freshness check — no aws/boto3 call, no network.
# live = hourly token still valid · idle = cached but lapsed (refreshes on next use) · gone = no token, must log in.
# The 7-day session's true death can't be known locally, so a dead session reads as "idle", not "gone".
local cfg="${AWS_CONFIG_FILE:-$HOME/.aws/config}"
local session acct cache exp now state sbg sfg glyph
IFS=$'\t' read -r session acct < <(awk -v h="[profile $AWS_PROFILE]" '
$0==h {i=1; next}
/^\[/ {i=0}
i && /^[[:space:]]*sso_session[[:space:]]*=/ {v=$0; sub(/^[^=]*=[[:space:]]*/,"",v); s=v}
i && /^[[:space:]]*sso_account_name[[:space:]]*=/ {v=$0; sub(/^[^=]*=[[:space:]]*/,"",v); n=v}
i && /^[[:space:]]*sso_account_id[[:space:]]*=/ {v=$0; sub(/^[^=]*=[[:space:]]*/,"",v); a=v}
END {print s "\t" (n!=""?n:a)}' "$cfg" 2>/dev/null)
if [[ -z $session ]]; then
state=plain
else
cache="$HOME/.aws/sso/cache/$(printf %s "$session" | sha1sum | cut -c1-40).json"
if [[ ! -f $cache ]]; then
state=gone
else
exp=$(sed -n 's/.*"expiresAt"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$cache" | head -1)
printf -v now '%(%s)T' -1
exp=$(date -d "$exp" +%s 2>/dev/null)
[[ -n $exp && $exp -gt $now ]] && state=live || state=idle
fi
fi
case $state in
live) sbg=214 sfg=236 glyph='' ;; # bright AWS orange — creds are live
idle) sbg=240 sfg=214 glyph=$' ' ;; # grey clock — lapsed, will refresh on next use
gone) sbg=160 sfg=231 glyph=$' ' ;; # red cross — no cached token, run brolly
plain) sbg=238 sfg=250 glyph='' ;; # non-SSO profile — neutral
esac
# powerline pill: amazon-glyph | profile ; \001/\002 wrap non-printing bytes for correct prompt-width math
local E=$'\033' A=$'\001' B=$'\002' PL=$'' AWS=$''
printf '%s%s%s%s%s' \
"${A}${E}[48;5;238;38;5;214;1m${B} ${AWS} " \
"${A}${E}[38;5;238;48;5;${sbg}m${B}${PL}" \
"${A}${E}[48;5;${sbg};38;5;${sfg};1m${B} ${glyph}${AWS_PROFILE}${acct:+ · $acct} " \
"${A}${E}[0;38;5;${sbg}m${B}${PL}" \
"${A}${E}[0m${B} "
}
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 — if you want OS-keychain-encrypted tokens today, reach for aws-vault, granted, or aws-sso-cli (see caveats below and the roadmap).
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/— the very same cache theawsCLI uses. brolly adds no encryption of its own; it's a thin layer over the stock cache by design. For OS-keychain encryption today, use aws-vault / granted / aws-sso-cli. Encrypted storage is the lead roadmap item below.
Roadmap
- Keychain-backed token storage via a
credential_processmode. An opt-in mode where brolly does the device auth itself, stores the SSO token in the OS keychain (via the Pythonkeyringlibrary), and registers itself as each profile'scredential_process— so credentials never sit in plaintext on disk and SDK consumers stay fully transparent (justAWS_PROFILE, no shell wrapper). The prompt pill's freshness check would then read a small non-secret expiry sidecar instead of the plaintext cache. On headless Linux with no OS keychain,keyringfalls back to an encrypted-file backend guarded by a passphrase. This mode makes brolly "take over credential resolution," which is exactly the trade the plaintext default avoids — so it stays opt-in, keeping brolly a thin stock- cache layer by default. - Test suite, CI, and publish to the
fduplexPyPI 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.1.0.tar.gz.
File metadata
- Download URL: brolly-0.1.0.tar.gz
- Upload date:
- Size: 12.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
91f84c9468fba46b8122dbe98a3166b29010862319a19923276f50fa591594c1
|
|
| MD5 |
7f7acb29b6b444e55049926892a01c39
|
|
| BLAKE2b-256 |
af4b98f7c6b6ef38c57d64e1a7d5eccfd1cebe42819dfa143a176e313f2dc3f6
|
Provenance
The following attestation bundles were made for brolly-0.1.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.1.0.tar.gz -
Subject digest:
91f84c9468fba46b8122dbe98a3166b29010862319a19923276f50fa591594c1 - Sigstore transparency entry: 2227577362
- Sigstore integration time:
-
Permalink:
fduplex/brolly@1c019112f6d7cf868e87ed5f832b83feea395d89 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/fduplex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1c019112f6d7cf868e87ed5f832b83feea395d89 -
Trigger Event:
release
-
Statement type:
File details
Details for the file brolly-0.1.0-py3-none-any.whl.
File metadata
- Download URL: brolly-0.1.0-py3-none-any.whl
- Upload date:
- Size: 13.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 |
1b22187142deee5b437ee3c430e092449ac2b0befc2bd1caae999b709ad1c7e6
|
|
| MD5 |
2552f85b76c8f5711c514b818322299c
|
|
| BLAKE2b-256 |
d34ca9509be9f129717f9010795ce0e8c24256dd04ec7ba2a0f4187be7316c5b
|
Provenance
The following attestation bundles were made for brolly-0.1.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.1.0-py3-none-any.whl -
Subject digest:
1b22187142deee5b437ee3c430e092449ac2b0befc2bd1caae999b709ad1c7e6 - Sigstore transparency entry: 2227577919
- Sigstore integration time:
-
Permalink:
fduplex/brolly@1c019112f6d7cf868e87ed5f832b83feea395d89 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/fduplex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@1c019112f6d7cf868e87ed5f832b83feea395d89 -
Trigger Event:
release
-
Statement type: