A CLI tool for managing encrypted environment variables with per-service isolation.
kagi (鍵, Japanese for "key") keeps your secrets encrypted at rest using XChaCha20-Poly1305 while making them easy to inject into applications during development and deployment.
- Encrypted secrets at rest with XChaCha20-Poly1305.
- Team-ready by default: one developer is just one member.
- Service and environment scopes like
api/developmentandweb/production. developmentis the default environment, so daily commands stay short.- Nested service inference lets
kagi run bun devwork inside./api. .kagi/is designed to be committed; private keys stay on each device.get --showandexportrequire terminal confirmation before revealing values.
cargo install --git https://github.com/BANG88/kagi.gitRequires Rust 1.85+ (2024 edition).
git clone https://github.com/BANG88/kagi.git
cd kagi
cargo install --path .kagi init --nested --envs--envs without a value creates the standard environments:
development, test, and production.
development is the default, so you usually do not type it. --nested lets
kagi infer the service from the folder you are in.
Commit the generated .kagi/ files:
git add .kagi .gitignore
git commit -m "chore: initialize kagi"Private keys are not written to .kagi/.
From the repository root:
kagi set api DATABASE_URL postgres://localhost/api
kagi set api production DATABASE_URL postgres://db/prodInside ./api:
kagi set DATABASE_URL postgres://localhost/api
kagi set production DATABASE_URL postgres://db/prodBoth short commands write to the same scopes:
| Command | Scope |
|---|---|
kagi set api DATABASE_URL ... |
api/development |
kagi set DATABASE_URL ... inside ./api |
api/development |
kagi set api production DATABASE_URL ... |
api/production |
kagi set production DATABASE_URL ... inside ./api |
api/production |
kagi get
kagi get api
kagi get api productionget lists services, environments, and keys with masked values. Reveal values
only when you really need them:
kagi get api --show
kagi get api DATABASE_URLBoth commands require an interactive y confirmation.
From the repository root:
kagi run api bun dev
kagi run api production bun startInside ./api:
kagi run bun dev
kagi run production bun startkagi run injects the selected environment variables into the child process.
For shell syntax such as pipes, redirects, or $VAR expansion, run a shell
explicitly:
kagi run api sh -c 'echo "$DATABASE_URL" | wc -c'git add .kagi
git commit -m "chore: update kagi secrets"Do not commit real .env files. kagi init updates .gitignore so .env,
.env.*, and local private material stay out of Git.
| Task | Command |
|---|---|
| Initialize with standard envs | kagi init --nested --envs |
| Set development secret | kagi set api KEY value |
| Set production secret | kagi set api production KEY value |
| List masked keys | kagi get |
| Reveal listed values | kagi get api --show |
| Run app with development env | kagi run api bun dev |
| Run app from inside service folder | kagi run bun dev |
| Add an environment | kagi env add staging |
| Rename an environment | kagi env rename staging preview |
| Delete an environment | kagi env del preview |
| Import an env file | kagi import api --file .env.local |
| Export all service envs | kagi export api --out . |
| Sync missing keys from example | kagi sync --service api |
Use --service <name> when a shortcut would be ambiguous:
kagi set --service api production DATABASE_URL postgres://db/prod
kagi run --service api production bun startEnvironment names cannot conflict with existing service names.
Import existing local files:
kagi import api --file .env.development
kagi import api production --file .env.productionExport creates normal runtime files when needed:
kagi export api --out .That writes one file per environment:
.env.development
.env.test
.env.production
Exporting decrypted values requires terminal confirmation. Prefer kagi run
for day-to-day scripts.
sync is useful when .env.example gains a new key:
kagi sync --service apiExisting values are never overwritten.
A project is always team-ready. If you work alone, you are the only member.
New device or teammate:
kagi join --name alice
git add .kagi/access.json
git commit -m "chore: request kagi access"An existing member approves:
kagi member list
kagi member approve <member_id>
git add .kagi/access.json
git commit -m "chore: approve kagi member"If multiple people request access at the same time, keep all pending entries in
.kagi/access.json when merging their PRs.
Remove access:
kagi member remove <member_id>
git add .kagi
git commit -m "chore: remove kagi member"member remove rotates the project key internally and re-encrypts current
secrets for active members. If rotation is interrupted, kagi writes a local
journal outside the repository and retries safely on the next command.
For CI, store the project key in your secret manager and mount it as a file:
KAGI_PROJECT_KEY_FILE=/run/secrets/kagi_project_key kagi run api bun testKAGI_PROJECT_KEY=<64-hex-chars> is also supported when a file mount is not
available, but a file secret is easier to keep out of logs.
For local Docker development, prefer running the process through kagi on the host:
kagi run api docker compose upIf the container itself must read env files, export them when needed and keep
.env* ignored by Git.
Commit these:
.kagi/kagi.json
.kagi/access.json
.kagi/secrets/**/*.enc
.env.example
Do not commit these:
real .env / .env.* files
local project keys
local age identities / private keys
KAGI_PROJECT_KEY values
logs or screenshots containing secrets
The repository contains encrypted secret stores, public member recipients, and encrypted access wrappers. It does not contain the raw project key or private identity keys.
Secrets are encrypted with XChaCha20-Poly1305 and authenticated with their scope name, so an encrypted file cannot be silently moved to another scope.
kagi get <key>, kagi get --show, and kagi export reveal decrypted data and
require confirmation. kagi run is safer for scripts, but it is not a sandbox:
the child process receives the selected secrets as environment variables.
If every active member loses their local key material and no CI secret exists, the encrypted secrets are unrecoverable by design.
kagi follows Clean Architecture with four layers:
| Layer | Responsibility |
|---|---|
| Domain | Entities (Service, Secret), repository traits, error types, parsers |
| Application | Use cases: InitService, SetSecretService, GetSecretService, RunCommandService, etc. |
| Infrastructure | Concrete implementations: FileStore, XChaChaEncryptor, KeyManager, SystemCommandRunner |
| CLI | Argument parsing (clap), command dispatch, terminal styling |
This makes it trivial to swap the file-based store for a remote backend or replace the crypto implementation without touching business logic.
# Run all tests
cargo test
# Run integration tests only
cargo test --test integration_tests
# Run the real OS keychain smoke test
cargo test test_os_keychain_project_key_survives_local_data_loss -- --ignored
# Try the Bun example
cd tests
kagi init --nested --envs
cd api
kagi set MESSAGE "from kagi"
bun dev
# Install locally
cargo install --path .The default test suite uses isolated local storage so it can run in CI. The ignored keychain smoke test requires a real unlocked OS keychain/session and verifies that kagi can still load the project key after local data files are removed.
MIT
