Skip to main content
Shadow credentials are fake API keys that FTL generates and injects into the sandbox. The agent writes code using these fake values, and your real keys never enter the container.

How it works

When you run ftl code, FTL loads credentials from your .env file and generates shadow values before booting the sandbox:
1

Load real credentials

FTL reads all key-value pairs from .env in your project root:
You can specify additional environment variable names in .ftlconfig under "shadow_env". FTL will pull these from os.environ on the host.
2

Generate shadow values

For each real credential, FTL generates a recognizable fake value using secrets.token_hex(8):
The prefix is ftl_shadow_, followed by the lowercased variable name and a 16-character hex token.Example:
3

Build swap table

FTL creates two data structures:
  • shadow_env: {VAR_NAME: shadow_value} — injected into the container as environment variables
  • swap_table: {shadow_value: real_value} — used by the network proxy to swap credentials at runtime
4

Inject into container

Shadow credentials are written to a file inside the container (/tmp/.ftl_env) and sourced before agent commands:
When the agent or its generated code runs os.getenv("STRIPE_SECRET_KEY"), it receives the shadow value.

Network proxy

By default, the agent sees only shadow values. If it makes an API call using ftl_shadow_stripe_secret_key_..., the request will fail because Stripe doesn’t recognize that key. To enable live API calls, install the network proxy:
The proxy intercepts all outbound HTTP/HTTPS traffic from the container and swaps shadow values back to real keys before they reach the upstream server:
Flow:
1

Agent code makes request

2

Request goes through proxy

The container routes all traffic through http://host.docker.internal:<port> (configured via HTTP_PROXY env var).
3

Proxy swaps credentials

4

Upstream receives real key

The API call succeeds. The agent never learns the real key — it only sees the response.

HTTPS and MITM

For HTTPS traffic, the proxy performs a man-in-the-middle attack using an ephemeral CA:
The CA is installed in the container’s trust store before the agent runs:
The CA key is never written to disk. It’s generated in memory at proxy startup and discarded when the proxy stops.

Proxy exclusions

The agent’s own API calls (e.g., Claude Code calling api.anthropic.com) bypass the proxy to avoid SSL verification issues:
These domains are added to NO_PROXY so traffic goes direct.

Lint detection

Before showing the diff, FTL scans for hardcoded shadow values:
This catches cases where the agent writes:
The agent should always use environment variables:
Shadow credentials are not a substitute for read-only or test API keys. They prevent accidental leaks, but if the proxy is enabled, the agent can still make live API calls using your real keys.

AWS Secrets Manager integration

When secrets_manager_prefix is set in .ftlconfig, FTL fetches credentials from AWS Secrets Manager instead of reading .env:
Secrets with JSON object values are expanded into individual keys:
Plain-string secrets use the last path segment as the key name, uppercased:

Configuration reference

If secrets_manager_prefix is set, .env is ignored. All secrets must come from AWS.

Security considerations

  • Shadow values are logged: FTL prints the count of shadow credentials but never logs the values themselves
  • Proxy CA is ephemeral: The CA key is generated in memory and never written to disk. It’s valid for 24 hours and discarded when the proxy stops
  • Swap table lives in memory: The {shadow: real} mapping is stored in the proxy process, not in the container or on disk
  • Container sees only shadows: Even if you docker exec into a running container, you’ll only find shadow values in /tmp/.ftl_env
If you enable CloudWatch or Langfuse tracing, ensure your logging policies redact credentials. FTL does not send shadow values to external services, but agent output may contain them.