Skip to main content
Every AI integration needs API keys — for your LLM provider, for speech-to-text, for web search, and more. Comis gives you three ways to manage these secrets, from simple environment variables for getting started to a fully encrypted secrets store for production. This page walks you through each option, helps you choose the right one, and shows you how to set up the encrypted store step by step.

Three Ways to Store Secrets

Comis supports three methods for managing secrets, each with different security trade-offs. You can use any combination of these methods, and you can upgrade from one to another at any time without downtime.

Environment Variables (.env)

The simplest approach and the one most people start with. Create a .env file in your Comis data directory and set variables like OPENAI_API_KEY=sk-.... Comis loads .env automatically on startup. This is the fastest way to get running, but secrets are stored in plain text on disk — anyone with file access can read them.
~/.comis/.env
Plain text .env files are fine for local development, but not recommended for production. Anyone with access to the file can read your secrets.

SecretRef in Config

Instead of putting secrets directly in your YAML config, you can reference secrets from three different sources using the SecretRef syntax. This keeps your config file free of actual secrets, making it safer to share, back up, or commit to version control. The simplest form references an environment variable:
~/.comis/config.yaml
For more advanced cases, Comis supports three resolution providers:
~/.comis/config.yaml
The ${VARIABLE_NAME} form tells Comis to look up the value from the SecretManager at startup. The actual secret never appears in the YAML file itself. The file: and exec: forms keep secrets out of .env files entirely — they live wherever your secret store keeps them. Comis includes a built-in encrypted secrets store that uses AES-256-GCM encryption with HKDF-SHA256 key derivation. Secrets are encrypted at rest in a dedicated database file. Even if someone gains access to your server files, they cannot read the secrets without the master key. This is the recommended approach for any deployment where security matters — which includes most production environments. The encrypted store provides:
  • Encryption at rest — Secrets are never stored in plain text on disk. Each encryption uses a fresh 32-byte salt and 12-byte IV; the algorithm is versioned via an HKDF info string (comis-secrets-v1) so it can be rotated without breaking older ciphertexts.
  • Key derivation — The master key is never used directly; a derived key is created for each operation using HKDF-SHA256.
  • Integrity verification — AES-256-GCM provides authenticated encryption via a 16-byte tag, so tampered data is detected and rejected.
  • In-memory cache — Decrypted values live in process memory (defensive copies, not shared references) so repeated reads do not require repeated decryption. The cache is invalidated on rotation and cleared on shutdown.

Encrypted Store (Auto-Generated)

The encrypted secrets store is enabled by default. On first boot, Comis auto-generates a 32-byte master key and writes it to ~/.comis/.env as SECRETS_MASTER_KEY=<hex> (file mode 0600, owner-read-only). Backup obligation: The key in ~/.comis/.env is the only copy. Losing it makes secrets.db permanently unreadable (AES-256-GCM, no key escrow). Back up this file to a secure location immediately after first boot.

Opting Out

To run without the encrypted store, set the storage mode in config.yaml:
security.storage: file keeps a plaintext-at-0600 store instead. The daemon emits a startup WARN when a non-encrypted storage mode is active.

End-to-End Walkthrough: Store, Scope, Rotate, Delete

Here is a complete example of managing an OpenAI API key for a single agent named customer-support, from initial storage to rotation to deletion.
1

Store the secret

Add the key to the encrypted store. The CLI prompts interactively so the value never appears in your shell history.
2

Scope it to one agent

Edit your config so only the customer-support agent can read the key:
~/.comis/config.yaml
Reload the config: node packages/cli/dist/cli.js config reload.
3

Verify the scope

Trigger a read from the wrong agent and confirm it is denied. Watch the audit log:
Every access — successful, denied, or not-found — emits a secret:accessed audit event tagged with the agent ID.
4

Rotate the key

When the upstream provider issues a new key, write it over the old one. Comis re-encrypts on write and invalidates its in-memory cache:
Existing agent sessions pick up the new value on the next read — no daemon restart required.
5

Delete the secret

When you decommission the integration, remove the key from the store:
The encrypted ciphertext is wiped from disk and the cache entry is cleared. Any agent that subsequently calls secret.require("OPENAI_API_KEY") will throw with a diagnostic error message containing the key name.

Per-Agent Secret Access

By default, every agent can access all secrets in the store. While this is convenient for getting started, it means a compromised agent could potentially access secrets it does not need. For better security, restrict each agent to only the secrets it needs using glob patterns. This follows the principle of least privilege — each agent only gets the secrets it requires for its specific role.
~/.comis/config.yaml
If an agent tries to access a secret that does not match its allow patterns, the request is denied and an audit event is recorded. This means a compromised or misbehaving agent cannot access secrets it does not need.
An empty allow list means unrestricted access. Once you add any pattern to the list, only matching secrets are accessible to that agent.

Pattern Matching

The allow list supports standard glob patterns. Patterns are case-sensitive, so make sure they match the exact casing of your secret names:

Auditing Your Secrets

Regular auditing helps you catch configuration drift and ensure your secrets are properly protected. Run comis doctor to check your secrets configuration. The audit examines:
  • Plaintext secrets in config files — Secrets that should be migrated to the encrypted store or referenced via SecretRef. A field that already uses a ${VAR} env-substitution reference or a structured SecretRef is recognized as properly configured and is not flagged.
  • Known provider API keys in .env files — Provider-specific keys (like OpenAI, Anthropic, Deepgram) that should be in the encrypted store for production
  • File permission issues — Config and secrets files that have overly permissive access (readable by other users on the system)
  • Missing master key — The encrypted store is enabled but the SECRETS_MASTER_KEY environment variable is not set
  • Unencrypted backup copies — Backup files that may contain unencrypted secret data
The audit produces actionable findings with specific remediation steps. For example, if it finds a plaintext API key in your config, it tells you exactly how to migrate it to the encrypted store.
Run comis doctor after any configuration change to verify that your secrets are still properly managed. It is also a good practice to include it in your deployment checklist.

Configuration Reference

All secrets-related settings in your config file: The SECRETS_MASTER_KEY environment variable is auto-generated on first boot and written to ~/.comis/.env (mode 0600). You can provide it explicitly to override the auto-generated value. This key is never stored in the config file itself.
To disable the encrypted store entirely, set security.storage: env (or file) in config.yaml. The daemon then boots in that mode and emits a startup WARN. Use env only if you need to manage secrets exclusively via .env files.

Provider API Keys

When configuring LLM providers via the providers_manage tool, API keys follow the same SecretManager workflow:
  1. Store the key: gateway({ action: "env_set", env_key: "NVIDIA_API_KEY", env_value: "nvapi-..." })
  2. Reference in provider config: providers_manage({ action: "create", provider_id: "nvidia", config: { apiKeyName: "NVIDIA_API_KEY", ... } })
The provider configuration stores only the key name (apiKeyName), never the key value itself. The actual key is resolved at runtime via SecretManager. For local providers like Ollama that don’t require API keys, omit apiKeyName entirely.

Broker Injection (Network Boundary)

For exec tools that drive API-key CLIs, the credential broker provides a second injection path that keeps the key out of the sandbox entirely. Two injection paths for exec tools: For step-by-step broker setup, see Credential Broker →.

OAuth Credential Storage

OAuth profiles for subscription-based providers (currently OpenAI Codex) are stored separately from the read-only SecretManager interface documented above. The OAuth credential store is mutable because tokens refresh on a 30-minute cadence and the rotated refresh token must be persisted before the lock holding the in-flight refresh releases. Two storage backends are supported, selected by security.storage:

File mode (security.storage: file)

  • Path~/.comis/auth-profiles.json (under COMIS_DATA_DIR if overridden).
  • Permissions — mode 0o600 is enforced on every write. Files created with weaker perms are rejected at boot.
  • Atomic write — the daemon writes to a temp file, fsyncs, then renames into place. Crashes mid-write cannot leave a half-written file visible. When the daemon runs under node --permission, the fsync call is skipped (the fd-based fsync API is disabled by the permission model — see Node Permissions reference). Writes remain atomic (tmp -> rename) but are best-effort durability under --permission.
  • Cross-process locking — multiple daemons or the daemon + a comis auth login CLI all refreshing the same profile coordinate via proper-lockfile. The refresh-then-persist critical section runs under the lock; the lock is released only after the rotated refresh token is on disk.
  • Hot-reload — a chokidar watcher with 100ms debounce surfaces external writes (e.g., comis auth login from a different shell) to the running daemon without restart.

Encrypted mode (security.storage: encrypted)

  • Storage — the oauth_profiles table inside the same SQLite database as secrets.db.
  • Algorithm — each row stores the full profile JSON as one AES-256-GCM ciphertext blob with per-row 12-byte IV and 16-byte auth tag. The denormalized expires_at epoch-millisecond column is the only plaintext field, kept for fast comis doctor scans.
  • Key derivation — shares the SecretsCrypto engine with the encrypted secrets store; HKDF-SHA256 derives a fresh per-row key from the master.
  • RequirementSECRETS_MASTER_KEY must be set; the daemon fails fast on boot otherwise.
  • Hot-reload limitation — SQLite WAL writes do not surface as change events on the database file; CLI-written profiles require a daemon restart to take effect.

Email semi-redaction in logs

Profile identities are emails (e.g., user@example.com). Comis logs never include the access or refresh token at any level (Pino auto-redaction is the safety net; the canonical fields list in CLAUDE.md excludes them). Emails are semi-redacted before logging using a first-char + last-char per atom strategy: user@example.com becomes u***@e***.com. This preserves enough identity to debug a multi-account routing issue while denying log readers the full email. The redaction is performed at the structured-log call site, not by Pino’s redaction list, so it survives even if the log call accidentally interpolates the email into the message string.

Threat model

  • T-OAUTH-DISK-EXFILDisk-read attacker. An attacker with read access to ~/.comis/ could exfiltrate stored access and refresh tokens. Mitigation: Plaintext mode enforces 0o600 perms (user-only read). Encrypted mode stores tokens as AES-256-GCM ciphertext with per-row IV/auth-tag, defeating raw-byte scans (verified by canary tests).
  • T-OAUTH-REFRESH-RACEConcurrent refresh race. Two daemon instances or daemon+CLI refreshing the same token simultaneously could result in refresh_token_reused (which auto-locks the account at OpenAI). Mitigation: Per-profile cross-process file lock via proper-lockfile with 5-retry exponential backoff; refresh persistence happens before lock release, so concurrent callers see the rotated token via the lock-protected critical section.
  • T-OAUTH-ENV-DRIFTEnv-var drift. An operator updates OAUTH_OPENAI_CODEX in .env after a profile is already in the store; the daemon silently uses the stored profile, leading to confusion. Mitigation: WARN-once-per-(provider, process) when the env-var refresh-token differs from the stored profile, with hint: "env-override-ignored" and errorKind: "config_drift". Operators should run comis auth logout --profile <id> then restart the daemon to re-bootstrap from env, OR delete the env var.
For the full PKCE flow, refresh semantics, and wizard-vs-CLI parity matrix, see OAuth concepts.

Output Guard for Secret Egress

writeSecretGuard controls what happens when the daemon detects a secret-shaped value in a file write path (for example, a tool writing a .env.example or a test fixture). This prevents a careless agent from writing a live API key to disk in cleartext. Configure via security.writeSecretGuard in config.yaml:
~/.comis/config.yaml
block mode carries a false-positive risk on legitimate files that contain hex strings or long random tokens (e.g., test fixtures, SSH public keys). Test your agent workloads before enabling block in production.
Source: packages/core/src/config/schema-security.ts:82z.enum(["warn", "block", "off"]).default("warn").optional()

Switching Storage Modes

The single security.storage key selects the credential storage backend for all stores:
~/.comis/config.yaml
If choosing encrypted, ensure SECRETS_MASTER_KEY is set in .env. If it is not set, run comis secrets init to generate one. Because AppConfigSchema uses z.strictObject, any unrecognized key under security: triggers a VALIDATION_ERROR and the daemon refuses to start — there is no silent fallback. After changing the mode, verify boot:
If the daemon starts and returns a list (empty or with keys), the switch is complete. Check for stranded credentials: At boot, the daemon logs a WARN if the inactive backend still holds credentials the active mode cannot reach:
  • "Inactive encrypted secrets store has real credentials" — emitted when switching from encrypted to file or env
  • "Inactive file secret store has real secrets" — emitted when switching from file to encrypted
Each WARN includes a hint field with manual migration steps. Run comis secrets list after the storage-mode switch, then manually re-import any stranded credentials.

Live Credential Apply

Storing, rotating, or deleting a credential mid-conversation takes effect immediately — no daemon restart required. When you run comis secrets set <NAME> or comis secrets delete <NAME>, the daemon writes to the credential store and then upserts (or removes) the value in the shared in-memory MutableSecretManager Map in the same RPC call. Both the broker and exec agents read from that Map on every request, so the next tool invocation observes the new value with no restart. The RPC response carries "restarting": false to confirm:

What applies live vs. what requires a restart

The encrypted-mode OAuth limitation is a known constraint of using SQLite WAL for storage. For workloads that rotate OAuth profiles frequently, consider security.storage: file or triggering a graceful daemon restart after comis auth login.
Source: packages/daemon/src/api/secrets-handlers.ts:399-414, 579-596; packages/core/src/security/secret-manager.ts:81-129

Defense in Depth

How secrets fit into the security layers

Hardening

Complete security hardening checklist

Secret Manager Reference

Technical reference for the secret manager

Configuration

Config.yaml setup guide

OAuth

Subscription-based authentication for Codex