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_KEYenvironment 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:
- Fetches the secret row and validates its status
- Identifies the requested version (or "latest")
- Invokes the provider's
resolveVersionmethod to obtain plaintext - Records access via
recordAccessEventfor 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
heartbeatRunstable
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 oraccess.*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:
createRunSecretRedactionRegistryinitializes helpers for the runredactForRunandredactForIssuescan for registered secret values- Matches are replaced with
REDACTED_EVENT_VALUEbefore 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
server/src/services/secrets.ts— Central secret service with resolution, validation, and audit loggingserver/src/secrets/provider-registry.ts— Provider registration and lookupserver/src/secrets/local-encrypted-provider.ts— Default AES encryption provider with master key managementserver/src/services/run-secret-redaction.ts— Runtime log redaction registryserver/src/home-paths.ts— Master key file location resolutionpackages/shared/src/types/secrets.ts— Core type definitions (SecretProvider,EnvBinding)server/src/services/authorization.ts— Authorization layer for agent access control- Heartbeat runs schema — Run context snapshots containing redaction state
Summary
- Encrypted at rest: All secrets use AES encryption (local) or cloud-native encryption (AWS/GCP) with master key protection
- Bound before access: Explicit
companySecretBindingsrows 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →