# Token and credential paths

Every credential Speedwave stores lives under one data directory, `~/.speedwave/` by default. Set the `SPEEDWAVE_DATA_DIR` environment variable to move it elsewhere: every path below then resolves under your chosen location, and since Speedwave reads the variable once per process, changing it takes a restart. See [environment variables](/docs/reference/env-vars/) for the full list Speedwave reads at startup, and [how credentials are handled](/docs/security/credentials/) for the security rules that keep one service from reading another's files.

## Layout

- ~/.speedwave/
  - tokens/
    - **&lt;project&gt;**/
      - **&lt;service&gt;**/ per-service worker credentials
        - … credential files
      - llm/
        - **&lt;provider_id&gt;**\_api_key LLM provider API key
  - claude-home/
    - **&lt;project&gt;**/
      - .claude/
        - .credentials.json Anthropic OAuth (owned by Claude Code)
  - oauth/
    - **&lt;project&gt;**/
      - **&lt;service&gt;**.json plugin OAuth refresh tokens (off-mount)
  - plugin-state/
    - **&lt;slug&gt;**/
      - bridge-token persisted host-bridge token
  - ide-bridge/
    - **&lt;port&gt;**.lock IDE Bridge UUID token
`<project>` is the per-project namespace. `<service>` is an MCP service id, either a built-in worker or a plugin slug. `<provider_id>` is an LLM provider id from `claude.llm`. `<slug>` is a plugin slug, and `<port>` is the IDE Bridge port.

## Paths at a glance

| Path | Holds | Mounted | Mode |
| --- | --- | --- | --- |
| `tokens/<project>/<service>/<file>` | Credentials for one MCP service | That service's worker only, at `/tokens` (read-only) | file `0o600`, dir `0o700` |
| `tokens/<project>/llm/<provider_id>_api_key` | API key for one LLM provider | `proxy` container only, at `/tokens` (read-only), as env name `SPW_KEY_<PROVIDER_ID>` | file `0o600`, dir `0o700` |
| `claude-home/<project>/.claude/.credentials.json` | Anthropic OAuth credentials | `${CLAUDE_HOME}` at `/home/speedwave` (read-write) | managed by Claude Code |
| `oauth/<project>/<service>.json` | Plugin OAuth refresh tokens and client secrets | Not mounted; a host-side `oauth` worker reads it | file `0o600`, dir `0o700` |
| `plugin-state/<slug>/bridge-token` | Persisted host-bridge auth token | Not mounted | file `0o600` when present |
| `ide-bridge/<port>.lock` | IDE Bridge UUID v4 auth token | `/home/speedwave/.claude/ide/` (read-only) | file `0o600`, dir `0o700` |

## Per-service worker tokens

Each credential-bearing MCP service gets its own directory, `tokens/<project>/<service>/`, holding whatever files that service declares, created only for services whose credential file list is non-empty. [That one directory gets mounted read-only into the matching worker](/docs/under-the-hood/workers/), at `/tokens`, and the `PLUGIN_TOKEN_PATH_MISMATCH` SecurityCheck rule confirms the mounted path actually matches `tokens/<project>/<service>/` before the container starts. A compromised worker exposes only that service's own credentials.

## LLM provider keys

An LLM provider key configured under `claude.llm` lands at `tokens/<project>/llm/<provider_id>_api_key`, named after the provider id plus the suffix `_api_key`. That `llm` directory is mounted read-only into the `proxy` container only, with each key injected as an environment variable named `SPW_KEY_<PROVIDER_ID>`, the provider id uppercased with hyphens turned to underscores:

```text title="SPW_KEY env-name and path examples"
openrouter     -> SPW_KEY_OPENROUTER
my-anthropic   -> SPW_KEY_MY_ANTHROPIC

tokens/proj/llm/openrouter_api_key
```
**Caution:** LLM provider keys never reach the `claude` container: the `PROXY_VOLUMES` SecurityCheck rule pins the `proxy` container's mounts to `config:ro`, `tokens/<project>/llm:/tokens:ro`, and `usage:rw`, with no other volumes and no `network_mode`. The one exception is the Anthropic API key, which stays on the `claude` container's environment because Claude Code forwards it directly.

## Anthropic OAuth credentials

Anthropic OAuth credentials live at `claude-home/<project>/.claude/.credentials.json`, written and refreshed by Claude Code inside the `${CLAUDE_HOME}` bind mount at `/home/speedwave`. Speedwave's part is limited to locating the file for the compose mount and clearing it on `speedwave logout`.

## Plugin OAuth refresh tokens

Plugin OAuth refresh tokens and client secrets never live under `tokens/`; they go off-mount instead, to `oauth/<project>/<service>.json`, which keeps every path under `tokens/` read-only everywhere it is mounted. The filename is the service id, so a built-in OAuth service such as `sharepoint` writes `sharepoint.json`.

## Plugin state and the bridge token

Mutable per-plugin state lives at `plugin-state/<slug>/`, separate from the signed plugin files themselves. One example is the persisted host-bridge auth token, `plugin-state/<slug>/bridge-token`. Desktop's host bridge writes it with mode `0o600`, and only when the plugin manifest opts into `persistent_token`.

## IDE Bridge lock file

[The IDE Bridge lock file](/docs/features/ide-bridge/), `ide-bridge/<port>.lock`, holds a UUID v4 auth token that Desktop regenerates on every start and never persists across restarts. It's written `0o600` inside a `0o700` parent directory and mounted into the container read-only, at `/home/speedwave/.claude/ide/`.

## File permissions

Speedwave sets every credential file to mode `0o600` and its parent directory to `0o700`, owned by your user account. On Windows, the equivalent is a DACL with a single full-control entry for your account, applied when the file or directory is created.

On Unix, Speedwave also checks these modes at startup and auto-fixes incorrect bits; it cannot fix ownership mismatches, which need root. Windows has no equivalent startup check: a DACL is set once at creation time and never re-verified or repaired on later starts.