How Paperclip Manages Secrets and Injects Them Into Agent Execution Contexts

Paperclip treats every secret as a first-class resource with encrypted storage, pluggable providers, strict authorization checks, and automatic log redaction to ensure secrets never leak in agent runtime environments.

The open-source Paperclip platform (paperclipai/paperclip) provides a comprehensive secrets management system that secures sensitive data from storage through injection into agent execution contexts. This article examines the complete lifecycle—from provider abstraction and binding configuration to runtime resolution and automatic redaction—based on the actual source code implementation.

Secret Storage Architecture and Provider Abstraction

Paperclip's secrets system begins with a pluggable provider architecture that abstracts how secrets are encrypted, stored, and retrieved.

The Secret Service Core

The secretService factory in server/src/services/secrets.ts serves as the central orchestrator. It exposes CRUD operations and implements the core resolution logic used throughout the platform.

Providers are registered in server/src/secrets/provider-registry.ts and must implement the common interface defined in packages/shared/src/types/secrets.ts. The default implementation is the local encrypted provider (server/src/secrets/local-encrypted-provider.ts), which:

  • Encrypts secret values using AES encryption
  • Derives the master key from the PAPERCLIP_SECRETS_MASTER_KEY environment variable or a file on disk
  • Locates the key file via server/src/home-paths.ts (default: ~/.paperclip/instances/{instance}/secrets/key)

Alternative providers include AWS Secrets Manager and GCP Secret Manager implementations, selectable per-secret.

Resolving Secret Values: The Internal Flow

When a secret value is needed, resolveSecretValueInternal in secrets.ts executes a multi-step retrieval process:

  1. Fetches the secret row and validates its status
  2. Identifies the requested version (or "latest")
  3. Invokes the provider's resolveVersion method to obtain plaintext
  4. Records access via recordAccessEvent for audit compliance

The plaintext value is never cached beyond the immediate request, and every access is logged to the secret-access audit log.

Agent-Specific Secret Access and Authorization

Agents cannot directly request arbitrary secrets. Instead, they use resolveSecretValueForAgentAccess, which enforces a strict authorization protocol.

Required Credentials

Agents must present:

  • A valid agent JWT (agent_jwt)
  • A verified heartbeat run ID from the heartbeatRuns table

The function validates the run exists, confirms the agent's authorization through authorizationService, and verifies the binding context.

Binding Context Verification

Bindings define which secrets an agent can access and where they map in the runtime environment. The system checks:

  • Config path patterns: env.* for environment variables or access.* for API access
  • Binding ID validation: If supplied, the binding must appear in the run's manifest (paperclipSecrets.manifest)
// Resolve a secret for an agent (called from an adapter)
import { secretService } from "@paperclipai/server/src/services/secrets";

async function getOpenAiKey(
  db: Db,
  companyId: string,
  agentId: string,
  runId: string,
) {
  const ctx = {
    agentId,
    configPath: "env.OPENAI_API_KEY",           // configPath is the binding key
    heartbeatRunId: runId,
    actorSource: "agent_jwt" as const,
    registerForRedaction: async (value) => {/* store for later redaction */},
  };

  // Returns { value: string, version: number }
  const { value } = await secretService(db).resolveSecretValueForAgentAccess(
    companyId,
    "secret-openai",   // secretId from DB
    "latest",
    ctx,
  );
  return value; // plaintext key that the adapter can set in the process env
}

Injecting Secrets Into Agent Runtime Environments

Once authorized and resolved, secrets flow into agent execution through a controlled injection mechanism.

The registerForRedaction callback—passed in the binding context—captures the plaintext value for later log scrubbing. The adapter code then places the secret into the agent's environment, typically:

  • Setting process.env[KEY] in Docker containers
  • Passing values via sandbox APIs
  • Injecting into configuration files for isolated runtimes

The adapter layer (implemented in adapter-utils or provider-specific adapters) handles the final delivery without exposing secrets to unauthorized components.

Automatic Secret Redaction in Logs

Paperclip prevents accidental secret exposure through runtime redaction that intercepts all logged output.

The run-secret-redaction module (server/src/services/run-secret-redaction.ts) maintains per-run registries of secret fingerprints in the contextSnapshot. When an agent produces output:

  1. createRunSecretRedactionRegistry initializes helpers for the run
  2. redactForRun and redactForIssue scan for registered secret values
  3. Matches are replaced with REDACTED_EVENT_VALUE before persistence or API response
// Register a secret value for redaction (used by the runtime when logging)
import { createRunSecretRedactionRegistry } from "@paperclipai/server/src/services/run-secret-redaction";

async function logSomething(
  db: Db,
  companyId: string,
  runId: string,
  msg: string,
) {
  const redactor = createRunSecretRedactionRegistry(db);
  const safeMsg = await redactor.redactForRun(companyId, runId, msg);
  console.log(safeMsg); // secret values replaced with "***REDACTED***"
}

Binding Secrets to Agents

Before runtime resolution, secrets must be explicitly bound to their consumers. The companySecretBindings table stores these relationships with metadata about target type, target ID, and config path mapping.

// Adding a binding for an agent (e.g., via API)
import { secretService } from "@paperclipai/server/src/services/secrets";

await db.insert(companySecretBindings).values({
  companyId,
  secretId,
  targetType: "agent",
  targetId: agentId,
  configPath: "env.OPENAI_API_KEY",
  // other fields …
});

The assertBindingContext function in secrets.ts enforces these constraints during resolution, ensuring agents cannot access unbound secrets even if they bypass higher-level checks.

Complete Secret Lifecycle in Paperclip

Step Implementation Key File
Create/Import Provider-specific encryption localEncryptedProvider.createSecret or cloud provider equivalents
Persist Database rows with encrypted material secretService CRUD in server/src/services/secrets.ts
Bind Config path mapping to consumer companySecretBindings table; assertBindingContext
Resolve for Agent JWT + run validation → binding check → provider resolution resolveSecretValueForAgentAccess
Inject Adapter receives plaintext, sets environment Adapter implementations (Docker, sandbox, etc.)
Redact Fingerprint-based log scrubbing run-secret-redaction.ts

Key Implementation Files

Summary

  • Encrypted at rest: All secrets use AES encryption (local) or cloud-native encryption (AWS/GCP) with master key protection
  • Bound before access: Explicit companySecretBindings rows with config path mapping prevent unauthorized retrieval
  • Strict agent authorization: JWT + verified run ID + binding manifest verification required for every resolution
  • Automatic runtime redaction: All secrets registered during injection are scrubbed from logs via fingerprint matching
  • Full audit trail: Every access recorded with context for compliance and security monitoring

Frequently Asked Questions

How does Paperclip encrypt secrets when using the default local provider?

The local encrypted provider in server/src/secrets/local-encrypted-provider.ts uses AES encryption with a master key loaded from either the PAPERCLIP_SECRETS_MASTER_KEY environment variable or a file located via server/src/home-paths.ts. The provider encrypts secret values before storage and decrypts them only during resolution, with the master key never persisted in the database.

What prevents an agent from accessing secrets belonging to another agent?

Three layers enforce isolation: the authorizationService validates the agent JWT, the heartbeat run ID is verified against the heartbeatRuns table, and the binding context (assertBindingContext) confirms the secret is explicitly bound to that specific agent via companySecretBindings. If the binding ID is supplied, it must also appear in the run's paperclipSecrets.manifest.

How does Paperclip ensure secrets don't appear in agent logs or API responses?

Before any secret value is returned to an agent, the registerForRedaction callback captures its fingerprint. The run-secret-redaction.ts module maintains these fingerprints in the run's contextSnapshot. All logging passes through redactForRun or redactForIssue, which replace any registered secret values with REDACTED_EVENT_VALUE before storage or transmission.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →