Clients & Libraries
aikeys
Keep API keys in one encrypted local store and let coding agents reach them by name. This guide goes from install to rendering .env files in CI.
Install
aikeys needs Node.js 18 or newer and has no runtime dependencies. The repository is public under the MIT license; it is not published to the npm registry, so install it from GitHub:
npm install -g github:afterdarksys/aikeys # adds aikeys and apikeys to your PATH
Or from a checkout:
git clone https://github.com/afterdarksys/aikeys.git cd aikeys npm install npm link # adds aikeys and apikeys to your PATH aikeys help
Then create the config and vault:
aikeys setup # file vault (default) aikeys setup --store keychain # or the macOS Keychain / Linux Secret Service aikeys where # show the paths in use
The keychain store needs security on macOS or secret-tool on Linux; where neither exists, asking for it is an error. On Windows use the file vault. See Keychain or file vault for the differences.
Store and read a secret
Names are section/key, with an optional version that defaults to default. Keep one version per environment if that helps, such as prod and test.
aikeys set openai/api_key prod # prompts for the secret, then the vault password aikeys get openai/api_key prod # prints the value aikeys set github/token --store keychain # this one goes to the OS keychain aikeys get github/token
Prompts are hidden and written to stderr. get prints only the value, with no trailing newline unless stdout is a terminal, so it is safe to capture:
OPENAI_API_KEY="$(aikeys get openai/api_key prod)" npm test
Running set again with the same name and version replaces the stored value. There is no delete or list command; vault.json shows the names you have stored.
Render .env templates
Commit a template with placeholders, and generate the real file when you need it. Keep the output in .gitignore.
.env.template
OPENAI_API_KEY={{aikeys:openai/api_key@prod}}
ANTHROPIC_API_KEY={{aikeys:anthropic/api_key}}
LOG_LEVEL=infoaikeys render .env.template .env # rendered .env
- A placeholder without
@versionreads versiondefault. - Everything outside the placeholders is copied as is, so the same approach works for YAML, JSON or TOML.
- If any placeholder names a missing secret, render stops and writes nothing.
- Values are inserted byte for byte, and a placeholder that happens to appear inside a secret value is not expanded.
- The output is written atomically with mode 0600. An existing file is replaced, so a world-readable
.envdoes not stay world-readable, and a failed render leaves the previous file untouched.
Find and import stray secrets
aikeys scan . # src/config.env:3 openai sk-a...wxyz # src/config.env:4 openai_api_key abcd...pqrs aikeys scan . --import # imported scan/src-config.env-openai-1@imported
Findings are masked. The scanner recognizes OpenAI-style sk- keys, Anthropic sk-ant- keys, AWS access key IDs and assignments like SOME_API_KEY=.... It skips .git, node_modules, binaries and files over 2 MiB. Use --import only when you want the findings in your vault; afterwards, replace the literal values in the source with placeholders and rotate anything that was committed.
Non-interactive use in CI and agents
Two variables replace the prompts: AIKEYS_PASSWORD for the vault password and AIKEYS_SECRET for the value given to set. AIKEYS_HOME points aikeys at a different directory.
export AIKEYS_HOME="$RUNNER_TEMP/aikeys" export AIKEYS_PASSWORD="$VAULT_PASSWORD" # from your CI secret store aikeys setup AIKEYS_SECRET="$OPENAI_KEY" aikeys set openai/api_key prod printf '%s' "$STRIPE_KEY" | aikeys set stripe/secret_key test # stdin works too aikeys render .env.template .env
When stdin is not a terminal, aikeys reads the secret or password from it, so piping works. stdin can be read only once, which is why the password comes from AIKEYS_PASSWORD in the piped example. The vault password is asked for once per process.
empty secret refused or empty vault password refused), including when stdin is empty or closed, as with < /dev/null. Nothing is stored or printed.Agents
Give an agent the commands, not the vault. The repository's AGENTS.md and the skills below tell an agent to use:
aikeys get <section>/<key> [version] aikeys render <template> <output> aikeys scan <dir-tree>
For the file store, whoever has AIKEYS_PASSWORD can read every entry encrypted under it. If an agent should reach only some keys, put those in a separate AIKEYS_HOME with its own password, or render the files it needs yourself before starting it.
Agent skills
The repository ships one skill, aikeys-secret-access, in two layouts with the same content: one for Codex and one for Claude Code.
| Folder | For |
|---|---|
skills/codex/aikeys-secret-access/ | Codex |
skills/claude/aikeys-secret-access/ | Claude Code |
# Codex: symlink so updates to the checkout apply mkdir -p ~/.codex/skills ln -s "$(pwd)/skills/codex/aikeys-secret-access" ~/.codex/skills/aikeys-secret-access # Claude Code: copy the folder into your skills directory cp -R skills/claude/aikeys-secret-access ~/.claude/skills/
The skill tells the agent to:
- Never print raw secret values in chat, logs, summaries, commits or comments unless the user asks for the value itself.
- Prefer aikeys render over writing secrets into config files by hand, and treat rendered files as secret-bearing.
- Never edit ~/.apikeys/vault.json directly, and never commit generated files such as .env.
- Pass secrets to commands through command-local substitution, after checking that shell tracing and verbose logging are off.
- Run scan --import only when the user wants discovered secrets added to the vault.
Security notes
What is encrypted
| Item | Protection |
|---|---|
| Secret values, file store | AES-256-GCM. Key derived per entry from the vault password with scrypt (Node.js defaults: N=16384, r=8, p=1) into 32 bytes, with a random 16-byte salt and a random 12-byte IV per write. 16-byte GCM tag. |
| Secret values, keychain store | Held by the macOS Keychain or the Linux Secret Service, not by aikeys |
| Names, versions, store type, timestamps | Plain text in vault.json |
| config.json | Plain text by design |
| Rendered output | Plain text; always written atomically at mode 0600 |
Tamper handling
Each entry records alg and kdf, and aikeys refuses to decrypt anything other than aes-256-gcm with scrypt, so an edited vault cannot downgrade the cipher or KDF. Salt, IV and tag lengths are checked before decryption. A wrong password, altered ciphertext, a forged tag or a swapped IV all fail with exit code 1 and nothing on stdout. The repository's test suite covers each of these cases.
Things to know
- There is no password check at setup and no single master key. Each entry is encrypted with the password given when it was written, so a typo creates an entry only that typo can open.
- The keychain store never puts the secret in a command argument. On macOS it drives
security -iwith the value hex-encoded on stdin, which limits keychain secrets to roughly 1,900 bytes; on Linuxsecret-toolreads it from stdin. Every write is read back and compared, and a mismatch (for example a value with tabs or other non-printable characters) removes the item and fails. - Asking for the keychain store where no keychain tool exists is an error; aikeys never falls back to the file vault.
- Keep
AIKEYS_PASSWORDout of shell history and shared environments; anything that can read it can decrypt the vault. - The home directory is created with mode 0700. config.json, vault.json and rendered output are always rewritten atomically at 0600, and a symlink at the target is replaced rather than followed.
- The agentPolicy block in config.json is advisory. The CLI does not enforce it and prints a one-time notice on stderr saying so.