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';
| Export | Description |
|---|
credentials | Read credentials via dot-notation paths (get, require) |
encrypt / decrypt / generateKey | Low-level AES-256-GCM encryption utilities |
setProvider / getProvider | Swap the credential storage backend |
encryptedYamlProvider | Default provider: encrypted YAML files on disk |
MissingCredentialError | Thrown by credentials.require() when a path has no value |
MissingKeyError | Thrown when no decryption key can be found |
resolveCredentialsPath / resolveKeyPath / … | Path resolution helpers for credentials and key files |
Quick Start
1. Initialize credentials
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
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
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.
| Method | Arguments | Returns | Throws |
|---|
credentials.get(path, defaultValue?) | path: string, defaultValue?: unknown | The credential value, the default, or undefined | Never |
credentials.require(path) | path: string | The credential value | MissingCredentialError 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:
| File | Path |
|---|
| Encrypted credentials | config/credentials.yml.enc |
| Encryption key | config/credentials.key |
Environment-specific — per NODE_ENV (production or development):
| File | Path |
|---|
| Encrypted credentials | config/credentials/{environment}.yml.enc |
| Encryption key | config/credentials/{environment}.key |
Per-workflow — scoped to a single workflow:
| File | Path |
|---|
| Encrypted credentials | src/workflows/{name}/credentials.yml.enc |
| Encryption key | src/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:
- Environment variable
OUTPUT_CREDENTIALS_KEY (or OUTPUT_CREDENTIALS_KEY_{ENVIRONMENT} for env-specific files)
- Key file at
config/credentials.key (or config/credentials/{environment}.key)
- Throws
MissingKeyError if neither is found
Workflow credentials:
- Environment variable
OUTPUT_CREDENTIALS_KEY_{WORKFLOW_NAME} (uppercased)
- Key file at
src/workflows/{name}/credentials.key
- Falls back to the global key resolution chain
| Scope | Environment Variable | Example |
|---|
| Global | OUTPUT_CREDENTIALS_KEY | OUTPUT_CREDENTIALS_KEY=a1b2c3... |
| Environment | OUTPUT_CREDENTIALS_KEY_{ENVIRONMENT} | OUTPUT_CREDENTIALS_KEY_PRODUCTION=a1b2c3... |
| Workflow | OUTPUT_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.
| Command | Description |
|---|
output credentials init | Generate an encryption key and encrypted credentials file |
output credentials edit | Decrypt, open in $EDITOR, re-encrypt on save |
output credentials show | Print decrypted credentials (for debugging) |
output credentials get <path> | Print a single credential value by dot-notation path |
Shared Flags
| Flag | Short | Description |
|---|
--environment | -e | Target environment (e.g., production, development) |
--workflow | -w | Target a specific workflow directory |
--force | -f | Overwrite 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
- Worker loads
.env — ANTHROPIC_API_KEY is set to "credential:anthropic.api_key"
- 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:
{
"output": {
"hookFiles": ["node_modules/@outputai/credentials/dist/hooks.js"]
}
}
- Worker calls
runStartupHooks() — resolveCredentialRefs() scans process.env for credential: prefixed values and replaces each with the decrypted credential
ANTHROPIC_API_KEY is now the real API key string
- 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);
| Method | Arguments | Returns |
|---|
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.