> ## Documentation Index
> Fetch the complete documentation index at: https://comis-feature-skill-archive-import.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Secrets

> How to manage API keys and passwords safely in Comis

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.

| Method                       | Security | Best For                       | Setup                        |
| ---------------------------- | -------- | ------------------------------ | ---------------------------- |
| Environment variables (.env) | Basic    | Getting started, development   | Easy                         |
| SecretRef in config          | Better   | Referencing env vars from YAML | Easy                         |
| Encrypted secrets store      | Best     | Production deployments         | Auto-generated on first boot |

### 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.

```bash title="~/.comis/.env" theme={null}
OPENAI_API_KEY=sk-proj-abc123...
ANTHROPIC_API_KEY=sk-ant-abc123...
DEEPGRAM_API_KEY=dg-abc123...
```

<Warning>
  Plain text `.env` files are fine for local development, but not recommended for
  production. Anyone with access to the file can read your secrets.
</Warning>

### 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:

```yaml title="~/.comis/config.yaml" theme={null}
providers:
  anthropic:
    apiKey: ${ANTHROPIC_API_KEY}   # Resolved from environment at runtime
  openai:
    apiKey: ${OPENAI_API_KEY}      # Resolved from environment at runtime
```

For more advanced cases, Comis supports three resolution providers:

| Provider | Syntax                                   | What it does                                                                                                                            |
| -------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `env`    | `env:provider/KEY_NAME` or `${KEY_NAME}` | Reads from the SecretManager (env vars or encrypted store)                                                                              |
| `file`   | `file:provider/path/to/file`             | Reads an absolute path on disk (max 1MB, no symlinks to restricted paths). Append `#/json/pointer` to extract a field from a JSON file. |
| `exec`   | `exec:provider/id`                       | Invokes a credential helper binary via JSON-RPC (10s default timeout)                                                                   |

```yaml title="~/.comis/config.yaml" theme={null}
# Read Anthropic key from a sealed file deployed alongside the daemon
providers:
  anthropic:
    apiKey: file:vault/etc/comis/secrets.json#/anthropic/api_key

# Pull AWS keys from a credential helper (e.g., aws-vault, pass)
channels:
  s3-uploader:
    accessKeyId: exec:aws-vault/AWS_ACCESS_KEY_ID
    secretAccessKey: exec:aws-vault/AWS_SECRET_ACCESS_KEY
```

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.

### Encrypted Secrets Store (Recommended for Production)

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`:

```yaml theme={null}
security:
  storage: env   # read-only, reads .env / process.env only (no encrypted store)
```

`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.

<Steps>
  <Step title="Store the secret">
    Add the key to the encrypted store. The CLI prompts interactively so the
    value never appears in your shell history.

    ```bash theme={null}
    node packages/cli/dist/cli.js secrets set OPENAI_API_KEY
    # Enter value: ******** (paste the key, press Enter)
    # Stored: OPENAI_API_KEY (encrypted)
    ```
  </Step>

  <Step title="Scope it to one agent">
    Edit your config so only the `customer-support` agent can read the key:

    ```yaml title="~/.comis/config.yaml" theme={null}
    agents:
      customer-support:
        secrets:
          allow:
            - "OPENAI_API_KEY"
      # Other agents have no `secrets.allow` set, so they cannot read this key
      # (assuming you have set an allow list for them too -- empty defaults to
      # unrestricted, with a one-time warning).
    ```

    Reload the config: `node packages/cli/dist/cli.js config reload`.
  </Step>

  <Step title="Verify the scope">
    Trigger a read from the wrong agent and confirm it is denied. Watch the
    audit log:

    ```bash theme={null}
    node packages/cli/dist/cli.js logs follow --filter "secret:accessed"
    # ... agentId=analytics secretName=OPENAI_API_KEY outcome=denied
    # ... agentId=customer-support secretName=OPENAI_API_KEY outcome=success
    ```

    Every access -- successful, denied, or not-found -- emits a `secret:accessed`
    audit event tagged with the agent ID.
  </Step>

  <Step title="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:

    ```bash theme={null}
    node packages/cli/dist/cli.js secrets set OPENAI_API_KEY
    # Enter value: ******** (paste the new key)
    # Updated: OPENAI_API_KEY (re-encrypted, cache invalidated)
    ```

    Existing agent sessions pick up the new value on the next read -- no
    daemon restart required.
  </Step>

  <Step title="Delete the secret">
    When you decommission the integration, remove the key from the store:

    ```bash theme={null}
    node packages/cli/dist/cli.js secrets delete OPENAI_API_KEY
    # Removed: OPENAI_API_KEY
    ```

    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.
  </Step>
</Steps>

## 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.

```yaml title="~/.comis/config.yaml" theme={null}
agents:
  customer-support:
    secrets:
      allow:
        - "openai_*"             # Only OpenAI-related secrets
        - "ANTHROPIC_API_KEY"    # And the Anthropic key
  analytics:
    secrets:
      allow:
        - "GOOGLE_*"             # Only Google-related secrets
```

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.

<Tip>
  An empty `allow` list means unrestricted access.
  Once you add any pattern to the list, only matching secrets are accessible to
  that agent.
</Tip>

### 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:

| Pattern       | Matches                            | Does Not Match                |
| ------------- | ---------------------------------- | ----------------------------- |
| `openai_*`    | `openai_key`, `openai_org`         | `OPENAI_KEY` (case-sensitive) |
| `ANTHROPIC_*` | `ANTHROPIC_API_KEY`                | `anthropic_key`               |
| `*_API_KEY`   | `OPENAI_API_KEY`, `GOOGLE_API_KEY` | `openai_key`                  |

## 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.

<Tip>
  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.
</Tip>

## Configuration Reference

All secrets-related settings in your config file:

| Setting                       | Default             | Description                                                                                                                                                                                                |
| ----------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `security.storage`            | `"encrypted"`       | Credential storage backend for all stores: `encrypted` (AES-256-GCM SQLite), `file` (plaintext at 0600), or `env` (read-only, `.env`/process.env). The secrets database is always `<data dir>/secrets.db`. |
| `agents.<name>.secrets.allow` | `[]` (unrestricted) | Glob patterns for allowed secret names                                                                                                                                                                     |

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.

<Info>
  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.
</Info>

## 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:**

|                       | `secretRefs` (env injection)                             | Credential broker (network injection)                                             |
| --------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------- |
| **What it does**      | Injects the secret as an env var inside the exec sandbox | Injects the secret at the network boundary (TLS termination)                      |
| **Secret visibility** | Env var is visible inside the sandbox (`echo $MY_KEY`)   | Placeholder only inside sandbox; real key never enters                            |
| **Use when**          | Simple scripts needing a key as an environment variable  | API-key CLIs (Claude Code, curl with auth, Finnhub SDK)                           |
| **Platform**          | All platforms                                            | Kernel-enforced broker-only egress requires Linux; TLS injection works everywhere |

For step-by-step broker setup, see [Credential Broker →](/security/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`](/reference/config-yaml#security):

| Mode                  | Path                                       | Hot-reload                     | Threat posture                  |
| --------------------- | ------------------------------------------ | ------------------------------ | ------------------------------- |
| `encrypted` (default) | `oauth_profiles` table inside `secrets.db` | No -- SQLite WAL masks events  | AES-256-GCM ciphertext per row  |
| `file`                | `~/.comis/auth-profiles.json`              | Yes -- chokidar 100ms debounce | Filesystem perms only (`0o600`) |

### 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, `fsync`s, 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](/reference/node-permissions)). 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.
* **Requirement** -- `SECRETS_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.

<Snippet file="oauth-token-warning.mdx" />

### 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-EXFIL** -- *Disk-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-RACE** -- *Concurrent 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-DRIFT** -- *Env-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](/security/oauth).

## 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`:

```yaml title="~/.comis/config.yaml" theme={null}
security:
  writeSecretGuard: warn   # warn (default) | block | off
```

| Value            | Behavior                                                                                                                                       |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `warn` (default) | Write proceeds with scrubbed content and a redirect hint logged. Safe for `.env.example` files and hex SHAs.                                   |
| `block`          | Write is rejected when secret-shaped values are detected. Use in high-security environments where no plaintext secrets should ever touch disk. |
| `off`            | No write-time secret scan. Use only if you have an external DLP tool or the false-positive rate on your workload is unacceptable.              |

<Warning>
  `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.
</Warning>

**Source:** `packages/core/src/config/schema-security.ts:82` -- `z.enum(["warn", "block", "off"]).default("warn").optional()`

## Switching Storage Modes

The single `security.storage` key selects the credential storage backend for all stores:

```yaml title="~/.comis/config.yaml" theme={null}
security:
  storage: encrypted    # AES-256-GCM SQLite -- production default
  # storage: file       # plaintext 0o600 files -- simple or dev setups
  # storage: env        # read-only from process.env -- zero-file deployments
```

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:

```bash theme={null}
node packages/cli/dist/cli.js secrets list
```

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:

```json theme={null}
{ "name": "ANTHROPIC_API_KEY", "stored": true, "restarting": false }
```

### What applies live vs. what requires a restart

| Credential type                   | CLI command                   | Live?  | Note                                                       |
| --------------------------------- | ----------------------------- | ------ | ---------------------------------------------------------- |
| Named secrets                     | `comis secrets set <NAME>`    | Yes    | Applies on the next broker/exec request                    |
| Named secrets (delete)            | `comis secrets delete <NAME>` | Yes    | Removed from Map immediately                               |
| MCP OAuth tokens                  | `comis mcp login`             | Yes    | Token written to credential store and applies live         |
| OAuth profiles (`file` mode)      | `comis auth login`            | Yes    | chokidar hot-reload picks up the new profile               |
| OAuth profiles (`encrypted` mode) | `comis auth login`            | **No** | SQLite WAL masks file-change events; daemon restart needed |

<Info>
  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`.
</Info>

**Source:** `packages/daemon/src/api/secrets-handlers.ts:399-414, 579-596`; `packages/core/src/security/secret-manager.ts:81-129`

## Related

<CardGroup cols={2}>
  <Card title="Defense in Depth" icon="shield-halved" href="/security/defense-in-depth">
    How secrets fit into the security layers
  </Card>

  <Card title="Hardening" icon="lock" href="/security/hardening">
    Complete security hardening checklist
  </Card>

  <Card title="Secret Manager Reference" icon="file-code" href="/reference/secret-manager">
    Technical reference for the secret manager
  </Card>

  <Card title="Configuration" icon="gear" href="/installation/configuration">
    Config.yaml setup guide
  </Card>

  <Card title="OAuth" icon="shield-check" href="/security/oauth">
    Subscription-based authentication for Codex
  </Card>
</CardGroup>
