Skip to main content
AI apps need a lot of API keys. Sharing .env files is risky, and coding agents shouldn’t see your secrets. The @outputai/credentials package keeps API keys, database passwords, and third-party tokens encrypted at rest in your repository. Secrets are stored as encrypted YAML files that you commit alongside your code — no more .env files drifting out of sync between developers, no more external vault subscriptions. At runtime, a single encryption key decrypts everything. Each workflow can have its own scoped credentials that override shared defaults, so you get both team-wide consistency and per-workflow flexibility. You manage credentials through the CLI and read them in your steps with a simple dot-notation API.

What’s in the Package

import {
  // Credential access
  credentials,

  // Encryption utilities
  encrypt, decrypt, generateKey,

  // Provider system
  setProvider, getProvider,
  encryptedYamlProvider,

  // Error types
  MissingCredentialError, MissingKeyError,

  // Path resolution
  resolveCredentialsPath, resolveKeyPath, resolveKeyEnvVar,
  resolveWorkflowCredentialsPath, resolveWorkflowKeyPath, resolveWorkflowKeyEnvVar,
  getNestedValue
} from '@outputai/credentials';
ExportDescription
credentialsRead credentials via dot-notation paths (get, require)
encrypt / decrypt / generateKeyLow-level AES-256-GCM encryption utilities
setProvider / getProviderSwap the credential storage backend
encryptedYamlProviderDefault provider: encrypted YAML files on disk
MissingCredentialErrorThrown by credentials.require() when a path has no value
MissingKeyErrorThrown when no decryption key can be found
resolveCredentialsPath / resolveKeyPath / …Path resolution helpers for credentials and key files

Quick Start

1. Initialize credentials

output credentials init
This generates a random 256-bit encryption key at config/credentials.key and an encrypted YAML file at config/credentials.yml.enc with a starter template.

2. Add your secrets

output credentials edit
This decrypts the file, opens it in $EDITOR, and re-encrypts on save. Fill in your secrets:
config/credentials.yml.enc (decrypted content)
anthropic:
  api_key: sk-ant-xxxxx
openai:
  api_key: sk-xxxxx
stripe:
  secret_key: sk_live_xxxxx

3. Read credentials in a step

steps.ts
import { step, z } from '@outputai/core';
import { credentials } from '@outputai/credentials';

export const callApi = step({
  name: 'callApi',
  inputSchema: z.object({ prompt: z.string() }),
  outputSchema: z.string(),
  fn: async (input) => {
    const apiKey = credentials.require('anthropic.api_key');
    // use apiKey to call the Anthropic API...
  }
});

The credentials Object

The credentials object is the primary API for reading secrets at runtime.
MethodArgumentsReturnsThrows
credentials.get(path, defaultValue?)path: string, defaultValue?: unknownThe credential value, the default, or undefinedNever
credentials.require(path)path: stringThe credential valueMissingCredentialError if not found
The path argument uses dot-notation to navigate nested YAML. For example, 'anthropic.api_key' reads the api_key field under the anthropic key.
// Safe — returns the default when missing
const region = credentials.get('aws.region', 'us-east-1');

// Strict — throws MissingCredentialError if the credential doesn't exist
const apiKey = credentials.require('anthropic.api_key');
Credentials are loaded and decrypted once per scope (global or per-workflow), then cached for the process lifetime.

Credential Scopes

Credentials are organized into three levels. More specific scopes override broader ones. Global — shared across all workflows:
FilePath
Encrypted credentialsconfig/credentials.yml.enc
Encryption keyconfig/credentials.key
Environment-specific — per NODE_ENV (production or development):
FilePath
Encrypted credentialsconfig/credentials/{environment}.yml.enc
Encryption keyconfig/credentials/{environment}.key
Per-workflow — scoped to a single workflow:
FilePath
Encrypted credentialssrc/workflows/{name}/credentials.yml.enc
Encryption keysrc/workflows/{name}/credentials.key
my-project/
├── config/
│   ├── credentials.yml.enc           # Global credentials (encrypted)
│   ├── credentials.key               # Global key (DO NOT commit)
│   └── credentials/
│       ├── production.yml.enc        # Production credentials
│       ├── production.key
│       ├── development.yml.enc       # Development credentials
│       └── development.key
└── src/workflows/
    └── payment_processing/
        ├── workflow.ts
        ├── steps.ts
        ├── credentials.yml.enc       # Workflow-specific credentials
        └── credentials.key           # Workflow-specific key
Environment is auto-detected from NODE_ENV. Only "production" and "development" are recognized — other values are ignored. If an environment-specific file doesn’t exist, the system falls back to the default config/credentials.yml.enc.

Key Resolution

The system resolves decryption keys using a fallback chain: Global credentials:
  1. Environment variable OUTPUT_CREDENTIALS_KEY (or OUTPUT_CREDENTIALS_KEY_{ENVIRONMENT} for env-specific files)
  2. Key file at config/credentials.key (or config/credentials/{environment}.key)
  3. Throws MissingKeyError if neither is found
Workflow credentials:
  1. Environment variable OUTPUT_CREDENTIALS_KEY_{WORKFLOW_NAME} (uppercased)
  2. Key file at src/workflows/{name}/credentials.key
  3. Falls back to the global key resolution chain
ScopeEnvironment VariableExample
GlobalOUTPUT_CREDENTIALS_KEYOUTPUT_CREDENTIALS_KEY=a1b2c3...
EnvironmentOUTPUT_CREDENTIALS_KEY_{ENVIRONMENT}OUTPUT_CREDENTIALS_KEY_PRODUCTION=a1b2c3...
WorkflowOUTPUT_CREDENTIALS_KEY_{WORKFLOW_NAME}OUTPUT_CREDENTIALS_KEY_PAYMENT_PROCESSING=a1b2c3...
Use environment variables for keys in CI/CD and production deployments. Use key files for local development.

Credential Merging

When a step runs inside a workflow that has its own credentials file, the workflow credentials are deep-merged over the global credentials. Workflow values override global values at the same path. Global credentials (config/credentials.yml.enc):
anthropic:
  api_key: sk-ant-global
aws:
  region: us-east-1
  secret: aws-global-secret
Workflow credentials (src/workflows/my_workflow/credentials.yml.enc):
anthropic:
  api_key: sk-ant-workflow-specific
stripe:
  secret_key: sk_live_workflow
Merged result (what credentials.get() sees inside the workflow):
anthropic:
  api_key: sk-ant-workflow-specific   # Overridden by workflow
aws:
  region: us-east-1                   # From global
  secret: aws-global-secret           # From global
stripe:
  secret_key: sk_live_workflow        # Added by workflow

CLI Commands

The CLI provides four commands for managing credentials. See CLI for the full reference.
CommandDescription
output credentials initGenerate an encryption key and encrypted credentials file
output credentials editDecrypt, open in $EDITOR, re-encrypt on save
output credentials showPrint decrypted credentials (for debugging)
output credentials get <path>Print a single credential value by dot-notation path

Shared Flags

FlagShortDescription
--environment-eTarget environment (e.g., production, development)
--workflow-wTarget a specific workflow directory
--force-fOverwrite existing credentials (init only)
--environment and --workflow are mutually exclusive.

Examples

# Initialize global credentials
output credentials init

# Initialize production credentials
output credentials init --environment production

# Initialize workflow-specific credentials
output credentials init --workflow payment_processing

# Edit global credentials in $EDITOR
output credentials edit

# Edit production credentials
output credentials edit -e production

# Show decrypted credentials (debugging)
output credentials show

# Get a specific credential value
output credentials get anthropic.api_key

# Get a workflow-specific credential
output credentials get stripe.key --workflow payment_processing

Environment Variable Convention

Many libraries read their API keys from environment variables. Rather than duplicating secrets between your .env file and encrypted credentials, you can wire credentials directly into environment variables using the credential: prefix. In your .env file, set any environment variable value to credential:<dot.path>:
ANTHROPIC_API_KEY=credential:anthropic.api_key
OPENAI_API_KEY=credential:openai.api_key
When the worker starts, these references are resolved before any workflow code runs. ANTHROPIC_API_KEY becomes the actual decrypted value from config/credentials.yml.enc, so LLM SDKs and any library reading from process.env work without changes.

How Resolution Works

  1. Worker loads .envANTHROPIC_API_KEY is set to "credential:anthropic.api_key"
  2. Worker reads output.hookFiles from package.json and imports each listed file at startup. Including node_modules/@outputai/credentials/dist/hooks.js registers the credential resolution hook:
package.json
{
  "output": {
    "hookFiles": ["node_modules/@outputai/credentials/dist/hooks.js"]
  }
}
  1. Worker calls runStartupHooks()resolveCredentialRefs() scans process.env for credential: prefixed values and replaces each with the decrypted credential
  2. ANTHROPIC_API_KEY is now the real API key string
  3. LLM SDK reads it normally when a workflow activity runs
The worker logs the resolved variable names on startup:
Startup hooks resolved env vars {"vars":["ANTHROPIC_API_KEY","OPENAI_API_KEY"]}

Precedence

Real environment variable values are never overwritten. If ANTHROPIC_API_KEY is already set to a non-credential: value (from the shell, CI secret injection, etc.), it is left unchanged. This lets you override credentials at deploy time without modifying files.

The _env Section

New projects are scaffolded with an _env section in the credentials template that documents the intended mapping:
anthropic:
  api_key: sk-ant-...
openai:
  api_key: sk-...

_env:
  ANTHROPIC_API_KEY: anthropic.api_key
  OPENAI_API_KEY: openai.api_key
The _env section is metadata only — it documents the intent but doesn’t drive resolution. Resolution is driven by the credential: values in .env. Keep both in sync when you add credentials.

Programmatic Access

To call resolution outside the worker startup context:
import { resolveCredentialRefs } from '@outputai/credentials';

// Returns array of env var names that were resolved
const resolved = resolveCredentialRefs();
console.log('Resolved:', resolved);
// → ["ANTHROPIC_API_KEY", "OPENAI_API_KEY"]

Security

The credentials system uses AES-256-GCM encryption via the @noble/ciphers library. Each encryption generates a unique random nonce, so the same plaintext produces different ciphertext every time.
Never commit .key files to version control. Add *.key to your .gitignore. The encrypted .yml.enc files are safe to commit — they cannot be decrypted without the key.
Additional protections:
  • Key files are created with file mode 0o600 (owner read/write only)
  • During credentials edit, the temporary plaintext file is overwritten with null bytes before deletion

Custom Providers

The default provider reads encrypted YAML files from disk. You can replace it with a custom provider for alternative backends (e.g., AWS Secrets Manager, HashiCorp Vault) using setProvider():
import { setProvider } from '@outputai/credentials';
import type { CredentialsProvider } from '@outputai/credentials';

const vaultProvider: CredentialsProvider = {
  loadGlobal: ({ environment }) => {
    // Return credentials as a plain object
    return { anthropic: { api_key: fetchFromVault('anthropic-key') } };
  },
  loadForWorkflow: ({ workflowName, workflowDir, environment }) => {
    // Return workflow-specific credentials, or null to use global only
    return null;
  }
};

setProvider(vaultProvider);
MethodArgumentsReturns
loadGlobal(context){ environment: string | undefined }Record<string, unknown>
loadForWorkflow(context){ workflowName: string, workflowDir: string | undefined, environment?: string }Record<string, unknown> | null
Returning null from loadForWorkflow means the workflow has no overrides — global credentials are used as-is.

API Reference

For complete TypeScript API documentation, see the Credentials Module API Reference.