Skip to content

Environment Variables

The OpenPets SDK provides a robust environment variable management system through loadEnv, setEnv, and related functions. These functions handle loading configuration from multiple sources with proper priority ordering and persisting pet-specific settings.

Quick Start

typescript
import { loadEnv, setEnv, getEnv, clearEnv } from "openpets-sdk"

// Load all environment variables for a pet
const env = loadEnv("my-pet")
const apiKey = env.MY_PET_API_KEY

// Save environment variables to .env.pets
setEnv("my-pet", {
  MY_PET_API_KEY: "secret_key_here",
  MY_PET_BASE_URL: "https://api.example.com"
})

// Get a single environment variable
const key = getEnv("MY_PET_API_KEY", "my-pet")

// Clear all or specific environment variables
clearEnv("my-pet")                         // Clear all
clearEnv("my-pet", ["MY_PET_API_KEY"])     // Clear specific keys

loadEnv(petId)

Loads environment variables for a pet from multiple sources, merging them with proper priority.

Signature

typescript
function loadEnv(petId?: string): Record<string, string>

Parameters

ParameterTypeRequiredDescription
petIdstringNoThe pet identifier (e.g., "github", "jira"). If omitted, only global configuration is loaded.

Return Value

Returns an object containing all loaded environment variables as key-value pairs.

Load Order (Priority)

Environment variables are resolved from project-local files first, with the closest project directory taking precedence. If a key is not found in those files, loadEnv falls back to process.env, which lets external secret providers such as Doppler work by launching pets with those variables already present.

Resolution order is:

  1. pets.json or legacy .pets/config.json _global section - applies to all pets
  2. pets.json or legacy .pets/config.json pet-specific section - overrides global
  3. .env files - project environment files
  4. .env.pets files - OpenPets-managed local secrets
  5. System environment variables (process.env) - fallback when no project-local value exists

Directory Traversal

loadEnv searches for configuration files by traversing up from the current working directory:

  1. Current working directory
  2. Git repository root (if in a git repo)
  3. Parent directories containing .env, .env.pets, pets.json, or .pets/config.json

This allows plugins running from ~/.config/opencode to still access project-specific configuration.

Example

typescript
import { loadEnv, createLogger } from "openpets-sdk"

export const MyPlugin = async () => {
  const logger = createLogger("my-plugin")
  
  // Load environment variables for this pet
  const env = loadEnv("my-plugin")
  
  // Access variables - fallback to process.env for compatibility
  const API_KEY = env.MY_PLUGIN_API_KEY || process.env.MY_PLUGIN_API_KEY
  const BASE_URL = env.MY_PLUGIN_BASE_URL || "https://api.default.com"
  
  if (!API_KEY) {
    logger.warn("API key not configured")
    // Return limited toolset...
  }
  
  logger.info("Plugin initialized", { hasApiKey: !!API_KEY })
  // ...
}

Doppler-backed Projects

Pets should not call Doppler directly. A pet declares the variables it needs in package.json, reads them with loadEnv("<pet-id>"), and stays unaware of whether the values came from Doppler, .env.pets, .env, pets.json, or the shell.

Pet Contract

Declare the required variables in the pet package:

json
{
  "name": "@openpets/posthog",
  "envVariables": {
    "required": [
      {
        "name": "POSTHOG_API_KEY",
        "description": "PostHog personal API key"
      }
    ],
    "optional": [
      {
        "name": "POSTHOG_HOST",
        "description": "PostHog instance URL, for example https://eu.posthog.com"
      },
      {
        "name": "POSTHOG_PROJECT_ID",
        "description": "Default PostHog project ID"
      }
    ]
  }
}

Read those variables through loadEnv at runtime:

typescript
import { createPlugin, loadEnv, z } from "openpets-sdk"

export const PosthogPlugin = async () => {
  const env = loadEnv("posthog")
  const apiKey = env.POSTHOG_API_KEY
  const host = env.POSTHOG_HOST || "https://app.posthog.com"

  if (!apiKey) {
    return createPlugin([
      {
        name: "posthog-test-connection",
        description: "Check whether PostHog credentials are configured",
        schema: z.object({}),
        async execute() {
          return JSON.stringify({
            success: false,
            status: "not_configured",
            missing: ["POSTHOG_API_KEY"]
          })
        }
      }
    ])
  }

  // Build the full toolset using apiKey and host.
}

Project Setup With Doppler

Configure the pet in the target project without committing secrets:

json
{
  "$schema": "https://pets.raggle.co/config.json",
  "schemaVersion": 1,
  "enabled": ["posthog"],
  "petConfig": {
    "posthog": {
      "readOnly": true
    }
  }
}

Store the actual values in Doppler, using the exact names the pet declares:

text
POSTHOG_API_KEY
POSTHOG_HOST
POSTHOG_PROJECT_ID

Run OpenPets under Doppler so those values are present in process.env:

bash
doppler run --project my-project --config dev -- pets auth posthog --status
doppler run --project my-project --config dev -- pets tools posthog
doppler run --project my-project --config dev -- pets exec posthog-test-connection --plugin posthog --json

If the project already loads Doppler through direnv, the same commands can be run without the doppler run -- ... prefix after the .envrc has been allowed and loaded:

bash
direnv status
pets auth posthog --status
pets exec posthog-test-connection --plugin posthog --json

Do not use pets auth to copy Doppler-managed secrets into .env.pets unless you intentionally want a local override. pets auth --status is safe for checking whether the current shell or Doppler wrapper exposes the required variable names.

First-class Provider Support

If OpenPets adds first-class secret providers, keep the provider selection at the project configuration layer rather than inside individual pets. The provider should run before live pet loading, merge the returned key/value pairs into the loadEnv result, and avoid printing secret values.

Example future shape:

json
{
  "$schema": "https://pets.raggle.co/config.json",
  "schemaVersion": 1,
  "enabled": ["posthog"],
  "envProviders": [
    {
      "type": "doppler",
      "project": "my-project",
      "config": "dev",
      "pets": ["posthog"]
    }
  ]
}

The equivalent provider contract would be:

typescript
interface EnvProvider {
  type: string
  load(options: {
    projectDir: string
    petId?: string
  }): Promise<Record<string, string>>
}

Until that provider layer exists in the SDK and CLI, use doppler run -- ... or a project .envrc so Doppler populates process.env before OpenPets starts.


setEnv(petId, envVars, projectDir?)

Persists environment variables for a pet in .env.pets by default. This is the recommended local storage target for pet-specific credentials when a project is not using an external secret provider such as Doppler.

Signature

typescript
function setEnv(
  petId: string,
  envVars: Record<string, string>,
  projectDir?: string,
  options?: { target?: "secrets" | "config" }
): { success: boolean; message: string }

Parameters

ParameterTypeRequiredDescription
petIdstringYesThe pet identifier (e.g., "whoop", "github")
envVarsRecord<string, string>YesKey-value pairs to save
projectDirstringNoProject directory path. Defaults to process.cwd()
options.target"secrets" | "config"NoDefaults to "secrets", which writes .env.pets. Use "config" only for non-sensitive environment-style toggles in pets.json.

Return Value

Returns an object with:

  • success: boolean - Whether the operation succeeded
  • message: string - Success or error message

Behavior

  1. Creates or updates .env.pets when target is "secrets"
  2. Creates or updates pets.json under envConfig when target is "config"
  3. Automatically adds .env.pets and .pets/ to .git/info/exclude (local git exclude)
  4. Preserves existing configuration for other pets

Example: OAuth Token Storage

typescript
import { setEnv, loadEnv, createLogger } from "openpets-sdk"

const PET_ID = "whoop"
const logger = createLogger(PET_ID)

// After OAuth callback, save tokens
async function handleOAuthCallback(code: string) {
  const tokenData = await exchangeCodeForToken(code)
  
  // Save tokens to .env.pets
  const result = setEnv(PET_ID, {
    WHOOP_ACCESS_TOKEN: tokenData.access_token,
    WHOOP_REFRESH_TOKEN: tokenData.refresh_token || ""
  })
  
  if (result.success) {
    logger.info("Tokens saved successfully")
  } else {
    logger.error("Failed to save tokens", { error: result.message })
  }
  
  return result
}

// Later, load the saved tokens
function getAccessToken() {
  const env = loadEnv(PET_ID)
  return env.WHOOP_ACCESS_TOKEN || process.env.WHOOP_ACCESS_TOKEN
}

Example: Saving API Configuration

typescript
import { setEnv, z, createPlugin, type ToolDefinition } from "openpets-sdk"

const configTool: ToolDefinition = {
  name: "my-plugin-configure",
  description: "Save API configuration for future use",
  schema: z.object({
    apiKey: z.string().describe("Your API key"),
    baseUrl: z.string().optional().describe("Custom API base URL")
  }),
  async execute(args) {
    const config: Record<string, string> = {
      MY_PLUGIN_API_KEY: args.apiKey
    }
    
    if (args.baseUrl) {
      config.MY_PLUGIN_BASE_URL = args.baseUrl
    }
    
    const result = setEnv("my-plugin", config)
    
    return JSON.stringify({
      success: result.success,
      message: result.success 
        ? "Configuration saved to .env.pets"
        : result.message
    }, null, 2)
  }
}

getEnv(key, petId?)

Retrieves a single environment variable value.

Signature

typescript
function getEnv(key: string, petId?: string): string | undefined

Parameters

ParameterTypeRequiredDescription
keystringYesThe environment variable name
petIdstringNoThe pet identifier for pet-specific lookup

Example

typescript
import { getEnv } from "openpets-sdk"

const apiKey = getEnv("GITHUB_TOKEN", "github")
if (!apiKey) {
  throw new Error("GITHUB_TOKEN not configured")
}

clearEnv(petId, keys?, projectDir?)

Clears environment variables for a pet from .env.pets and, by default, legacy or non-secret values from pets.json envConfig.

Signature

typescript
function clearEnv(
  petId: string,
  keys?: string[],
  projectDir?: string
): { success: boolean; message: string }

Parameters

ParameterTypeRequiredDescription
petIdstringYesThe pet identifier
keysstring[]NoSpecific keys to clear. If omitted, clears all keys for the pet.
projectDirstringNoProject directory path. Defaults to process.cwd()

Example

typescript
import { clearEnv } from "openpets-sdk"

// Clear all configuration for a pet (e.g., on logout)
const result = clearEnv("whoop")
console.log(result.message) // "Cleared configuration for whoop"

// Clear only specific keys
clearEnv("my-plugin", ["MY_PLUGIN_API_KEY"])

getPetsConfig(projectDir?)

Retrieves the project pets.json configuration, falling back to legacy .pets/config.json when present.

Signature

typescript
function getPetsConfig(projectDir?: string): PetsConfig | null

Return Type

typescript
interface PetsConfig {
  enabled?: string[]
  disabled?: string[]
  envConfig?: Record<string, Record<string, string>>
  petConfig?: Record<string, PetSettings>
  last_updated?: string
}

Example

typescript
import { getPetsConfig } from "openpets-sdk"

const config = getPetsConfig()
if (config?.envConfig?.["my-plugin"]) {
  console.log("Pet config:", config.envConfig["my-plugin"])
}

Configuration File Format

pets.json Structure

json
{
  "$schema": "https://pets.raggle.co/config.json",
  "schemaVersion": 1,
  "enabled": ["jira", "github"],
  "disabled": ["deprecated-pet"],
  "envConfig": {
    "_global": {
      "OPENPETS_LOG_LEVEL": "info"
    },
    "openai": {
      "OPENAI_LOAD_SECONDARY_TOOLS": "true"
    },
    "jira": {
      "JIRA_BASE_URL": "https://company.atlassian.net"
    }
  },
  "petConfig": {
    "_global": {
      "readOnly": false
    },
    "jira": {
      "readOnly": true
    }
  },
  "last_updated": "2025-01-03T12:00:00.000Z"
}

Key Sections

SectionDescription
envConfig._globalEnvironment variables shared by all pets
envConfig.{petId}Pet-specific non-secret environment-style values or tool toggles (overrides global)
petConfig._globalSettings applied to all pets (e.g., readOnly)
petConfig.{petId}Pet-specific settings (overrides global)

Best Practices

1. Always Use loadEnv with Pet ID

typescript
// Good - loads pet-specific config including global overrides
const env = loadEnv("my-plugin")

// Avoid - only loads global config
const env = loadEnv()

2. Provide Fallback to process.env

For compatibility with users who set environment variables directly:

typescript
const API_KEY = env.MY_PLUGIN_API_KEY || process.env.MY_PLUGIN_API_KEY

3. Use setEnv for OAuth Tokens

OAuth flows should save tokens using setEnv so they persist across sessions:

typescript
// After successful OAuth
setEnv("my-plugin", {
  ACCESS_TOKEN: tokenData.access_token,
  REFRESH_TOKEN: tokenData.refresh_token
})

4. Handle Missing Configuration Gracefully

typescript
const env = loadEnv("my-plugin")
const API_KEY = env.MY_PLUGIN_API_KEY

if (!API_KEY) {
  // Return limited toolset with setup instructions
  return createPlugin([
    {
      name: "my-plugin-configure",
      description: "Configure the plugin with your API key",
      // ...
    }
  ])
}

// Full functionality when configured
return createPlugin(allTools)

5. Don't Log Sensitive Values

typescript
const logger = createLogger("my-plugin")
const env = loadEnv("my-plugin")

// Good - log presence, not value
logger.info("Configuration loaded", { 
  hasApiKey: !!env.MY_PLUGIN_API_KEY,
  hasBaseUrl: !!env.MY_PLUGIN_BASE_URL 
})

// Bad - never log secrets
// logger.info("API Key:", env.MY_PLUGIN_API_KEY)

Security Notes

Local Secret Files

  • .env.pets is automatically excluded from git via .git/info/exclude
  • Legacy .pets/ configuration is also excluded because older projects may contain credentials there
  • Do not commit .env.pets, .pets/, exported Doppler files, or generated secret snapshots
  • Each project should keep prompt-safe config in pets.json and local secrets in .env.pets or an external provider

Priority for Secrets

For sensitive values like API keys:

  1. Managed secrets: Use Doppler or another provider to populate process.env before running pets
  2. Local development: Use .env.pets via pets auth or setEnv
  3. Compatibility: Use .env when a project already has one
  4. Prompt-safe config: Use pets.json petConfig or non-secret envConfig values

Credential Rotation

When rotating credentials:

typescript
// Update with new credentials
setEnv("my-plugin", {
  MY_PLUGIN_API_KEY: "new_key_here"
})

// Verify new credentials work
const env = loadEnv("my-plugin")
const isValid = await testConnection(env.MY_PLUGIN_API_KEY)

// If failed, can clear and restore old method
if (!isValid) {
  clearEnv("my-plugin", ["MY_PLUGIN_API_KEY"])
}