Skip to main content
nono provides two ways to keep credentials out of the sandboxed process:
  • Proxy injection (--credential) - The agent talks to a local reverse proxy that injects real API keys on the fly. The credential never enters the sandbox, not even as an environment variable. This is the recommended approach for LLM API keys.
  • Environment variable injection (--env-credential) - Loads secrets from the system keystore and injects them as environment variables before the sandbox is applied. Simpler, but the secret is visible in the process environment.

The proxy acts as a reverse proxy for configured credential routes. The agent sends plain HTTP to localhost:<port>/<service>/... and the proxy:
  1. Strips the service prefix
  2. Injects the real credential as an HTTP header
  3. Forwards to the upstream over TLS
  4. Streams the response back
The agent never sees the API key. Even if the agent is compromised, it cannot extract credentials from its own environment or memory. Streaming responses (SSE for chat completions, MCP Streamable HTTP, A2A JSON-RPC) are forwarded without buffering.

Quick Start

The proxy sets OPENAI_BASE_URL=http://127.0.0.1:<port>/openai in the child’s environment. Most LLM SDKs respect this variable and redirect API calls through the proxy automatically.

Storing Credentials

Built-in credential routes load secrets from the source defined in network-policy.json. If a built-in route omits credential_key, the keyring username defaults to the service name under the keyring service nono.

macOS

Linux

The keyring crate uses service, username, and target attributes. You must use these exact attribute names:
Important: Use username (not account) as the attribute name. The target default attribute is required for the keyring crate to find the entry. Built-in Anthropic, GitHub, and GitLab routes read from host environment variables instead of the keyring.

1Password Integration

nono supports 1Password op:// URIs as a credential source anywhere you would use a keyring account name. For CLI env injection, use --env-credential-map <op://...> <ENV_VAR>. This also works in profile-based credentials (env_credentials, custom_credentials). The 1Password CLI (op) must be installed and authenticated.

Finding Your Secret Path

op:// URIs have the format op://<vault>/<item>/<field>. Use the op CLI to discover each segment:
The field name depends on the item type — “Password” items have a password field, “API Credential” items typically have credential, and custom items use whatever field labels you set. Run op item get to see all available fields for a given item.

CLI: Direct Environment Variable Injection

Pass an op:// URI to --env-credential-map with an explicit destination env var:
Legacy syntax is still accepted for compatibility: --env-credential 'op://vault/item/field=MY_VAR'.

Profile: Environment Credential Injection

In a profile’s env_credentials section, use an op:// URI as the key instead of a keyring account name:

Profile: Proxy Credential Injection

For network API keys, proxy injection is recommended — the child process never sees the real secret. Use an op:// URI in credential_key:

Mixed Mode: Environment + Proxy Credentials

You can combine both injection modes in a single profile. Use env_credentials for non-network secrets (database passwords, tokens) and custom_credentials with proxy injection for API keys:

Apple Passwords Integration (macOS)

nono supports Apple Passwords entries via apple-password:// URIs in both --env-credential-map and profile credential fields (env_credentials, custom_credentials.credential_key). URI format:
  • server: website/service hostname (for example, github.com)
  • account: account/username for that entry (for example, alice@example.com)
nono resolves this URI using macOS security find-internet-password -s <server> -a <account> -w.

CLI: Direct Environment Variable Injection

For Apple Passwords CLI env injection, use --env-credential-map so the credential reference and destination variable are unambiguous.

Profile: Environment Credential Injection

Profile: Proxy Credential Injection


Bitwarden Integration

nono supports Bitwarden entries via bw:// URIs in both --env-credential-map and profile credential fields (env_credentials, custom_credentials.credential_key). The Bitwarden CLI (bw) must be installed and authenticated. URI format:
For proxy credentials, set env_var explicitly when credential_key uses bw://:

Custom Keyring Service

Use keyring://<service>/<account> when a credential lives in the system keyring under a service name other than nono:

Environment Variables

When credential routes are configured, the proxy sets SDK-specific base URL environment variables: Most LLM SDKs (OpenAI Python, Anthropic Python, etc.) respect these variables and redirect API calls through the proxy automatically.

Credential Route Configuration

The built-in network-policy.json defines default credential routes: All services in this table can be used directly with --credential <service> (e.g. --credential github, --credential anthropic) without any custom credential definition in your profile. Note: OpenAI’s upstream includes /v1 because the OpenAI SDK expects the base URL to include the version prefix. Anthropic’s SDK adds /v1/messages automatically, so its upstream is the root URL.

Using Credentials in Profiles

User profiles can specify which credential services to enable in the network section:

Custom Credential Definitions

For APIs not covered by the built-in services, you can define custom credentials in your profile. This lets you use --credential with any API while keeping credentials out of the sandbox.
Important: Use underscores, not hyphens, in credential names (the keys in custom_credentials). The credential name is used to generate environment variables like TELEGRAM_BASE_URL. Shell variable names cannot contain hyphens, so my-api would create MY-API_BASE_URL which cannot be referenced as $MY-API_BASE_URL in shell scripts. Use my_api instead.

CLI Command Capture (cmd://)

For credentials produced by a host CLI, use cmd://<name> and define a matching top-level credential_capture entry:
The proxy captures lazily: the first matching request pauses while the supervisor runs the command, captures stdout, caches it in memory for cache_ttl_secs, injects it into the upstream request, and records an audit event. Empty stdout is rejected after trailing newlines are trimmed. The sandboxed process receives only the phantom token, never the captured credential, and audit records never include the credential value. Capture commands receive request context in environment variables: The command array is resolved at capture time: bare $VAR tokens in any element are expanded from the nono process environment (i.e. the environment of the launcher, not the sandboxed child). Unset variables are left as-is. cache_path_regex is evaluated against the request path. If it matches, capture group 1 becomes the cache scope; otherwise the full match is used. This lets one capture entry cache separately for different repository or organization owners without re-running the auth command for every endpoint path. For commands that prefer request metadata on stdin, set stdin: "request_json":
The command receives JSON containing session_id, credential_name, route_id, request_host, request_path, request_method, and cache_scope. For auth helpers that must inject more than one header, use JSON output with an explicit header allowlist:
The helper prints:
The proxy rejects unlisted headers, hop-by-hop headers, invalid header names, non-string values, and values containing CR or LF. Interactive capture is explicit. Set interaction.stdio: true when the command needs to write prompts to the terminal via inherited stderr. To also read from the terminal (for helpers that genuinely need to prompt the user for input), additionally set interaction.stdin: true. Stdin defaults to /dev/null so that helpers which only need stderr do not accidentally inherit the terminal’s stdin. Browser auth is also scoped to the individual capture command:
When interaction.open_urls is present, nono gives that capture command a temporary browser helper. Opens routed through BROWSER or the macOS open shim are validated against the capture entry, independent of the child sandbox’s top-level open_urls. Non-URL open fallback through the shim is available only when allow_launch_services is true.

Injection Modes

Custom credentials support multiple injection patterns to accommodate different API authentication schemes:
Header Mode (default)
Injects the credential as an HTTP header with optional formatting. This is the most common authentication pattern.
URL Path Mode
Replaces a phantom token in the URL path with the real credential. Useful for APIs like Telegram Bot API that embed authentication tokens in the path (e.g., /bot{token}/method).
The agent sends requests with a phantom token:
The proxy validates the phantom token matches the session token, then replaces it with the real credential:
Query Parameter Mode
Adds or replaces a query parameter with the credential value. Common for APIs that use URL query parameters for authentication (e.g., Google Maps API).
The agent sends requests with a phantom token in the query parameter:
The proxy validates the phantom token, then replaces it with the real credential:
The credential value is URL-encoded automatically.
Basic Auth Mode
Injects a Base64-encoded Basic Authentication header. The credential value should be stored in username:password format in the keystore.
Store the credential in username:password format:
The proxy automatically Base64-encodes the credential and injects it as Authorization: Basic <encoded>.

Proxy Overrides

By default, the same inject_mode and related fields control both how the proxy validates the incoming phantom token from the sandboxed client and how it injects the real credential into the outbound upstream request. For most APIs these are identical, but some clients require a different shape on the proxy side. One such case is kubectl: it does not send a Bearer token to a non-HTTPS proxy, so the proxy needs to accept the phantom token as a query parameter while still injecting the real credential as a header upstream. The optional proxy block lets you override any of the inbound-parsing settings independently:
Fields in the proxy block: Any field omitted from the proxy block falls back to the corresponding top-level value. Outbound upstream credential injection always uses the top-level fields.

Phantom Token Validation

For url_path and query_param modes, the agent must include the session token (NONO_PROXY_TOKEN) as a placeholder in the request. The proxy validates this phantom token before replacing it with the real credential. Invalid or missing phantom tokens result in HTTP 401 Unauthorized responses. Store the credential in the system keystore:
Then run with the custom credential:
Custom credentials can also override built-in services. For example, to route OpenAI requests through a custom proxy:

Security Validation

Custom credentials are validated at startup:
  • Upstream URL must be HTTPS (HTTP is only allowed for localhost, 127.0.0.1, or ::1)
  • Credential key must be alphanumeric (letters, numbers, and underscores only) unless it uses a supported URI scheme such as op://, bw://, apple-password://, keyring://, file://, or env://
Invalid configurations will fail with a clear error message before the sandbox is applied.

AWS SigV4 Signing

Some upstreams authenticate with AWS Signature Version 4 rather than a static bearer token. Amazon Bedrock is the common case. Setting aws_auth on a custom credential turns the route into a signing route: the proxy resolves your AWS credentials on the host, strips whatever authentication the agent sent, and re-signs each outbound request with SigV4 before it reaches AWS. The agent never holds a usable AWS credential. Add an aws_auth block to the route and reference the route name in credentials:
An empty aws_auth: {} is the usual starting point. The proxy resolves credentials through the standard AWS credential chain and infers the signing region and service from the upstream host. All three fields are optional overrides: aws_auth cannot be combined with credential_key or auth on the same route. A route signs with SigV4, injects a keystore secret, or runs the OAuth2 exchange, and only one of those applies at a time.
Credential resolution
Credentials resolve on the host, in the supervisor process, before the sandbox is applied. The same sources the AWS SDK and CLI already use keep working: AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY in the launching environment, static or SSO profiles in ~/.aws, and container or instance metadata. SSO profiles work without extra setup, so aws sso login followed by a profile reference is enough. Because signing happens host-side, you normally want to keep real AWS credentials out of the sandbox. Deny the AWS environment variables and hand the agent placeholders so an SDK inside the sandbox still initialises but cannot sign anything itself:
Region-pinned endpoints
Bedrock and most AWS services expose region-specific hostnames, and the SigV4 signature is scoped to one region. The upstream host therefore pins the route to a single region. To reach a second region, define a second route with its own upstream (for example bedrock-runtime.eu-west-1.amazonaws.com). When the region cannot be read from the host, set region explicitly so the signature scope stays correct.
Running it
Signing routes run like any other custom credential. Reference the route through the profile’s credentials list as shown above, or pass it on the command line:
To confirm signing end to end, watch the proxy log. Signed requests report the resolved service and region and the injected SigV4 headers:
A 403 on the signed request usually means the resolved credentials lack permission for the target model or service, or the region scope does not match the endpoint.

Session Token Authentication

Reverse proxy requests are authenticated using the session token. The proxy generates a unique 256-bit token per session and passes it to the child via the NONO_PROXY_TOKEN environment variable. For credential routes, the sandboxed client must present that token in the configured inbound credential location: the configured header, URL path placeholder, or query parameter. With the default header mode, this is the credential header itself, such as Authorization for OpenAI or x-api-key for Anthropic. Requests without a valid phantom token are rejected with 401 Unauthorized. For no-credential reverse proxy routes, authentication uses proxy auth and invalid or missing credentials are rejected with 407 Proxy Authentication Required. This prevents other localhost processes from accessing the credential injection routes.

Endpoint Filtering

Credential routes can be restricted to specific HTTP method+path combinations using --allow-endpoint or endpoint_rules in custom credential definitions. See Networking — Endpoint Filtering for full documentation including pattern syntax.

WSL2 Limitations

On WSL2, proxy-based credential injection (--credential) is blocked by default. The proxy itself works, but the network lockdown that prevents the child from bypassing the proxy cannot be kernel-enforced — WSL2’s seccomp notify conflict (microsoft/WSL#9548) blocks the fallback, and Landlock V4 (kernel 6.7+) is not yet available. Environment variable injection (--env-credential) works normally on WSL2 — it does not depend on the proxy. To opt in to proxy mode without network enforcement, set wsl2_proxy_policy: "insecure_proxy" in your profile’s security config. See Credential Proxy on WSL2 for details.

Go CLI Tools (macOS)

Go CLI tools (gh, terraform) on macOS ignore SSL_CERT_FILE and verify TLS exclusively via the system trust store (com.apple.trustd). By default, they reject nono’s proxy-minted certificates with x509: certificate is not trusted. To fix this, add --trust-proxy-ca which persists the proxy CA in macOS Keychain and adds it to the user trust store:
The first run triggers a single biometric/password prompt. Subsequent runs reuse the existing CA with zero prompts. The CA regenerates automatically when it expires (default: 1 day, configurable via --proxy-ca-validity). See --trust-proxy-ca for details.

Security Properties

  • Credentials never enter the sandbox - The agent process has no access to API keys, even through environment variables or memory
  • Session token isolation - Credential reverse proxy routes validate the phantom token in the configured header, path, or query parameter; no-credential reverse routes and CONNECT tunnels use proxy auth
  • Managed secret sources - Credentials can be loaded from the OS keyring, 1Password, Bitwarden, Apple Passwords, explicit files, or host environment variables
  • Zeroized in memory - Credential values are stored in Zeroizing<String> and wiped from memory on drop
  • Session-scoped - Credentials are loaded once at proxy startup and never written to disk or logged
  • Header stripping - Credential routes strip the configured credential header before injecting the real credential, along with hop-by-hop headers. No-credential reverse routes pass application headers such as Authorization through.

Audit Logging

Reverse proxy requests are logged with the service name and status code, but credential values are never logged:

Environment Variable Injection

For credentials that don’t need proxy-based protection (e.g., database URLs, custom tokens), you can load secrets from the system keystore and inject them as environment variables.

Quick Start

The secret is loaded from the keystore and injected as $OPENAI_API_KEY (uppercased account name).

How It Works

This ensures the sandboxed process cannot access the keystore directly — it only receives the specific secrets you authorized.

Storing Secrets

All nono secrets are stored under the service name nono in the system keystore.

macOS Keychain

You can also use the Keychain Access application:
  1. Open Keychain Access (search in Spotlight or find in /Applications/Utilities/)
  2. Select the login keychain in the sidebar
  3. Click File > New Password Item (or press Cmd+N)
  4. Fill in: Keychain Item Name: nono, Account Name: openai_api_key, Password: your API key
  5. Click Add
When nono accesses a secret for the first time, macOS will prompt you to allow access. Click Always Allow to avoid repeated prompts.
macOS Keychain Access prompt showing Always Allow option

Linux Secret Service

Linux uses the Secret Service API, typically provided by GNOME Keyring or KWallet. You need secret-tool (part of libsecret-tools) and a running keyring daemon. Installation:
Usage:
Important: The target default attribute is required for the keyring crate to find the entry.

Using env-credential

CLI Flag

Specify comma-separated account names to load:
The environment variable name is auto-generated by uppercasing the account name:

Profile-Based Secrets

Profiles can declare which credentials to load in the env_credentials section (the previous name secrets is still accepted):
Then use the profile directly:
The env_credentials section maps keystore account names to environment variable names, giving you full control over naming.

Error Handling

Secret not found:
Store the secret first using the platform-specific commands above. Keystore locked:
Unlock your keystore (typically by entering your login password) and press Enter. Multiple entries:
Delete the duplicate entries using your OS keystore manager.

Headless Linux Environments

Secret Service (GNOME Keyring) can be problematic on headless servers and SSH sessions because it requires D-Bus and a graphical login. pass uses GPG encryption and works well in headless environments.
Wrapper script:

Option 2: Environment Variables via Wrapper

For simple setups, export secrets from a protected file:
Wrapper script:
File-based secrets are less secure than a proper keystore. Ensure the file has strict permissions (chmod 600) and is not backed up to insecure locations.

Option 3: Set Up Headless Keyring

If you prefer to use Secret Service in headless mode:
Add to ~/.bashrc or ~/.zshrc for SSH sessions:

Security Considerations

What nono protects:
  • Keystore file access - Sandbox blocks direct access to ~/Library/Keychains (macOS) and keyring files
  • Memory exposure - Secrets wrapped in Zeroizing<String> and cleared after use
  • Environment variable filtering - Injected credentials bypass the environment.allow_vars allow-list, so they always reach the child process even when other variables are filtered. See Environment Variable Filtering.
Limitations:
  • Environment variable visibility - On Linux, /proc/PID/environ is readable by same-user processes. For maximum protection, use proxy injection instead.
  • Malicious use of credentials - nono cannot prevent a sandboxed process from misusing legitimately obtained credentials
Best practices:
  1. Use unique account names (e.g., myapp_openai_key rather than api_key)
  2. Rotate secrets regularly
  3. Only grant secrets that are actually needed
  4. Prefer proxy injection for LLM API keys

Next Steps