How to Give an AI Agent Code Execution Without Handing Over Your Credentials
Agents need API keys to be useful — but the moment a key lives inside the sandbox, one prompt injection can exfiltrate it. Here's how to give an agent a credential it can use but never read.
Most useful agents need a secret. They call an LLM API, query your database, hit an internal service — and each of those needs a key, a token, or a password. The default way to provide one is to set it as an environment variable inside the sandbox where the agent runs.
That's also the problem. The sandbox is exactly where untrusted, model-driven code executes. If the agent is prompt-injected, or a dependency is compromised, anything inside that sandbox can read its own environment, dump /proc, or list the process table — and your key walks out the door. You've handed the keys to the one component you don't trust. (We wrote about how this actually plays out in Why Sandboxes Alone Won't Secure Your AI Agents.)
The fix isn't a better secret-hiding trick inside the box. It's to keep the secret out of the box entirely, and attach it on the way out.
The idea: inject at the egress proxy, never in the VM
Every sandbox's outbound traffic already passes through a security proxy that sits between the VM and the internet. A credential vault uses that chokepoint:
- You store the secret server-side. It's written to the vault backend and is never returned by any API again.
- The sandbox is created with a reference to that secret — just its name. Inside the VM, the environment variable holds only the placeholder string
declaw:vault-managed. - When the agent makes an outbound request to a host you've scoped the secret to, the proxy attaches the real credential to that request. Requests to any other host get nothing.
The agent can use the credential against the destination you intend, but it can never read it. A compromised agent that exfiltrates its whole environment leaks a placeholder.
Step by step
1. Install the SDK
pip install declaw
The same flow exists in the TypeScript (@declaw/sdk), Go (declaw-go), and CLI (declaw) clients — substitute as needed.
2. Store the secret
The value is write-only — it goes into the vault and is never returned by create, list, or get. To keep this walkthrough reproducible, we'll scope a demo token to a public echo endpoint so you can see exactly what arrives upstream. domain_regex uses the vault's ~ prefix to mark a case-insensitive regex — always anchor it with ^…$ so postman-echo.com can't also match an attacker-suffixed host like postman-echo.com.evil.com:
from declaw import VaultClient, VaultScope
vault = VaultClient() # uses DECLAW_API_KEY
vault.create_secret(
name="demo-token",
value="super-secret-value", # never returned again
scopes=[
VaultScope(
domain_regex=r"~^postman-echo\.com$",
injection_type="bearer", # -> Authorization: Bearer <value>
),
],
)
For a known provider, skip the hand-written scope entirely — name a built-in preset and the destination and injection format are filled in for you:
vault.create_secret(name="openai", value="sk-...", provider="openai")
3. Create the sandbox with a reference, not the value
Pass a map of environment variable name → secret name. Add the destination host to the network allowlist so its traffic actually flows through the proxy:
from declaw import Sandbox
sbx = Sandbox.create(
template="base",
network={"allow_out": ["postman-echo.com"]},
vault_refs={"DEMO_TOKEN": "demo-token"},
)
4. Run the agent — and confirm it can't see the key
Inside the VM, the environment variable is just a placeholder:
$ printenv DEMO_TOKEN
declaw:vault-managed
But an outbound request to the scoped host carries the real credential, attached by the proxy after it leaves the VM:
$ curl -s https://postman-echo.com/get
# reflected request headers include:
# "authorization": "Bearer super-secret-value"
The credential is attached only to hosts that match the secret's scope. Broaden the agent's network access however you like — the value is still only added on postman-echo.com, and every other destination sees nothing.
5. Rotate or revoke without redeploying
list returns metadata only (never the value). rotate swaps the stored value while keeping the scope; running sandboxes pick it up on their next outbound request via a short-lived cache — no restart. delete removes the metadata and the value.
for s in vault.list_secrets():
print(s.secret_id, s.name, s.rotation_due)
vault.rotate_secret("demo-token", "a-new-secret-value")
vault.delete_secret("demo-token")
What to check
If the value stays hidden but the request shows up with no credential, it's almost always one of the first two rows below — injection only fires when the host both matches a scope and flows through the proxy.
| Check | Why it matters | How to verify |
|---|---|---|
| Secret created, value not echoed back | The value is write-only | create returns an id, never the value |
Destination host is in allow_out |
Injection happens at the L7 proxy; a host outside the policy bypasses it | Add the host to the sandbox's network.allow_out |
Custom domain_regex uses ~ and is anchored |
Without ~ it's an exact-string match and never fires; without ^…$ it can over-match |
~^api\.example\.com$ |
| Env var inside the VM shows the placeholder | Confirms the value never entered the sandbox | printenv → declaw:vault-managed |
| Scoped host's request carries the credential | Confirms injection fired | Echo endpoint reflects the header |
| Other hosts carry nothing | Confirms scoping | Allow a second host in allow_out; its requests have no credential |
Limitations to know
Keeping this honest — the vault is powerful but not magic:
- Certificate-pinned upstreams can't be injected. Injection rides the proxy's TLS interception. An upstream that pins certificates and rejects the proxy's CA won't accept the intercepting proxy, so there's nothing to inject into.
- Socket-broker wire encryption. The agent↔proxy leg is always cleartext inside the sandbox's own network namespace (where the proxy is the only gateway). The proxy↔database leg can opt into TLS — STARTTLS for SMTP, connect-time for Redis and MongoDB, in-protocol for Postgres and MySQL — so a database that mandates TLS on the wire is brokered too.
Next step
Store one real key, create a sandbox with a vault_ref, and run printenv inside it — seeing the placeholder where your key used to be is the whole pitch in five lines. Grab the SDK at declaw.ai, and for the bigger picture of what else crosses the sandbox boundary, read Why Sandboxes Alone Won't Secure Your AI Agents.