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"])
}