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
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 keysloadEnv(petId)
Loads environment variables for a pet from multiple sources, merging them with proper priority.
Signature
function loadEnv(petId?: string): Record<string, string>Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
petId | string | No | The 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:
pets.jsonor legacy.pets/config.json_globalsection - applies to all petspets.jsonor legacy.pets/config.jsonpet-specific section - overrides global.envfiles - project environment files.env.petsfiles - OpenPets-managed local secrets- 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:
- Current working directory
- Git repository root (if in a git repo)
- 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
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:
{
"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:
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:
{
"$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:
POSTHOG_API_KEY
POSTHOG_HOST
POSTHOG_PROJECT_IDRun OpenPets under Doppler so those values are present in process.env:
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 --jsonIf 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:
direnv status
pets auth posthog --status
pets exec posthog-test-connection --plugin posthog --jsonDo 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:
{
"$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:
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
function setEnv(
petId: string,
envVars: Record<string, string>,
projectDir?: string,
options?: { target?: "secrets" | "config" }
): { success: boolean; message: string }Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
petId | string | Yes | The pet identifier (e.g., "whoop", "github") |
envVars | Record<string, string> | Yes | Key-value pairs to save |
projectDir | string | No | Project directory path. Defaults to process.cwd() |
options.target | "secrets" | "config" | No | Defaults 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 succeededmessage:string- Success or error message
Behavior
- Creates or updates
.env.petswhentargetis"secrets" - Creates or updates
pets.jsonunderenvConfigwhentargetis"config" - Automatically adds
.env.petsand.pets/to.git/info/exclude(local git exclude) - Preserves existing configuration for other pets
Example: OAuth Token Storage
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
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
function getEnv(key: string, petId?: string): string | undefinedParameters
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | The environment variable name |
petId | string | No | The pet identifier for pet-specific lookup |
Example
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
function clearEnv(
petId: string,
keys?: string[],
projectDir?: string
): { success: boolean; message: string }Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
petId | string | Yes | The pet identifier |
keys | string[] | No | Specific keys to clear. If omitted, clears all keys for the pet. |
projectDir | string | No | Project directory path. Defaults to process.cwd() |
Example
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
function getPetsConfig(projectDir?: string): PetsConfig | nullReturn Type
interface PetsConfig {
enabled?: string[]
disabled?: string[]
envConfig?: Record<string, Record<string, string>>
petConfig?: Record<string, PetSettings>
last_updated?: string
}Example
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
{
"$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
| Section | Description |
|---|---|
envConfig._global | Environment variables shared by all pets |
envConfig.{petId} | Pet-specific non-secret environment-style values or tool toggles (overrides global) |
petConfig._global | Settings applied to all pets (e.g., readOnly) |
petConfig.{petId} | Pet-specific settings (overrides global) |
Best Practices
1. Always Use loadEnv with Pet ID
// 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:
const API_KEY = env.MY_PLUGIN_API_KEY || process.env.MY_PLUGIN_API_KEY3. Use setEnv for OAuth Tokens
OAuth flows should save tokens using setEnv so they persist across sessions:
// After successful OAuth
setEnv("my-plugin", {
ACCESS_TOKEN: tokenData.access_token,
REFRESH_TOKEN: tokenData.refresh_token
})4. Handle Missing Configuration Gracefully
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
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.petsis 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.jsonand local secrets in.env.petsor an external provider
Priority for Secrets
For sensitive values like API keys:
- Managed secrets: Use Doppler or another provider to populate
process.envbefore runningpets - Local development: Use
.env.petsviapets authorsetEnv - Compatibility: Use
.envwhen a project already has one - Prompt-safe config: Use
pets.jsonpetConfigor non-secretenvConfigvalues
Credential Rotation
When rotating credentials:
// 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"])
}Related Documentation
- Configuration Guide - General configuration options
- Read-Only Mode - Restricting pets to read operations
- Security Guide - Security best practices
- API Reference - Full SDK API documentation