> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nanoclaw.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Credentials

> How NanoClaw keeps raw API keys out of agent containers — the OneCLI Agent Vault, credential stubs, approval cards, and the .env opt-out.

NanoClaw's credential invariant: **agent containers never hold raw API keys**. Secrets live in the OneCLI Agent Vault on the host, and the vault's gateway injects them into outbound HTTPS requests in flight. An agent that is prompt-injected, jailbroken, or just curious can dump its entire environment and filesystem and find nothing worth stealing.

## How the vault works

Every container is launched with `HTTPS_PROXY` pointing at the vault's gateway. When the agent calls a real API URL — `api.anthropic.com`, `api.github.com`, `gmail.googleapis.com` — the gateway intercepts the request, matches the host against its stored secrets, and rewrites the auth header with the real token before the request leaves for the internet. Standard HTTP clients (curl, fetch, requests, axios, git) honor `HTTPS_PROXY` automatically, so the agent just makes normal requests with no auth headers at all.

If the target service isn't connected yet, the gateway returns an error with a `connect_url` the agent surfaces to you, instead of a silent failure.

NanoClaw talks to the vault through two values in `.env`: `ONECLI_URL` (the OneCLI app/API — note that what OneCLI's own docs call the *gateway* is a separate proxy listener that agent egress actually flows through; these pages use "gateway" loosely for the whole vault) and `ONECLI_API_KEY` (the host's own credential for the vault API).

### Credential stubs

Some tools insist on finding a credential locally before they'll start — MCP servers especially. For these, the container gets **stub files** whose secret values are the placeholder string `"onecli-managed"`. The Gmail tool is the worked example: the Gmail MCP server reads `~/.gmail-mcp/credentials.json` containing `"access_token": "onecli-managed"`, happily starts, and sends `Authorization: Bearer onecli-managed` — which the gateway rewrites to the real OAuth token on its way to `gmail.googleapis.com`. Files containing `onecli-managed` are managed by OneCLI; agents are instructed never to modify or delete them.

## Setting it up

The [setup wizard](/installation) handles the vault as one of its steps, with three paths:

* **Fresh install (default)** — runs the OneCLI installer (a Docker Compose stack plus the `onecli` CLI), points the CLI at the new instance, and writes `ONECLI_URL` to `.env`.
* **Reuse existing** — if setup detects a working `onecli` on the host, it asks before reinstalling. Choosing reuse leaves the running gateway untouched (re-running the installer would rebind the listener and break other apps using it) and just records its URL.
* **Remote vault** — set `NANOCLAW_ONECLI_API_HOST` before running setup to point at a OneCLI instance on another machine. Setup installs only the CLI, health-checks the remote URL, and writes `ONECLI_URL`. Pass the remote vault's API token via `NANOCLAW_ONECLI_API_TOKEN` so setup can authenticate the CLI and write `ONECLI_API_KEY`.

Setup installs the gateway and `onecli` CLI at the versions pinned in `versions.json` (`onecli-gateway`, `onecli-cli`), never at `latest`. It also probes the gateway's `/v1/health`: `@onecli-sh/sdk` 2.x talks to the gateway's `/v1` API, and a gateway that predates it answers `404` to every vault call — failures that look transient but are permanent. Setup never upgrades the gateway for you; it warns and points to the verbatim migration runbook, [`docs/onecli-upgrades.md`](https://github.com/nanocoai/nanoclaw/blob/main/docs/onecli-upgrades.md). The gateway pin also moves through `/update-nanoclaw` — see [Upgrading](/operate/upgrading).

On an existing install (for example after `/update-nanoclaw` brings in OneCLI as a breaking change), run the `/init-onecli` skill instead. It installs the vault, migrates any credentials it finds in `.env` (Anthropic keys, `OPENAI_API_KEY`, and similar container-facing secrets) into the vault, and removes the raw values from `.env`. Channel tokens like `TELEGRAM_BOT_TOKEN` stay in `.env` — the host process uses those, not the containers.

## Anthropic credentials

The wizard's auth step offers three ways to connect Claude, all of which end up as a vault secret rather than an env var:

<Tabs>
  <Tab title="Claude subscription (Pro/Max)">
    Setup runs `claude setup-token` under a PTY so the browser OAuth flow works, captures the resulting `sk-ant-oat…` token, and registers it:

    ```bash theme={null}
    onecli secrets create \
      --name Anthropic \
      --type anthropic \
      --value "$token" \
      --host-pattern api.anthropic.com
    ```
  </Tab>

  <Tab title="API key or existing OAuth token">
    Paste a `sk-ant-api…` key (from console.anthropic.com) or a `sk-ant-oat…` token you already have. Setup validates the shape and runs the same `onecli secrets create --type anthropic --host-pattern api.anthropic.com` registration.
  </Tab>

  <Tab title="Custom endpoint">
    Set `NANOCLAW_ANTHROPIC_BASE_URL` and `NANOCLAW_ANTHROPIC_AUTH_TOKEN` before setup to use an Anthropic-compatible endpoint. The token is stored as a generic secret with header injection (`--header-name Authorization --value-format 'Bearer {value}'`) keyed to the custom hostname. The container only sees `ANTHROPIC_BASE_URL` plus `ANTHROPIC_AUTH_TOKEN=placeholder` — the gateway overwrites the placeholder bearer on the wire.
  </Tab>
</Tabs>

## Adding a credential for a new service

Register a secret with a name and a **host pattern** — the host the gateway should inject it for:

```bash theme={null}
onecli secrets create \
  --name GitHub \
  --type api_key \
  --value "$TOKEN" \
  --host-pattern api.github.com
```

Then grant it to the agent groups that should use it. Note that `set-secrets` **replaces** the agent's whole list, so read and merge first:

```bash theme={null}
AGENT_ID=$(onecli agents list | jq -r '.data[] | select(.identifier=="<agentGroupId>") | .id')
CURRENT=$(onecli agents secrets --id "$AGENT_ID" | jq -r '[.data[]] | join(",")')
MERGED=$(printf '%s' "$CURRENT,<new-secret-id>" | tr ',' '\n' | sort -u | paste -sd ',' -)
onecli agents set-secrets --id "$AGENT_ID" --secret-ids "$MERGED"
```

`<agentGroupId>` is the `agentGroupId` field in the group's `container.json`. The same model powers `setup/register-claude-token.sh`, which accepts `SECRET_NAME` and `HOST_PATTERN` overrides if you want the subscription flow against a different secret name or host.

## Accessing the OneCLI dashboard

The vault ships a web dashboard at the same address as `ONECLI_URL` — this is the "OneCLI web UI" the OAuth-based [tool skills](/extend/tools) refer to. Open it in a browser to connect OAuth services (Gmail, Calendar), browse secrets, and watch gateway activity.

Two gotchas on a headless install:

* The dashboard binds where OneCLI binds. On installs where containers reach the gateway over the Docker bridge, that address isn't routable from your laptop — reach it through an SSH tunnel or a private network such as a tailnet rather than exposing the port publicly.
* OAuth **Connect** flows finish by redirecting the browser back into the dashboard at OneCLI's configured `APP_URL`, which defaults to `http://localhost:10254`. Started from a remote browser, the provider consent itself works (that redirect is derived from the request), but the final hop lands on localhost and dies unless `APP_URL` is set to the URL you actually browse from. The `connect_url` links the gateway hands to agents are built from the same value, so an unset `APP_URL` makes those unreachable too.

Dashboard specifics beyond this are OneCLI's territory — see the [OneCLI docs](https://onecli.sh/docs).

## Approving credential use

Secrets can be configured in OneCLI to require **manual approval** per request. When the gateway intercepts a request that needs one, it holds the HTTP connection open and asks NanoClaw, which delivers a **Credentials Request card** to an admin DM with Approve / Reject buttons. The card always names the agent; when the gateway supplies a structured summary, it shows an **Action** line plus labeled fields describing what the request does. Without one it falls back to the raw shape: a body preview and the `METHOD host/path` line.

* **Who gets the card:** admins of the originating agent group first, then global admins, then owners — the first one with a reachable DM. No eligible approver means auto-deny.
* **Approving or rejecting:** tap the button on the card. The decision is returned to the gateway, which either injects the credential and lets the request through or fails it.
* **Expiry:** unanswered cards expire on the gateway's TTL and the request is denied. Cards left over from a previous process are marked "Expired (host restarted)" at startup.

Inspect in-flight approvals with the [ncl CLI](/operate/ncl-cli): `ncl approvals list --status pending`. The CLI is read-only for approvals — decisions happen on the card.

## Opting out: the native credential proxy

If you don't want the vault — a single-user install where `.env` simplicity beats per-agent policies — the `/use-native-credential-proxy` skill adds an explicit opt-out. With `NANOCLAW_NATIVE_CREDENTIALS=true`, NanoClaw reads `ANTHROPIC_API_KEY`, `CLAUDE_CODE_OAUTH_TOKEN`, or `ANTHROPIC_AUTH_TOKEN` (plus optional `ANTHROPIC_BASE_URL`) straight from `.env` and threads them into the container's environment as standard SDK variables.

<Warning>
  This deliberately inverts the v2 invariant: the raw credential enters the container. You lose injection-at-the-boundary, per-agent secret grants, and approval cards. Use it only if you accept that tradeoff.
</Warning>

The skill is additive — with the flag unset, it's a no-op and the vault path is unchanged. It only covers Anthropic credentials; other service secrets still need the vault or manual handling.

## Egress lockdown

For defense in depth, `NANOCLAW_EGRESS_LOCKDOWN=true` places agents on an internal Docker network where the vault's gateway is the only reachable egress hop — agents physically cannot bypass the proxy. See [Hardening](/operate/hardening) for the full setup.
