Documentation menu

Configuration

aikeys configuration

aikeys keeps two JSON files in one home directory: config.json, which you and your agents may read, and vault.json, which only the CLI should write.

Setup

aikeys setup                     # default store: file
aikeys setup --store keychain    # default store: native keychain
aikeys setup --force             # overwrite an existing config.json

setup creates the home directory with mode 0700, writes config.json with mode 0600, and creates an empty vault.json (mode 0600) only if one does not already exist. --store must be file or keychain. Running it again without --force fails. --force rewrites config.json with the defaults and leaves the vault and its entries alone. Setup does not ask for a password; the file store takes a password each time a secret is written or read.

Files and paths

PathModeContents
~/.apikeys/0700Home directory. Override with AIKEYS_HOME.
~/.apikeys/config.json0600Default store, vault location, keychain service name, template and agent policy notes
~/.apikeys/vault.json0600One entry per secret version: ciphertext for the file store, a pointer for the keychain store

aikeys where prints the resolved home, config and vault paths. The home directory gets mode 0700 when aikeys creates it. config.json, vault.json and rendered output are always written atomically at 0600: aikeys writes a new 0600 temporary file in the same directory and renames it over the target, so an existing file never keeps a looser mode, and a symlink at the target is replaced rather than followed.

Keychain or file vault

filekeychain
Where the value livesvault.json, AES-256-GCM ciphertextmacOS Keychain or Linux Secret Service
PasswordVault password on every set and every readNone from aikeys; the OS keychain controls access
Backend commandNone (Node.js crypto)security on macOS, secret-tool on Linux
What vault.json holdsAlgorithm, KDF, salt, IV, tag and ciphertextStore name, keychain account and timestamp only
WindowsSupportedNot supported; set fails and tells you to use --store file

The store is chosen per write: set --store wins, then defaultStore from config.json, then file. A read uses whatever store the entry was written to, so one vault can mix both.

Keychain items are saved under the service name from keychainService (default aikeys) with the account section/key@version. Reads look the entry up in vault.json first, so a keychain item without a matching vault entry is not found.

There is no fallback between stores. If you ask for the keychain store on a system without security or secret-tool, the write fails and nothing is stored. The confirmation line names the store the secret was written to.

Keychain limits

The secret reaches security and secret-tool on stdin, never as a command-line argument. On macOS aikeys drives security -i and sends the value hex-encoded, which limits a keychain secret to roughly 1,900 bytes (less with long names); longer secrets are refused. Every keychain write is read back and compared, and on a mismatch the item is deleted and set fails, which in practice rejects values containing tabs or other non-printable characters. Keychain service and account names may only use letters, numbers, dot, underscore, dash, slash and @, so versions stored in the keychain are limited to those characters. Use the file store for anything outside these limits.

config.json reference

~/.apikeys/config.json (as written by setup)

{
  "version": 1,
  "defaultStore": "file",
  "vaultFile": "vault.json",
  "keychainService": "aikeys",
  "template": {
    "tokenPattern": "{{aikeys:<section>/<key>@<version>}}",
    "examples": [
      "OPENAI_API_KEY={{aikeys:openai/api_key@prod}}",
      "ANTHROPIC_API_KEY={{aikeys:anthropic/api_key}}"
    ]
  },
  "agentPolicy": {
    "preferRender": true,
    "doNotPrintSecretsInLogs": true,
    "allowedCommands": [
      "aikeys get <section>/<key> [version]",
      "aikeys render <template> <output>"
    ]
  }
}
KeyDefaultUsed by the CLINotes
version1NoFormat version
defaultStorefileYesfile or keychain. Used by set without --store and by scan --import.
vaultFilevault.jsonYesRelative paths resolve inside the home directory; absolute paths are used as given
keychainServiceaikeysYesService name for keychain items
template.tokenPatternsee aboveNoDocuments the placeholder form for agents and scripts
template.examplessee aboveNoExample template lines
agentPolicy.*see belowNotice onlyAdvisory policy for agents; see Policy options

The file is plain JSON on purpose, so agents and scripts can read the conventions without running the CLI. Until it exists, set, get, render and scan --import fail with run `aikeys setup` first. where, help and a plain scan work without it.

vault.json reference

~/.apikeys/vault.json

{
  "version": 1,
  "entries": {
    "openai/api_key": {
      "versions": {
        "prod": {
          "store": "file",
          "encrypted": {
            "alg": "aes-256-gcm",
            "kdf": "scrypt",
            "salt": "<base64, 16 bytes>",
            "iv": "<base64, 12 bytes>",
            "tag": "<base64, 16 bytes>",
            "ciphertext": "<base64>"
          },
          "updatedAt": "2026-09-28T12:24:51.206Z"
        }
      }
    },
    "github/token": {
      "versions": {
        "default": {
          "store": "keychain",
          "account": "github/token@default",
          "updatedAt": "2026-09-28T12:30:02.114Z"
        }
      }
    }
  }
}
FieldNotes
entries.<section/key>.versions.<version>One record per stored version. Writing the same name and version replaces it.
storefile or keychain. Any other value is refused on read.
encryptedFile store only. alg must be aes-256-gcm and kdf must be scrypt; salt, IV and tag must be 16, 12 and 16 bytes.
accountKeychain store only. The keychain account name.
updatedAtISO 8601 time of the last write

Secret names, versions, store types and timestamps are stored in clear text. Only the values are encrypted. Do not edit this file by hand.

Commands and flags

aikeys setup [--store file|keychain] [--force]
aikeys set <section>/<key> [version] [--store file|keychain]
aikeys get <section>/<key> [version]
aikeys render <template> <output>
aikeys scan <dir-tree> [--import]
aikeys where
aikeys help
CommandFlagsBehavior
setup--store file|keychain, --forceWrites config.json and an empty vault.json. See Setup.
set--store file|keychainStores a secret. Version defaults to default. An empty secret exits 1. put is an alias.
getnoneWrites the value to stdout. A trailing newline is added only when stdout is a terminal, so $(aikeys get ...) returns the exact value.
rendernoneReplaces every placeholder in the template and writes the output file atomically at 0600. Prints rendered <output>.
scan--importPrints file:line name masked-value for each finding. With --import, also stores each finding.
wherenonePrints home, config and vault paths. Works before setup.
helpnoneAlso -h, --help, or no command

Flags may appear anywhere after the command. --store takes a value, which must be file or keychain for both setup and set; any other value is an error.

Environment variables

VariableEffect
AIKEYS_HOMEHome directory instead of ~/.apikeys
AIKEYS_PASSWORDVault password for the file store. Used for every password prompt, so no prompt appears.
AIKEYS_SECRETSecret value for set. Used instead of stdin or the prompt.

These are the only variables aikeys reads.

How secrets and passwords are read

Each prompt resolves its input in this order:

Prompt123
Vault passwordAIKEYS_PASSWORDstdin, whenever it is not a terminalHidden prompt on stderr
Secret (set)AIKEYS_SECRETstdin, whenever it is not a terminalHidden prompt on stderr

stdin is read to the end and trailing whitespace, including the final newline, is removed. It can only be read once, so when you pipe the secret into set for the file store, pass the password in AIKEYS_PASSWORD. The secret is read before the password.

The vault password is asked for once per process, so rendering a template with several file-store placeholders prompts once. Every entry has its own salt, and nothing checks that all entries share one password.

An empty secret or an empty vault password, from any source, is refused: the command exits 1 with empty secret refused or empty vault password refused and stores or prints nothing. This includes empty or closed stdin (for example < /dev/null in a CI job) and Ctrl-D at the prompt. Non-terminal stdin is always used when the variable is not set; it never falls through to a prompt.

Template syntax

{{aikeys:section/key@version}}
{{aikeys:section/key}}          # version "default"

Placeholders can appear anywhere in any text file, any number of times. Text outside placeholders is copied unchanged.

RuleDetail
No whitespaceA placeholder containing a space or line break is not recognized and is left as written
Missing secretrender stops with missing secret: section/key@version and writes no output
Output fileWritten atomically with mode 0600. An existing output is replaced, so its old mode does not survive, and a failed render leaves the previous file untouched.
Literal valuesEvery placeholder is resolved first, then substituted in one pass. Values are inserted byte for byte, and a placeholder that appears inside a secret value is not expanded.
No escapingThere is no way to write a literal placeholder into the output

Secret names and versions

PartRule
section/keyExactly one slash. Each side uses letters, numbers, dot, underscore or dash.
versionAny label without spaces. Defaults to default.
Examplesopenai/api_key prod, github/token, stripe/secret_key test

Scanner rules

Pattern nameMatches
openaisk- followed by 20 or more letters, digits, underscores or dashes
anthropicsk-ant- followed by 20 or more of the same. These also match openai, so they are reported twice.
aws_access_key_idAKIA followed by 16 uppercase letters or digits
variable name, lowercasedAn assignment such as FOO_API_KEY=value where the name contains API, TOKEN, SECRET or KEY and the value is 16 or more characters

The scanner skips .git, node_modules and .apikeys directories, symbolic links, files over 2 MiB, files containing a NUL byte, and files ending in .png, .jpg, .jpeg, .gif, .pdf, .zip, .gz, .tar, .ico, .woff or .woff2. Values are masked as the first four and last four characters, or all asterisks when eight characters or fewer.

With --import, each finding is stored as scan/<file>-<name>-<n>@imported in the default store, where the file path is lowercased with other characters replaced by dashes and n numbers the findings in order.

Policy options

The agentPolicy block in config.json is guidance for agents that read the file. The CLI does not enforce it: it cannot tell an agent from a person, and it does not check commands against allowedCommands. The first time it reads a config containing the block, it prints a one-time notice to stderr saying so.

KeyDefaultMeaning
preferRendertrueGenerate config files with render rather than pasting values
doNotPrintSecretsInLogstrueKeep values out of logs, chat and summaries
allowedCommandsget, renderThe commands an agent should use

What the CLI does enforce

ControlDetail
Name formatNames outside section/key are refused
Empty inputEmpty secrets and empty vault passwords exit 1
Store choiceOnly file or keychain; no silent fallback between them
Keychain writesSecret passed on stdin, never argv; read back and verified
Vault primitivesOnly aes-256-gcm with scrypt decrypts; anything else fails closed
File modes0700 home when created; config, vault and rendered output always written atomically at 0600
Masked scan outputFindings are never printed in full

To restrict an agent for real, give it a shell without write access to the aikeys home, or keep file-store secrets behind a password the agent does not have.

Exit codes

CodeMeaning
0Success. scan exits 0 whether or not it finds anything.
1Any error. The message goes to stderr as aikeys: <message>, and stdout stays empty for a failed get.