Run a CLI with a group of environment variables — resolving the secret-bearing
ones from your OS keychain at the moment the target starts. The config holds
references, never values; nothing is sourced into your shell; and the target
is handed off with exec(2), so its PID, TTY, signals, and exit code behave
exactly as if it were launched directly.
$ kgx deepseek claudekgx is how you run things day to day; kg run deepseek claude is the long
form of the same command, just as uv tool run is the long form of uvx. Two
binaries: kg carries the whole surface (management verbs plus run), kgx
is run-only. One invocation can also merge several profiles —
kgx aws,gcp terraform pulls in aws and gcp at once.
- Why keygrp
- Features
- Install
- Quick start
- Usage
- Configuration
- Secrets
- Shell completion
- Security model
- Platform support
- Limitations
- Documentation
- Development
- License
Environment variables are the standard way to configure CLIs, but putting secrets in them has two problems: they sit in your shell — and every process you spawn — whether you need them or not, and they are unversioned by nature.
keygrp flips the default:
- Secrets live in the OS keychain, referenced — never stored — in a TOML config.
- On-demand injection: a secret only reaches a process when a profile that references it is run.
- No shell-init loading: profiles are never sourced into your shell.
- Keychain-backed secrets — store once with
kg secret set, reference from any number of profiles. Values never touch the config or shell history; the only stdout exception is the explicitkg secret get --revealopt-in. - Profile composition — inherit variables across profiles with
extends, and merge several profiles at run time with comma-separated combinations. - True
exechandoff — the target replaces kg's process image, so there is no wrapper process, no PID indirection, and no post-run hooks to get in the way. - Shell completion for fish, zsh, and bash — including delegating to the target command's own completion after the profile position.
- Encrypted backup & restore —
kg secret export/kg secret importmove your secrets between machines in a password-encrypted archive. kgxto run,kgto manage — the day-to-day invocation iskgx <profile> <program>; thekg runlong form and the management verbs (secret,check,init,completion) live onkg.
Requires Go 1.26+. Install both binaries straight from GitHub — no checkout
needed; the cmd/... pattern covers every command under cmd/:
$ go install github.com/mrchi/keygrp/cmd/...@latestPrefer to build from a checkout? go install ./cmd/... also works.
Upgrading from the pre-split keygrp binary? Uninstall it after switching:
$ rm "$(command -v keygrp)"-
Initialize —
kg initwrites a commented starter config at~/.config/keygrp/config.tomlif none exists, installs shell completion for bothkgandkgx, and runs a keychain probe so the macOS authorization dialog appears now rather than mid-setup:$ kg init -
Add a profile — edit
~/.config/keygrp/config.tomland declare the environments you want to run with:[profiles.deepseek] DEEPSEEK_API_KEY = "keychain://deepseek-api-key" [profiles.aws] AWS_ACCESS_KEY_ID = "keychain://aws-access-key-id" AWS_SECRET_ACCESS_KEY = "keychain://aws-secret-access-key" AWS_REGION = "ap-southeast-1" [profiles.gcp] GOOGLE_APPLICATION_CREDENTIALS = "keychain://gcp-credentials"
-
Store the secrets — one
kg secret setper keychain ref in the config, each with the same prompt-and-confirm flow:$ kg secret set deepseek-api-key Enter value for "deepseek-api-key": ******** Confirm value for "deepseek-api-key": ******** stored "deepseek-api-key"
Repeat for
aws-access-key-id,aws-secret-access-key, andgcp-credentials. -
Validate everything resolves without running anything:
$ kg check all refs resolve
-
Run a target with the profile's environment:
$ kgx deepseek claude -
Combine profiles — merge several profiles' env in one invocation, no config edit needed:
$ kgx aws,gcp terraformThe members merge under the same no-shadowing rule as
extends— see Combining profiles at run time for the rules.
| Command | Description |
|---|---|
kgx [--verbose] <combination> <program> [args...] |
run <program> with <combination>'s env — the recommended, day-to-day invocation (profiles comma-separated, e.g. aws,gcp); --verbose prints each injected variable with its origin profile |
kg run [--verbose] <combination> <program> [args...] |
the long form of kgx |
kg secret set [--stdin] <ref> |
store a secret in the keychain (hidden prompt, verify by re-entry; --stdin for piped input) |
kg secret get [--reveal] <ref> |
show whether a secret exists; --reveal prints its value |
kg secret delete <ref> |
remove a secret, with confirmation |
kg secret list |
list stored secret refs, flagging any missing from the keychain |
kg secret export [<file>] |
write all secrets to a password-encrypted archive (- for stdout) |
kg secret import [--skip-existing] [<file>] |
restore secrets from an archive (- for stdin) |
kg check [--profile <combination>] |
validate config and keychain refs without running anything |
kg init [--shell fish|zsh|bash] |
install completion, create a starter config, authorize the keychain |
kg completion fish|zsh|bash |
print a completion script |
Exit codes: 0 ok · 1 configuration error · 2 usage or keychain error.
After handoff the target's exit code is returned verbatim. Every verb accepts
--help.
Path: ~/.config/keygrp/config.toml, overridden by $KEYGRP_CONFIG. A single
file — there is no multi-file merging. kg init writes a comment-only starter
here when none exists, and never edits an existing config.
| Form | Example | Behavior |
|---|---|---|
| keychain ref | KEY = "keychain://ref" |
resolved from keychain at run time |
| plaintext | REGION = "ap-southeast-1" |
injected directly (non-secrets) |
- Ref names are global across profiles:
[profiles.aws]and[profiles.terraform]can both referencekeychain://aws-access-key-id. - Profile values override any value already in your environment, unconditionally.
- A missing keychain item is a fail-fast error: the target is not started.
- Non-string values are a parse error; unknown keys within a profile are accepted; unknown top-level tables are ignored.
A profile may inherit another profile's variables with the reserved extends
key — a profile name, or an array of names:
[profiles.aws]
AWS_ACCESS_KEY_ID = "keychain://aws-access-key-id"
[profiles.terraform]
extends = "aws" # inherit aws's variables
TF_TOKEN = "keychain://terraform-token"A profile's effective variable set is the union of its own variables and
every reachable base's — the transitive closure of extends, deduplicated by
profile (a diamond collapses to one copy). Rules:
- No shadowing — two distinct declarations of the same variable name within
one profile's reachable set is a configuration error, judged by name
regardless of value. A conflict invalidates only the profile that reaches it;
kg checkreports all conflicts,kg runfails fast. - All-or-nothing — a derived profile takes every variable of its bases; there is no exclusion syntax.
- Fail fast at load — a missing base or an
extendscycle is a configuration error, detected before any program lookup or keychain access, so a broken chain never triggers a keychain prompt. extendsis consumed by kg, never injected.
Name several profiles at once to merge their environments for a single invocation — no config edit needed:
$ kgx aws,gcp terraform
# kg run aws,gcp terraform is the long formEach member resolves its own extends chain, then the members merge under the
same no-shadowing rule as extends — a shared base collapses to one copy; two
declarations from distinct origins is a configuration error. Order does not
matter (aws,dev ≡ dev,aws); profile names cannot contain commas; a leading,
trailing, or double comma is a usage error. kg check --profile aws,gcp
validates a combination without running anything.
Secrets are stored in the OS keychain under service keygrp, with the ref as
the account. Because the keychain backend cannot list items, kg tracks written
refs in a registry file (<config dir>/refs, written 0600) that backs
kg secret list and the export/import commands.
kg secret export captures every ref's value into a single
password-encrypted archive (keygrp-secrets.kgx by default; - writes to
stdout), and kg secret import restores it (- reads from stdin) — so moving
secrets to a new machine is one ssh pipe away:
$ kg secret export - | ssh host kg secret import -The archive is a versioned envelope: PBKDF2-HMAC-SHA256 key derivation
(600,000 iterations), AES-256-GCM encryption, and the header bound as
authenticated data so a tampered or wrong-password archive fails decryption
rather than silently downgrading. See
docs/adr/0005-secret-export-import.md
for the full format.
Run kg init in the shell you use (or kg init --shell <name> to target
another). It detects the current shell, writes a comment-only starter config at
the config path if none exists, installs the completion script — which registers
both kg and kgx — and runs a read-only keychain probe so the macOS
authorization dialog appears now rather than at first secret set.
zsh needs one manual step kg cannot do for you — the script is written to
~/.zfunc/_kg, but zsh only loads completion dirs listed in fpath:
$ printf 'fpath+=(~/.zfunc)\nautoload -Uz compinit\ncompinit\n' >> ~/.zshrcWhat completion covers:
- after
kg— the verbs (run,secret,check,init,completion) and--help; - after
kg run/ afterkgx— profile names (a partial combination such asaws,<TAB>completes the remaining profiles, excluding already-selected ones); - after
kg secret— the operation and its refs/flags (secret exportandsecret importcomplete the<file>position as a file path); - after
kg run <profile>/ afterkgx <profile>— command names; - after
kg run <profile> <command>— the command's own completion, delegated to it (the target must have its completion installed).
Completion is generated from the config and refs registry at every <TAB>, so
config and secret changes never require regenerating. Regenerate only when
kg's own command structure changes: re-run kg init — it installs completion
for both kg and kgx, writing a kgx companion autoload file alongside the
primary for fish and bash (those shells load completion files by command
name). Placing the script by hand with kg completion <shell> > file installs
kg only and skips the kgx companion.
- Secrets are never written to the config, to stdout (unless
kg secret get --reveal), or to shell history — inline--valueis rejected. - The refs registry holds only ref names, never values. It can drift if a
keychain item is deleted by hand;
kg secret listflags the discrepancy. - Injection happens only at
exec; a secret's memory lifetime equals the target process. - The guarantee is scoped to keygrp-managed secrets: pre-existing secrets you
have already
exported in your shell are inherited unchanged — kg adds no new leak surface, but does not scrub the one you already have. - Profiles are never loaded at shell init.
macOS and Linux. The exec(2) handoff is POSIX; secrets use the macOS
Keychain / Linux Secret Service via
go-keyring. Windows is out of scope.
<program>is an executable onPATH; shell aliases and functions are bypassed.- No post-run hooks or logging (a deliberate consequence of
exechandoff).
The domain model and glossary live in
CONTEXT.md; design decisions are recorded as ADRs in
docs/adr/:
| ADR | Topic |
|---|---|
| 0001 | overall design & environment override rules |
| 0002 | shell completion & init |
| 0003 | profile extends |
| 0004 | run-time profile combination |
| 0005 | encrypted export / import |
| 0006 | completion candidate descriptions |
| 0007 | the kg / kgx CLI contract |
| 0008 | per-verb --help |
| 0009 | manual tag-push release with cross-compiled binaries |
| 0010 | platform scope: ubuntu CI, no Windows |
Requires Go 1.26+. Build and test from a checkout:
$ go build ./cmd/...
$ go test ./...Releases are manual and tag-driven. When main is green, cut one with
git tag vX.Y.Z && git push origin vX.Y.Z. The release workflow builds kg and
kgx for darwin/amd64, darwin/arm64, and linux/amd64, and attaches tarballs
plus sha256sums.txt to a GitHub Release for the tag (see docs/adr/0009).
The issue tracker and spec live under
.scratch/keygrp/. Contributions are welcome — open an
issue or a pull request. Keep changes scoped, update the relevant ADR when
behavior changes, and make sure the test suite passes. Commits follow the
Conventional Commits spec.