How to Pass Tokens Between AI Agents Without Writing Them Into Your Workflow State

Kubbi Team8 min read
A step-by-step procedure for removing credentials, OAuth tokens, and PII from LangGraph and CrewAI workflow state at agent handoff points, using temporary claim URLs instead.

OWASP's AI Agent Security Cheat Sheet is specific about what secure inter-agent communication requires: signed messages, validated memory writes, sensitive-field redaction in logs. It even provides Python class implementations. SecureAgentBus with JWT signing. SecureAgentMemory with TTL and checksums. PermissionManager wrappers. What it doesn't provide is the infrastructure to run any of this in production. That gap is where most teams currently live: they know the right answer, they don't have time to build it, and credentials keep ending up in queue messages. 1

This article is a procedure. It assumes you have a LangGraph or CrewAI pipeline with at least one sensitive payload crossing an agent boundary. API key, OAuth token, PII record, it doesn't matter. The goal is to remove that payload from your workflow state entirely and replace it with a claim URL the consuming agent redeems at the point of use.

Why workflow state is the wrong place for credentials

In a multi-agent pipeline, the message or state object that travels between agents is typically serialized, logged, retried on failure, and inspected during debugging. Every one of those operations is a potential exposure. When you put a credential into workflow state, you're not just passing it to the next agent. You're writing it into your orchestration layer's memory, your retry queue, and your debug logs simultaneously.

The OWASP framework treats inter-agent messages the same as API traffic: authenticated, schema-validated, and logged. That's correct. The problem is that when you log an authenticated message that contains an OAuth token, you've now stored that token in plaintext in your log aggregator. 1

IBM's work on agent memory demonstrates the same failure mode from a different angle. Persistent or unbounded agent memory can quietly accumulate sensitive information including API keys, personal data, and internal system details far longer than intended, making memory token limits a security control, not just a performance concern. A credential that enters agent memory at step 2 can still be sitting there at step 14. 2

The fix isn't better log scrubbing or more aggressive memory cleanup. Both of those require you to write and maintain additional code that knows about your data shapes. The fix is to not put the credential into workflow state in the first place.

Step 1: Identify every handoff point where a sensitive payload enters workflow state

Before you change anything, map the problem. Go through your pipeline and find every place where a credential, token, or PII record gets written into a message, a state dictionary, a tool call parameter, or a shared database row. In LangGraph, this is usually the state object passed between nodes. In CrewAI, it's the context or output passed between agents and tasks.

Look for three specific patterns. First: a value that was retrieved from a secrets manager or an OAuth flow getting written directly into the state dict. Second: a tool call whose parameters include something that shouldn't appear in a log. Third: a shared database row used as a handoff mechanism, where the sensitive value sits in a column until the next agent picks it up and someone cleans it up later.

The third pattern is the most common and the least visible. A database row looks like infrastructure, not like a credential in flight. But the data doesn't know it's temporary. It sits there until your cleanup job runs, and your cleanup job only runs if your cleanup code is correct and your orchestrator doesn't die between writes.

Step 2: Replace the payload with a kubbi.create() call at the producer

At each handoff point you identified in step 1, the producing agent or workflow step calls kubbi.create() instead of writing the payload into state. You pass the payload, a TTL, and a claim limit of one. The call returns a claim URL. That URL is what you write into workflow state.

The payload is encrypted with AES-256 before it touches storage. The claim URL carries no information about the payload itself. If your workflow state is serialized, logged, or inspected, the log entry contains a URL. The credential is not there.

The TTL is a hard expiry. Set it to something that reflects the actual expected latency of your pipeline, not the maximum theoretical latency. If your pipeline normally completes in 30 seconds, a 5-minute TTL gives you room for retries without leaving credentials sitting around indefinitely. The payload expires whether or not anyone claims it. You don't write cleanup code. You don't think about it again.

In LangGraph, this change happens inside the node function that currently writes the credential into state. The node still writes to state, but it writes a string URL instead of the credential value. The state schema doesn't need to change. The downstream graph structure doesn't need to change.

Step 3: The consuming agent calls kubbi.claim() at the point of use

The consuming agent reads the claim URL from workflow state and calls kubbi.claim() at the exact moment it needs the credential. Not at agent startup. Not during initialization. At the point of use: the line of code that makes the API call, opens the connection, or processes the PII record.

The payload is decrypted, delivered once, and gone. The claim limit of one means a second call to the same URL returns nothing. If your retry logic causes the consuming agent to run twice, the second run won't get the credential from the URL. You need to account for this in your retry design: either re-issue a new kubbi before retry, or handle the empty-claim case as a recoverable error that triggers re-issuance upstream.

The credential is now present in exactly one place at exactly one time: in memory, in the consuming agent, for the duration of the operation that needs it. It never lived in your message queue. It never lived in your workflow state. It never lived in your logs.

The common pitfall: treating the claim URL as the safe thing to pass carelessly

The claim URL is a bearer token. Anyone who has it can claim the payload, once, before the TTL expires. That's the design. It means the URL should travel through exactly the same authenticated path as the rest of your workflow state.

If you would not paste your OAuth token into a Slack message, don't paste the claim URL into a Slack message either. If you would not store a credential in a public S3 bucket, don't store the claim URL there. The security model works because the URL travels through your authenticated orchestration channel. Break that assumption and you've moved the exposure from the credential to the URL, which is a smaller win than it sounds.

In practice, most LangGraph and CrewAI pipelines already have an authenticated state store. The claim URL rides in that store the same way any other state value does. No new infrastructure is required for the URL itself. The change is in what the URL represents.

What this pattern maps to in the OWASP framework

OWASP's secure memory implementation calls for TTL expiration, redaction of sensitive data patterns in stored entries, and cryptographic integrity checksums. The pattern described here satisfies the TTL and redaction requirements without requiring you to implement or maintain any of that infrastructure yourself. The sensitive value never enters the memory layer. It can't appear in a memory dump because it was never there. 1

OWASP also classifies inter-agent messages as requiring the same controls as API traffic. The claim URL approach is consistent with that framing. The message carries a reference, not a value. The reference is useless without the authenticated claim operation. This is closer to a token exchange pattern than to passing a secret inline.

The gap the OWASP cheat sheet leaves open is the infrastructure question. Building a SecureAgentBus with JWT signing and a SecureAgentMemory with per-entry checksums is correct in principle. A five-person engineering team building features doesn't have the capacity to own that stack in production. The kubbi pattern is the same security property with none of the maintenance surface.

Adding producer-side revocation for human-in-the-loop workflows

Human-in-the-loop steps create a specific problem: the reviewing human may never complete the review. The workflow pauses. The credential sits wherever you put it, waiting for an action that might not come for hours, days, or at all.

OWASP's risk classification puts human approval gates on HIGH and CRITICAL actions: financial operations, deletions, external communications. Those are exactly the actions that also tend to involve sensitive credentials. The same step that requires human review also tends to be the step where a credential is most exposed if the reviewer never shows up. 1

With kubbi, the producer can revoke the claim URL before it's used. If the HITL step times out or is abandoned, the producing workflow step revokes the kubbi. The payload expires immediately regardless of TTL. When the workflow is retried or resumed, the producer issues a new kubbi with a new TTL. The credential is never stranded.

This is the property that shared-database-row handoffs can't provide. A database row doesn't know to expire itself when the human reviewer closes the browser tab. You have to write that logic, and that logic has to run even when your orchestrator doesn't. Revocable, expiring claim URLs are the correct primitive for this pattern. You don't build the revocation logic. It's already there.

Where to go next

Start with the noisiest handoff in your current pipeline. Not the riskiest in theory: the one where you already know credentials are showing up in places they shouldn't. Replace that single handoff with kubbi.create() at the producer and kubbi.claim() at the consumer. Confirm the credential is absent from your state store and your logs. Then work backward through the rest of your handoff map.

If you're on LangGraph, you can get started free at kubbi.ai. The integration is two function calls. It fits into your existing node structure without changes to your graph definition.

Footnotes

  1. https://cheatsheetseries.owasp.org/cheatsheets/AI_Agent_Security_Cheat_Sheet.html — OWASP AI Agent Security Cheat Sheet with Python implementations for secure memory, inter-agent communication, and permission management. 2 3 4

  2. https://www.ibm.com/think/tutorials/ai-agent-security — IBM BeeAI framework tutorial on AI agent security, including persistent memory accumulation of sensitive data.