Documentation menu

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=info
aikeys render .env.template .env
# rendered .env
  • A placeholder without @version reads version default.
  • 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 .env does 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.

An empty secret or vault password fails the command with exit code 1 (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.

FolderFor
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

ItemProtection
Secret values, file storeAES-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 storeHeld by the macOS Keychain or the Linux Secret Service, not by aikeys
Names, versions, store type, timestampsPlain text in vault.json
config.jsonPlain text by design
Rendered outputPlain 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 -i with the value hex-encoded on stdin, which limits keychain secrets to roughly 1,900 bytes; on Linux secret-tool reads 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_PASSWORD out 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.