How Agent API Keys Work with Hash-Based Storage and Company Boundaries in Paperclip
Agent API keys in Paperclip are stored as SHA‑256 hashes (never plain text) and automatically scope every request to a specific company by attaching companyId to the request actor.
Paperclip's agent API key system combines cryptographic hashing with strict tenant isolation. This article explains how the open-source paperclipai/paperclip repository implements secure key storage and enforces company boundaries at every layer of the request lifecycle.
How Agent API Keys Are Created and Hashed
When you create an agent API key, Paperclip never stores the actual secret. Instead, the createKey service in server/src/services/agents.ts performs a one-way transformation:
// server/src/services/agents.ts – createKey
const token = createToken(); // random secret
const keyHash = hashToken(token); // SHA‑256 hash
await db.insert(agentApiKeys).values({
agentId: id,
companyId: existing.companyId,
name,
keyHash,
responsibleUserId: options?.responsibleUserId?.trim() || null,
scopeConfig: scope.kind === "standard" ? null : scope,
});
return { token, /* other metadata */ };
The hashToken function applies SHA‑256 to the random token. Only the resulting keyHash is persisted to the agent_api_keys table defined in packages/db/src/schema/agent_api_keys.ts. The original token is returned to the client exactly once—if lost, it cannot be recovered.
Authenticating Requests with Hash-Based Lookup
The actorMiddleware in server/src/middleware/auth.ts handles every incoming request bearing an Authorization: Bearer <token> header. It re-hashes the incoming token and performs a constant-time lookup:
// server/src/middleware/auth.ts – actorMiddleware
const token = authHeader.slice("bearer ".length).trim();
const tokenHash = hashToken(token);
const key = await db
.select()
.from(agentApiKeys)
.where(and(eq(agentApiKeys.keyHash, tokenHash), isNull(agentApiKeys.revokedAt)))
.then(rows => rows[0] ?? null);
if (!key) { /* fallback to JWT or reject */ }
This design ensures the secret token never traverses the network after creation. A leaked database dump exposes only irreversible hashes, rendering the keys useless to attackers.
Enforcing Company Boundaries Through Actor Scoping
Once a key is authenticated, Paperclip binds the request to a specific tenant. The middleware populates req.actor with the companyId stored alongside the key:
// server/src/middleware/auth.ts
req.actor = {
type: "agent",
agentId: key.agentId,
companyId: key.companyId,
keyId: key.id,
keyScope: normalizeAgentApiKeyScope(key.scopeConfig),
onBehalfOfUserId: normalizeOptionalString(key.responsibleUserId),
// …
};
All downstream queries automatically filter by req.actor.companyId:
// Any service that queries resources
await db.select().from(someTable)
.where(eq(someTable.companyId, req.actor.companyId));
This guarantees that an agent key issued for Company A can never access Company B's resources, even if the key hash is somehow compromised and replayed.
Responsible User Requirements and Audit Logging
Paperclip enforces traceability by requiring every agent key to have a responsibleUserId. If this field is missing, the request is rejected and audited:
// server/src/middleware/auth.ts
if (!key.responsibleUserId) {
await auditAgentKeyMissingResponsibleUser(...);
throw forbidden("Responsible user is unavailable for this agent key");
}
The auditAgentKeyMissingResponsibleUser function logs the event for security review. This prevents anonymous agent actions and maintains accountability across company boundaries.
Key Revocation and Usage Tracking
Each authentication attempt updates lastUsedAt on the key record:
await db.update(agentApiKeys)
.set({ lastUsedAt: new Date() })
.where(eq(agentApiKeys.id, key.id));
Keys can be revoked by setting revokedAt to a timestamp. The authentication query explicitly checks isNull(agentApiKeys.revokedAt), ensuring revoked keys immediately fail validation.
Summary
- Hash-based storage — Agent API keys are SHA‑256 hashed before storage; only hashes touch the database.
- One-way secrets — Original tokens are generated once and never retrievable; loss requires key rotation.
- Company scoping — Every authenticated request carries
companyIdinreq.actor, enforcing tenant isolation automatically. - Responsible user enforcement — Missing
responsibleUserIdtriggers 403 Forbidden and audit logging. - Operational visibility —
lastUsedAttracking andrevokedAtchecks support key lifecycle management.
Frequently Asked Questions
How does Paperclip prevent database leaks from exposing agent API keys?
Paperclip stores only SHA‑256 hashes of tokens in the keyHash column. The original random token is returned to the client once during creation and discarded. Because the hash function is one-way, an attacker with database access cannot reverse the hashes to obtain usable tokens.
What happens if an agent API key is used without a responsible user?
The actorMiddleware checks key.responsibleUserId immediately after authentication. If null, it calls auditAgentKeyMissingResponsibleUser and throws a 403 Forbidden error. This ensures every agent action is traceable to a human user within the company.
Can an agent API key from one company access another company's data?
No. The companyId field is stored with each key and injected into req.actor.companyId during authentication. All database queries filter by this value, making cross-tenant access impossible regardless of key compromise or misconfiguration.
Where is the token hashing function implemented in Paperclip?
The hashToken function is defined in server/src/middleware/auth.ts at lines 53–55. It uses SHA‑256 to transform the bearer token into a lookup key for the agent_api_keys table.
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 →