profiles loads and unloads environment profiles in your current terminal.
Instead of one global pile of exports in .zshrc, keep one small file per
context (work, personal, aws-prod, …) in ~/.profiles and switch
between them. Everything a profile adds or changes (env vars, functions,
aliases) is undone again on unload. You never write cleanup code.
$ profiles work
✓ loaded work (+3 vars, ~1 var, 1 function)
$ profiles status
● work ~/.profiles/work.sh
+ AWS_PROFILE=work
+ KUBECONFIG=/Users/me/.kube/work
+ OPENAI_API_KEY=sk-proj****
~ PATH +/Users/me/work/bin
functions: gco
$ profiles unload
✓ unloaded workFor example, use your company's OpenAI and Anthropic keys in work and your
own keys in personal, and never paste the wrong key into the wrong terminal
again.
- Plain shell scripts. A profile is just a file that gets sourced, so it can hold env vars, functions, aliases, or any other shell code.
- Automatic unload. The tool records what each profile changed and puts
it back exactly.
PATH-style lists are handled entry by entry. - Stacking. Load several profiles at once and unload them in any order.
- New terminals. Load a default profile, ask which one to load, or remind
you, so you don't forget. Profiles can claim folders, and a folder's
.envis loaded too. - Tab completion. Profile names complete in zsh, bash and PowerShell, with each profile's description.
- Per terminal. Nothing global changes, and other terminals are unaffected.
- Git sync.
profiles link,pushandpullkeep your profiles in a private repo across machines. - Cross-platform. Works with zsh and bash on macOS and Linux, and with PowerShell on Windows.
Download a prebuilt binary for macOS, Linux or Windows (amd64 / arm64) from
the latest release,
and put profiles somewhere on your PATH:
# macOS on Apple Silicon: use darwin_amd64, linux_amd64, linux_arm64, … for others
curl -fsSL https://github.com/ryanparsa/profiles/releases/latest/download/profiles_darwin_arm64.tar.gz | tar xz profiles
mv profiles ~/.local/bin/Or with Go 1.27+:
go install github.com/ryanparsa/profiles/cmd/profiles@latestOr from source:
git clone https://github.com/ryanparsa/profiles.git
cd profiles
go build -o ~/.local/bin/profiles ./cmd/profiles # any directory on your PATHAdd one line to your shell's rc file, then open a new terminal:
| Shell | File | Line |
|---|---|---|
| zsh | ~/.zshrc (after oh-my-zsh / compinit) |
eval "$(profiles install zsh)" |
| bash | ~/.bashrc |
eval "$(profiles install bash)" |
| PowerShell | $PROFILE |
Invoke-Expression (& profiles install pwsh | Out-String) |
A program can't change its parent shell's environment. So profiles install
prints a small profiles shell function that runs the binary and applies the
code it hands back. It also sets up tab completion.
Without the rc line you can still run one command at a time with
eval "$(profiles load work)" (PowerShell:
profiles load work | Out-String | Invoke-Expression).
profiles new work # creates ~/.profiles/work.sh from a template and opens $EDITOR
profiles work # load it (same as: profiles load work)
profiles status # see what it changed
profiles unload # undo everything
profiles config # e.g. default = "work", so new terminals load itprofiles # list profiles (same as profiles list)
profiles work # load a profile
profiles work aws-prod # load several; they stack
profiles work --no-shared # load without the "shared" profile
profiles .env # load the .env file in this folder
profiles unload aws-prod # unload one…
profiles unload # …or all of them
profiles reload # re-source loaded profiles after editing them
profiles status # what's loaded here and what it changed
profiles list # all profiles (● = loaded in this terminal)
profiles new <name> # create from a template and open $EDITOR
profiles edit <name>
profiles show <name>
profiles diff <name> # preview which env vars loading it would change
profiles cp <src> <dst>
profiles rename <old> <new>
profiles rm <name>... # asks first (-f to skip)
profiles config # edit ~/.profiles/config.toml
profiles install <shell> # print the shell integration (zsh, bash, pwsh)Run profiles <command> --help for details.
Profiles live in ~/.profiles: <name>.sh for zsh and bash, <name>.ps1
for PowerShell.
# work: AWS, kube and AI keys for the day job <- first comment = description in list
# dirs: ~/work <- optional: new terminals here load it
export AWS_PROFILE=work
export KUBECONFIG="$HOME/.kube/work"
export OPENAI_API_KEY=sk-proj-...
export PATH="$HOME/work/bin:$PATH"
alias k=kubectl
gco() { git checkout "$@"; }
# Optional: runs on unload, for cleanup profiles can't do by itself.
profile_unload() {
ssh-agent -k >/dev/null
}PowerShell equivalent:
# work: AWS, kube and AI keys for the day job
$env:AWS_PROFILE = 'work'
$env:OPENAI_API_KEY = 'sk-proj-...'
$env:PATH = "$HOME\work\bin;$env:PATH"
Set-Alias -Scope Global k kubectl
function global:gco { git checkout @args }
function global:profile_unload { }Rules of thumb:
- Use
export. Only exported variables are tracked. The file is sourced inside a function, sodeclare,typesetandlocalcreate local variables. In PowerShell, use$env:and give functions and aliases global scope. - Changes made after loading win. If something else changes a variable after the profiles loaded, unload leaves that variable alone and warns you.
- Overwritten functions stay. Functions the profile defines are removed on unload. If the profile replaced a function that already existed, that function isn't restored.
- Names use letters, digits,
.,_and-, and can't be command names such aslist. - Secrets are masked on screen. For vars whose names contain
KEY,TOKEN,SECRET,PASSWORD,PASSWDorCREDENTIAL,profiles statusandprofiles diffshow only the first 7 characters of long values (so you can tell keys apart, e.g.sk-ant-****) and hide short values entirely.
With the rc line installed, every new terminal decides what to load. It checks these in order:
-
Nothing, if
startup = "none". Onlysharedis loaded. -
A profile that claims the folder the terminal opens in. List folders in the profile's header, separated by commas. Subfolders count too, and the deepest match wins:
# work: AWS, kube and AI keys for the day job # dirs: ~/work, ~/src/acme
-
The
startupmode fromconfig.toml:startupNew terminal "default"(default)loads the defaultprofile and says so; reminds you if none is set"prompt"asks which profiles to load; Enter, Ctrl-C or prompt_timeoutloads none"notice"loads nothing and reminds you: no profile loaded, run: profiles <name>The prompt also runs in subshells and new tmux panes. A profile they inherited is marked
●, and picking another one switches to it. Where no one can answer (scripts,ssh host cmd, VS Code reading your shell's env),promptacts likedefaultand nothing is printed.
Two things come along whatever was picked:
sharedis the base layer for settings you want everywhere. It's created on first run and loaded before any other profile, in new terminals and withprofiles <name>, so other profiles can override it. Turn it off withshared = false, or once withprofiles work --no-shared. After the first run it's a regular file, and deleting it doesn't bring it back..envin the folder the terminal opens in is loaded last, so its values win. It is read asKEY=valuelines (withexport, quotes, comments and$VARexpansion) and never run as code. Keys that would let a cloned repo run code in your shell, likePATH,LD_*,DYLD_*,BASH_ENVorPROMPT_COMMAND, are refused. It shows up inprofiles statusand goes away withprofiles unload .env. Load one by hand withprofiles .env, or turn this off withdotenv = false.
profiles config opens ~/.profiles/config.toml:
startup = "default" # new terminals: "default", "prompt", "notice" or "none"
default = "personal" # the profile startup = "default" loads (unset: none)
shared = true # load the "shared" profile before every other profile
dotenv = true # load the .env of the folder a new terminal opens in
prompt_timeout = 0 # seconds before the prompt gives up (0: wait)
editor = "code -w" # default: $VISUAL, then $EDITOR, then vi / notepad
confirm_delete = true # ask before `profiles rm`
quiet = false # hide the ✓ messages and new-terminal noticesEvery key can also be set from the environment with a PROFILES_ prefix, for
example PROFILES_QUIET=true or PROFILES_STARTUP=prompt. Invalid values are
reported as errors. To use a directory other than ~/.profiles, set
PROFILES_DIR. Colors in list and status are turned off when NO_COLOR
is set.
profiles link git@github.com:you/my-profiles.git # first machine: commits and pushes your profiles
profiles link git@github.com:you/my-profiles.git # another machine: checks them out
profiles push # commit everything and push
profiles pull # pull --rebase, then says which loaded profiles to reload
profiles git log --oneline # run any git command in ~/.profilesWarning
Profiles usually contain secrets. Only sync them to a private repository.
profiles uses your system git, so your existing SSH keys and credential
helpers work. Profiles named *.local.sh / *.local.ps1 are git-ignored, so
they stay on one machine. After a pull, profile files are reset to mode
0600.
profiles load workrecords a snapshot of the shell: its exported environment plus the names of its functions and aliases.- It sources
work.shand records a second snapshot. - The difference is stored in the
__PROFILE_STATEenvironment variable. That keeps state per terminal, lets subshells inherit it, and means no state files go stale on disk. profiles unload workturns that record back into shell code. It runsprofile_unloadif the profile defines one, restores or unsets variables, and removes the profile's functions and aliases.
Issues and pull requests are welcome. See CONTRIBUTING.md for building, testing, and how the internals fit together.
Copyright 2026 Ryan Parsa
Licensed under the Apache License, Version 2.0.