Skip to content
Bun
Bun
Utilities

Secrets

Use Bun's Secrets API to store and retrieve sensitive credentials securely

Store and retrieve sensitive credentials securely using the operating system’s native credential storage APIs.

This API is new and experimental. It may change in the future.
index.ts
import { secrets } from "bun";

let githubToken: string | null = await secrets.get({
  service: "my-cli-tool",
  name: "github-token",
});

if (!githubToken) {
  githubToken = prompt("Please enter your GitHub token");

  await secrets.set({
    service: "my-cli-tool",
    name: "github-token",
    value: githubToken,
  });

  console.log("GitHub token stored");
}

const response = await fetch("https://api.github.com/user", {
  headers: { Authorization: `token ${githubToken}` },
});

console.log(`Logged in as ${(await response.json()).login}`);

Overview

Bun.secrets provides a cross-platform API for managing sensitive credentials that CLI tools and development applications typically store in plaintext files like ~/.npmrc, ~/.aws/credentials, or .env. It uses:

  • macOS: Keychain Services
  • Linux: libsecret (GNOME Keyring, KWallet, and other secret service daemons)
  • Windows: Windows Credential Manager

All operations are asynchronous and non-blocking, running on Bun’s threadpool.

This API is mostly useful for local development tools. We may later add a provider option for production deployment secrets.


API

Bun.secrets.get(options)

Retrieve a stored credential.

import { secrets } from "bun";

const password = await Bun.secrets.get({
  service: "my-app",
  name: "[email protected]",
});
// Returns: string | null

// Or if you prefer without an object
const password = await Bun.secrets.get("my-app", "[email protected]");

Parameters:

  • options.service (string, required) - The service or application name
  • options.name (string, required) - The username or account identifier

Returns:

  • Promise<string | null> - The stored password, or null if not found

Bun.secrets.set(options)

Store or update a credential.

import { secrets } from "bun";

await secrets.set({
  service: "my-app",
  name: "[email protected]",
  value: "super-secret-password",
});

Parameters:

  • options.service (string, required) - The service or application name
  • options.name (string, required) - The username or account identifier
  • options.value (string, required) - The password or secret to store
  • options.persist ("local" or "enterprise", optional) - Windows only. Which computers can see the credential. Defaults to "enterprise". See Windows (Credential Manager)

Notes:

  • If a credential already exists for the given service/name combination, Bun replaces it
  • The operating system encrypts the stored value

Bun.secrets.delete(options)

Delete a stored credential.

const deleted = await Bun.secrets.delete({
  service: "my-app",
  name: "[email protected]",
});
// Returns: boolean

Parameters:

  • options.service (string, required) - The service or application name
  • options.name (string, required) - The username or account identifier

Returns:

  • Promise<boolean> - true if a credential was deleted, false if not found

Examples

Storing CLI Tool Credentials

// Store GitHub CLI token (instead of ~/.config/gh/hosts.yml)
await Bun.secrets.set({
  service: "my-app.com",
  name: "github-token",
  value: "ghp_xxxxxxxxxxxxxxxxxxxx",
});

// Or if you prefer without an object
await Bun.secrets.set("my-app.com", "github-token", "ghp_xxxxxxxxxxxxxxxxxxxx");

// Store npm registry token (instead of ~/.npmrc)
await Bun.secrets.set({
  service: "npm-registry",
  name: "https://registry.npmjs.org",
  value: "npm_xxxxxxxxxxxxxxxxxxxx",
});

// Retrieve for API calls
const token = await Bun.secrets.get({
  service: "my-app.com",
  name: "github-token",
});

if (token) {
  const response = await fetch("https://api.github.com/user", {
    headers: {
      Authorization: `token ${token}`,
    },
  });
}

Migrating from Plaintext Config Files

// Instead of storing in ~/.aws/credentials
await Bun.secrets.set({
  service: "aws-cli",
  name: "AWS_SECRET_ACCESS_KEY",
  value: process.env.AWS_SECRET_ACCESS_KEY,
});

// Instead of .env files with sensitive data
await Bun.secrets.set({
  service: "my-app",
  name: "api-key",
  value: "sk_live_xxxxxxxxxxxxxxxxxxxx",
});

// Load at runtime
const apiKey =
  (await Bun.secrets.get({
    service: "my-app",
    name: "api-key",
  })) || process.env.API_KEY; // Fallback for CI/production

Error Handling

try {
  await Bun.secrets.set({
    service: "my-app",
    name: "alice",
    value: "password123",
  });
} catch (error) {
  console.error("Failed to store credential:", error.message);
}

// Check if a credential exists
const password = await Bun.secrets.get({
  service: "my-app",
  name: "alice",
});

if (password === null) {
  console.log("No credential found");
}

Updating Credentials

// Initial password
await Bun.secrets.set({
  service: "email-server",
  name: "[email protected]",
  value: "old-password",
});

// Update to new password
await Bun.secrets.set({
  service: "email-server",
  name: "[email protected]",
  value: "new-password",
});

// The old password is replaced

Platform Behavior

macOS (Keychain)

  • Bun stores credentials in the user’s login keychain
  • The keychain may prompt for access permission on first use
  • Credentials persist across system restarts
  • Accessible by the user who stored them

Linux (libsecret)

  • Requires a secret service daemon such as GNOME Keyring or KWallet
  • Bun stores credentials in the default collection
  • May prompt for unlock if the keyring is locked
  • The secret service must be running

Windows (Credential Manager)

  • Bun stores credentials in Windows Credential Manager
  • Visible in Control Panel → Credential Manager → Windows Credentials
  • Persisted with the CRED_PERSIST_ENTERPRISE flag by default, so they’re scoped per user
  • Encrypted using Windows Data Protection API

With CRED_PERSIST_ENTERPRISE, a user account that has roaming state, such as a roaming profile on a domain, also sees the credential on other computers. To keep a secret on the current computer, for example a refresh token that belongs to one device, pass persist: "local". Bun then stores the credential with CRED_PERSIST_LOCAL_MACHINE.

import { secrets } from "bun";

await secrets.set({
  service: "my-app",
  name: "refresh-token",
  value: "rt_xxxxxxxxxxxxxxxxxxxx",
  persist: "local",
});

Every set() replaces the whole entry, so the persist of the latest set() applies. macOS and Linux ignore persist.

Security Considerations

  1. Encryption: The operating system’s credential manager encrypts credentials
  2. Access Control: Only the user who stored the credential can retrieve it
  3. No Plain Text: Passwords are never stored in plain text
  4. Memory Safety: Bun zeros out password memory after use
  5. Process Isolation: Credentials are isolated per user account

Limitations

  • Maximum password length varies by platform (typically 2048-4096 bytes)
  • Keep service and name reasonably short (under 256 characters)
  • Some special characters may need escaping depending on the platform
  • Requires appropriate system services:
    • Linux: Secret service daemon must be running
    • macOS: Keychain Access must be available
    • Windows: Credential Manager service must be enabled

Comparison with Environment Variables

Unlike environment variables, Bun.secrets:

  • ✅ Encrypts credentials at rest (thanks to the operating system)
  • ✅ Avoids exposing secrets in process memory dumps (Bun zeros the memory after it’s no longer needed)
  • ✅ Survives application restarts
  • ✅ Can be updated without restarting the application
  • ✅ Provides user-level access control
  • ❌ Requires OS credential service
  • ❌ Not very useful for deployment secrets (use environment variables in production)

Best Practices

  1. Use descriptive service names: Match the tool or application name If you’re building a CLI for external use, use a UTI (Uniform Type Identifier) for the service name.

    // Good - matches the actual tool
    { service: "com.docker.hub", name: "username" }
    { service: "com.vercel.cli", name: "team-name" }
    
    // Avoid - too generic
    { service: "api", name: "key" }
  2. Credentials-only: Don’t store application configuration in this API This API is slow; keep non-secret settings in a config file.

  3. Use for local development tools:

    • ✅ CLI tools (gh, npm, docker, kubectl)
    • ✅ Local development servers
    • ✅ Personal API keys for testing
    • ❌ Production servers (use proper secret management)

TypeScript

namespace Bun {
  interface SecretsOptions {
    service: string;
    name: string;
  }

  interface Secrets {
    get(options: SecretsOptions): Promise<string | null>;
    set(options: SecretsOptions & { value: string; persist?: "local" | "enterprise" }): Promise<void>;
    delete(options: SecretsOptions): Promise<boolean>;
  }

  const secrets: Secrets;
}
Was this page helpful?Suggest editsRaise issue