Skip to main content
Some coding agents do not use static API keys. They run an OAuth login flow, receive access and refresh tokens, and store those tokens locally for later API calls. That creates a difficult sandboxing problem:
  • The agent must be allowed to run /login, auth login, or a device-code flow.
  • The real OAuth token response must not become readable by the sandboxed agent.
  • Later API calls still need to authenticate successfully.
Sandboxed OAuth logins solve this by letting the login happen inside the sandbox while rewriting real token material before it reaches the agent.
The agent stores and reuses phantom tokens. When it later calls the provider’s API through the nono proxy, the proxy resolves the phantom back to the real token only for the declared API route.

When to use this

Use sandboxed OAuth logins when all of these are true:
  • The tool has an interactive OAuth login flow.
  • The tool needs to persist a session for future runs.
  • You do not want to grant the sandbox access to raw OAuth tokens, API keys, or host credential stores just so login works.
Examples include coding agents such as Codex and Claude Code when they are logged in with a user account rather than a manually supplied API key. If you already have a static API key and the client can use a configurable base URL, start with Credential Injection instead. It is simpler.

What the sandbox sees

The sandbox sees values shaped like credentials, but they are not credentials:
The real token material is stored outside the sandbox in nono’s OAuth capture store. The sandbox cannot redeem a phantom directly with the provider. A phantom only works when it is sent back through a matching nono proxy route.
A JWT-shaped phantom is still a phantom. It exists for clients that locally parse id_token as a JWT. The payload is synthetic and must not contain provider-specific secrets. Use jwt only for fields the client parses locally and does not later send as a bearer token.

The simplest profile

Start with a profile that allows the tool to run normally and enables network access. Save this as ~/.config/nono/profiles/my-agent-oauth.json. This example shows only the OAuth capture pieces:
Check that nono can load it:
Run the login command through that profile:
Look for these log lines:
If you see those, the token endpoint response was intercepted and rewritten before the sandboxed process received it.

How the pieces fit

credential_providers

A provider describes where login tokens come from and which API origins may use the resulting phantoms.
Provider names are local to the profile. They are not built-in Rust provider names. Packs and local profiles can define their own providers without changing nono.

token_endpoints

Each token endpoint is an HTTPS origin plus an absolute path. Path matching is exact after the query string is removed, so configure every token-bearing path the client may use.
If a provider uses more than one token endpoint, list each one. Device-code flows often poll one endpoint and then exchange at another. For safety, nono also inspects responses from configured capture hosts. If an unmatched path or error response contains common OAuth token fields such as access_token, refresh_token, or id_token, nono fails closed instead of streaming real token material back to the sandbox. This is a backstop, not configuration discovery; use debug logs to add the correct token_endpoints and response_fields.

response_fields

response_fields declares which JSON fields contain real token material.
Use opaque for normal bearer tokens and refresh tokens. Use jwt only when the client locally parses that field as a JWT, usually id_token. Do not mark an access token as jwt if the client will later resend the whole JWT as Authorization: Bearer ...; the synthetic value is not a provider-valid JWT.

request_body

When the client refreshes or exchanges tokens, it may send the phantom back to the token endpoint. nono must resolve that phantom before forwarding upstream. request_body controls how nono parses the outgoing token request: Use auto first unless you know the client requires a specific format.

request_nonce_fields

These are request fields where nono should look for phantoms before forwarding a token refresh or exchange request.
For JSON bodies, dotted paths are supported. For form bodies, use top-level field names.

api_hosts and credential_routes

api_hosts declares where phantoms may be resolved:
credential_routes binds a provider to one or more proxy routes:
Only those routes can resolve the provider’s phantoms. A random process cannot take nono_<64hex> and use it directly against the provider.

Add endpoint policy

Once the basic flow works, restrict what the logged-in agent can call. For example, allow only message creation:
This is the same endpoint policy model used by normal proxy credential routes.

Add client-specific CA variables

OAuth capture uses TLS interception for the configured token hosts. nono writes a temporary trust bundle and sets the common CA environment variables. Some clients need their own CA variable. Add it in the profile:
This does not add a new trust root globally. It only tells the sandboxed child where the per-session nono trust bundle is.

Add credential-store detection

Some tools persist auth state in a known JSON file or credential-store entry. You can describe that store so nono can create needed parent directories and detect whether expected fields contain phantoms. File JSON example:
Keychain JSON example:
Do not grant broad Keychain access just to make OAuth work. The goal is that the agent persists phantoms, not raw tokens. If a client requires a native credential store, verify the stored fields are phantom-shaped before relying on the profile.
The OAuth capture store itself lives outside the sandbox under nono’s state directory. It stores the real token material needed to resolve phantoms, with owner-only permissions. Entries are retained for up to 90 days and capped at 4096 phantoms. Treat this as plaintext-at-rest under your user account: protect the state directory the same way you protect other local credential stores.

Add helper commands

Helpers document how a user should check login state or start login/logout. They are command arrays, not shell strings:
Helpers are optional. They are useful in profiles and packs because a UI or TUI can show the right lifecycle command without hardcoding provider names.

Codex-style device auth

Codex device auth uses OpenAI auth endpoints and may parse id_token locally as a JWT. A profile for that flow needs:
  • both token endpoints
  • JWT-shaped phantom output for id_token
  • form request body parsing
  • Codex’s CA bundle variable
Save this as ~/.config/nono/profiles/codex-oauth.json:
Validate it:
Run device auth:
Expected evidence:

Claude Code-style OAuth

Claude Code’s OAuth flow exchanges tokens through Claude hosts and then calls Anthropic API hosts. A profile for that flow usually needs:
  • token endpoints for claude.ai, claude.com, and platform.claude.com
  • API host access for https://api.anthropic.com
  • browser opening for the login command
  • client state paths such as ~/.claude
In practice, extend your normal Claude Code profile and add an OAuth capture provider. The provider section looks like this:
Run login with browser-opening enabled:
After login, run normal sessions without browser-opening flags:
Expected evidence:

Verify what was stored

Do not print token values. Check only shape. For a JSON auth file:
For logs, useful proof looks like:
Avoid logs that dump request bodies, response bodies, or full environment variables. Those can contain real credentials from unrelated tools.

Troubleshooting

Token endpoint is not matched

Enable debug logging:
Look for:
Add the shown path to token_endpoints.

The client fails TLS verification

Add the client’s CA environment variable under network.tls_intercept.ca_env_vars. Keep the standard CA variables in place; this setting adds client-specific names.

The client says an ID token is invalid

If the failing field is id_token, change its response field kind from opaque to jwt:
Do not add provider-specific real claims unless the profile schema explicitly supports that. The JWT-shaped value should remain synthetic.

Refresh fails after login

Set request_body to the format the client actually uses:
or:
Then rerun with debug logging and confirm that refresh requests hit the configured token endpoint.

The client tries to read Keychain or another credential store

Do not grant broad credential-store access as the first fix. First verify that the OAuth token response was captured and that the client stored phantoms. If the client truly requires a native credential store for session metadata, grant only the minimum client-specific paths or system services and confirm stored token fields remain phantom-shaped.

Security model

Sandboxed OAuth login is designed around three boundaries:
  1. Real OAuth token responses are rewritten before the sandbox sees them.
  2. Phantoms resolve only through admitted proxy consumers.
  3. Token capture endpoints fail closed when nono cannot safely rewrite the response.
The unmatched-response backstop looks for common OAuth token field names. For providers that return token material under unusual names, configure the exact token endpoint and exhaustive response_fields; do not rely on the backstop as the primary control. That means a successful login should not give the agent a transferable bearer token. The agent can keep working through the nono-mediated route, but it cannot take nono_<64hex> and authenticate directly with the provider.