Skip to main content

Overview

The @tinyclaw/secrets package provides encrypted secrets management for Tiny Claw, powered by @wgtechlabs/secrets-engine. It stores API keys and other secrets with machine-bound AES-256-GCM encryption.

Features

  • Machine-bound encryption — Secrets are tied to the host machine
  • AES-256-GCM — Industry-standard authenticated encryption
  • Zero external dependencies — Self-contained storage
  • Dot-notation keys — Organized namespace (e.g., provider.ollama.apiKey)
  • Pattern-based listing — Glob support for key discovery

Storage Location

Secrets are stored in the user’s home directory at ~/.secrets-engine/ by default (configurable via SecretsConfig.path).

Installation

bun add @tinyclaw/secrets

Usage

Basic Setup

import { SecretsManager } from '@tinyclaw/secrets';

const secrets = await SecretsManager.create();

// Store a secret
await secrets.store('provider.openai.apiKey', 'sk-...');

// Retrieve a secret
const apiKey = await secrets.retrieve('provider.openai.apiKey');
console.log(apiKey); // 'sk-...'

// Clean up
await secrets.close();

Provider API Keys

// Store provider API key
await secrets.store('provider.anthropic.apiKey', 'sk-ant-...');

// Retrieve using helper method
const key = await secrets.resolveProviderKey('anthropic');
console.log(key); // 'sk-ant-...'

Check Before Retrieve

const exists = await secrets.check('provider.ollama.apiKey');
if (exists) {
  const key = await secrets.retrieve('provider.ollama.apiKey');
} else {
  console.log('API key not configured');
}

List Secrets

// List all secrets
const allKeys = await secrets.list();

// List with glob pattern (note: * does not cross dots)
const providerKeys = await secrets.list('provider.*.*');
console.log(providerKeys); // ['provider.openai.apiKey', 'provider.anthropic.apiKey', ...]

Custom Storage Path

const secrets = await SecretsManager.create({
  path: '/custom/path/.secrets',
});

console.log(secrets.storagePath); // '/custom/path/.secrets'

Agent Tools

import { createSecretsTools } from '@tinyclaw/secrets';

const secrets = await SecretsManager.create();
const tools = createSecretsTools(secrets);

// Now the agent has 4 tools:
// - store_secret
// - check_secret
// - retrieve_secret
// - list_secrets

API Reference

SecretsManager

SecretsManager.create(config?)

Create and initialize a secrets manager instance.
config
SecretsConfig
Configuration options.
config.path
string
Custom storage path. Defaults to ~/.secrets-engine/.
Returns: Promise<SecretsManager>

store(key, value)

Store or overwrite an encrypted secret.
key
string
required
Dot-notation key (e.g., "provider.ollama.apiKey").
value
string
required
The secret value to encrypt and store.
Returns: Promise<void>

check(key)

Check if a secret exists without decrypting it.
key
string
required
The key to check.
Returns: Promise<boolean> - true if the secret exists.

retrieve(key)

Retrieve and decrypt a stored secret.
key
string
required
The key to retrieve.
Returns: Promise<string | null> - The decrypted value, or null if not found.

list(pattern?)

List all secret key names matching an optional glob pattern.
pattern
string
Optional glob pattern to filter keys (e.g., "provider.*.*"). Note: * matches within a single dot-segment only.
Returns: Promise<string[]> - Array of matching key names (never secret values).

resolveProviderKey(providerName)

Retrieve a provider API key using the standard naming convention.
providerName
string
required
The provider name (e.g., "openai", "anthropic").
Returns: Promise<string | null> - The API key, or null if not found. This is a convenience method that constructs the key as provider.<providerName>.apiKey.

close()

Close the secrets engine and flush any pending writes. Returns: Promise<void>

Properties

size
number
Number of secrets currently stored.
storagePath
string
Absolute path to the secrets storage directory.

createSecretsTools(manager)

Create agent tools for secrets management.
manager
SecretsManager
required
The secrets manager instance.
Returns: Tool[] - Array containing 4 tools:
  • store_secret — Store or update a secret
  • check_secret — Check if a secret exists
  • retrieve_secret — Retrieve and decrypt a secret
  • list_secrets — List secret key names

Naming Conventions

Provider API Keys

API keys for LLM providers should follow this convention:
provider.<providerName>.apiKey
Examples:
  • provider.openai.apiKey
  • provider.anthropic.apiKey
  • provider.ollama.apiKey
  • provider.google.apiKey

Channel Secrets

Secrets for channel plugins (e.g., Discord bot tokens) should follow:
channel.<channelName>.<secretType>
Examples:
  • channel.discord.token
  • channel.telegram.botToken
  • channel.slack.webhookUrl

Custom Secrets

For custom secrets, use descriptive dot-notation:
await secrets.store('database.primary.password', '...');
await secrets.store('api.github.personalAccessToken', '...');
await secrets.store('service.stripe.apiKey', '...');

Helper Functions

buildProviderKeyName(providerName)

Construct a provider API key name.
import { buildProviderKeyName } from '@tinyclaw/secrets';

const keyName = buildProviderKeyName('openai');
console.log(keyName); // 'provider.openai.apiKey'

buildChannelKeyName(channelName, keyType)

Construct a channel secret key name.
import { buildChannelKeyName } from '@tinyclaw/secrets';

const keyName = buildChannelKeyName('discord', 'token');
console.log(keyName); // 'channel.discord.token'

Security Notes

  • Secrets are encrypted using AES-256-GCM with machine-bound keys
  • Encryption keys are derived from machine-specific identifiers
  • Secrets cannot be transferred between machines (by design)
  • Storage is local to the user’s home directory (not in project repos)
  • No secrets are ever logged or exposed in error messages

Error Handling

try {
  await secrets.store('invalid..key', 'value');
} catch (error) {
  console.error('Failed to store secret:', error.message);
}

try {
  const value = await secrets.retrieve('nonexistent.key');
  if (value === null) {
    console.log('Secret not found');
  }
} catch (error) {
  console.error('Failed to retrieve secret:', error.message);
}

Best Practices

  1. Always close the manager when done to ensure writes are flushed:
    await secrets.close();
    
  2. Check before retrieve to avoid null handling:
    if (await secrets.check('provider.openai.apiKey')) {
      const key = await secrets.retrieve('provider.openai.apiKey');
    }
    
  3. Use consistent naming to make secrets discoverable:
    // Good: follows convention
    await secrets.store('provider.anthropic.apiKey', '...');
    
    // Avoid: inconsistent naming
    await secrets.store('anthropicKey', '...');
    
  4. Never commit secrets to version control:
    # Add to .gitignore
    .secrets-engine/