The question comes up constantly in teams building production AI agents: "We need the agent to call our payment API. How do we give it the credentials without just... putting the API key in the system prompt?"

This guide covers every meaningful approach, what each one actually protects against, and what it doesn't.

Method 1: Credentials in system prompts

How it works: The API key is included directly in the system prompt or injected into the user message that sets up the agent's task.

system_prompt = f"""
You are a billing agent. Use this Stripe key to access invoices:
STRIPE_KEY={stripe_api_key}
...
"""

What it protects: Nothing. This is the worst approach. The credential appears in:

Avoid this entirely.

Method 2: Environment variables in the agent runtime

How it works: Credentials are set as environment variables in the agent's execution environment. The agent code reads them with os.environ.get("STRIPE_API_KEY").

export STRIPE_API_KEY="sk_live_..."
python run_agent.py

What it protects: Credentials don't appear in the prompt or conversation. Better than method 1.

What it doesn't protect: Environment variables are accessible to anything running in the same process. If your agent loads untrusted code, runs subprocesses, or can be induced via prompt injection to call a debug tool that dumps env vars, your credentials are exposed. They also don't expire — a compromised container image or deployment artifact exposes the key indefinitely.

Security rating: Acceptable for development, insufficient for production.

Method 3: Secrets management systems (Vault, AWS Secrets Manager)

How it works: Credentials are stored in a secrets manager. The agent fetches them at runtime via the secrets manager API.

import boto3
client = boto3.client('secretsmanager')
secret = client.get_secret_value(SecretId='stripe-api-key')
stripe_key = secret['SecretString']

What it protects: Credentials are centrally managed, auditable, and rotatable. Access to the secrets manager itself is controlled via IAM. This is significantly better than environment variables.

What it doesn't protect: Once fetched, the credential lives in the agent's runtime memory and can be accessed by anything in that process. The credential is still long-lived — it doesn't expire when the task is done. If the agent is compromised after fetching the credential, the attacker has a valid long-lived key.

Security rating: Good for credential storage and rotation. Doesn't solve the runtime exposure problem.

Method 4: Tool-level credential injection

How it works: The orchestration layer wraps API calls as tools. The agent invokes tools by name ("call_stripe_api"), and the tool implementation resolves credentials internally without exposing them to the agent context.

@tool
def get_stripe_customers(query: str) -> dict:
    # Credentials resolved here, never in agent context
    stripe.api_key = get_secret("stripe-api-key")
    return stripe.Customer.list(email=query)

What it protects: Credentials never appear in the LLM's context window. The agent can't accidentally include them in a response or be induced via prompt injection to output them. This is the right architectural boundary.

What it doesn't protect: The underlying tool implementation still uses a long-lived credential. If the tool implementation code is compromised or the secrets manager is breached, the credential is still valid indefinitely.

Security rating: Good architecture. Still needs short-lived credentials at the implementation layer.

Method 5: Just-in-time nonces (the right approach)

How it works: The orchestrator requests a scoped, time-limited token immediately before the agent needs it. The token is used and then expires.

# In orchestration layer, before task execution
nonce = await nonce_client.create(
    service="stripe",
    scope="read:customers",
    ttl="5m",
    agent_id="billing-agent-v2",
    task_id=current_task_id
)

# Pass nonce to the specific tool call, not the full agent context
result = await stripe_tool.call(
    query=customer_query,
    auth_token=nonce.token
)
# Nonce expires automatically after 5 minutes

What it protects:

What it doesn't protect: An attacker who compromises the vault that issues nonces has access to your real credentials. The vault itself becomes the security boundary and must be hardened accordingly.

Security rating: The right model. Combines the architectural boundary from method 4 with time-limited credentials.

Combining methods: the practical architecture

In production, the best architecture combines methods 4 and 5:

  1. Agent tools are implemented as wrappers that don't expose credentials to the LLM context (method 4).
  2. Each tool implementation requests a JIT nonce when called, rather than using a stored long-lived credential (method 5).
  3. Real credentials are stored in a secrets manager accessible only to the nonce issuance service (method 3).

The agent sees: tool names, tool outputs, task results. No credentials at any layer.

The credential flow: task trigger → orchestrator requests nonce → nonce issued with scope and TTL → tool uses nonce → task completes → nonce expires.

One thing to get right now

If you take nothing else from this guide: move your credentials out of system prompts immediately. Even if you don't implement JIT issuance yet, getting credentials out of the LLM context window eliminates the most dangerous exposure vector.

Tool-based credential abstraction (method 4) paired with a secrets manager (method 3) is a defensible intermediate state. JIT nonces are where you want to end up.