# Settings schema

> settings.json version 1 fields: inferenceProvider, inferenceRouterUsage, boxRuntime, MCP instruction maps, and atomic persist path.

- Repository: sashimikun/grok-bot-0.18-reconstructed
- GitHub: https://github.com/sashimikun/grok-bot-0.18-reconstructed
- Human docs: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15
- Complete Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/llms-full.txt

## Source Files

- `source/shared/node/settings/sand-settings-store.ts`
- `source/shared/inference-router.ts`
- `source/shared/box-runtime.ts`
- `source/shared/host-settings.ts`
- `source/host/extensions/settings/settings-service.ts`
- `source/shared/local-tool-permission.ts`

---

---
title: "Settings schema"
description: "settings.json version 1 fields: inferenceProvider, inferenceRouterUsage, boxRuntime, MCP instruction maps, and atomic persist path."
---

`SandSettingsStore` owns `settings.json` as a version-`1` `SandStoredSettings` document. `SettingsService` constructs the store at `join(getSandRootDir(), "settings.json")`. Settings → Router writes `inferenceProvider` and `boxRuntime` through `window.desktop.agent.setInferenceRouter` / `setBoxRuntime`; routed turns append `inferenceRouterUsage` via `recordInferenceUsage`. MCP instruction maps live in the same file and are mirrored to the box through `setHostSettings`. `boxRuntime` is store-only: it is not part of `HostSettingsUpdate`.

## Persist path

The file name is always `settings.json` under the resolved Sand data root.

| Resolution order | Condition | Root |
| --- | --- | --- |
| 1 | `SAND_DATA_ROOT` is a non-empty absolute path | that directory |
| 2 | `--user-data-dir` or `SAND_USER_DATA_DIR` is set | `<user-data-dir>/sand-data` |
| 3 | Packaged production (`SAND_PACKAGED=1`, not lab) | `~/.grokbot` |
| 4 | Unpackaged or lab | `~/.cursor/<variant>` (`sand-dev` or `sand-lab`) |

Packaged default file: `~/.grokbot/settings.json`. `settings.json` is also a signature entry for data-root settlement (alongside `gateway.json`, `host-secrets.json`, `host.lock`, and `.grokbot-data-root-v1`).

## Atomic write

Every persist pretty-prints JSON (`JSON.stringify(..., null, 2)`), writes a sibling temp file, then rename-replaces the live document:

```text
<settings.json>.<pid>.tmp  --write-->  renameSync -->  settings.json
```

`mkdirSync` creates the parent directory recursively. `load()` returns `emptySettings()` when the file is missing, JSON is invalid, or `version` is not `1`. Pending migration `downgrade-persisted-max-fast` runs after a successful parse and re-persists.

```mermaid
flowchart TB
  subgraph ui [Packaged Settings UI]
    Router["Settings → Router"]
  end
  subgraph rpc [Desktop RPC]
    IR["getInferenceRouter / setInferenceRouter"]
    BR["getBoxRuntime / setBoxRuntime"]
    Sync["syncHostSettingsToBox"]
  end
  subgraph host [Host]
    SS["SettingsService.setHostSettings"]
    Rec["recordInferenceUsage"]
  end
  subgraph store [SandSettingsStore]
    Persist["write pid tmp then rename"]
  end
  subgraph disk [Data root]
    File["settings.json"]
  end
  Router --> IR
  Router --> BR
  IR --> Persist
  BR --> Persist
  IR -->|"inferenceProvider"| Sync
  Sync --> SS
  SS --> Persist
  Rec --> Persist
  Persist --> File
```

## Document version and empty document

<ParamField body="version" type="1" required>
Must be exactly `1`. Any other value discards the file contents and loads `emptySettings()`.
</ParamField>

`emptySettings()` seeds:

| Field | Default |
| --- | --- |
| `version` | `1` |
| `mcpBoxServers` | `[]` |
| `autoUpdateWhenIdleOptIn` | `false` |
| `egressTunnelEnabled` | `false` |
| `webauthnProxyEnabled` | `true` |
| `mcpCustomInstructions` | `{}` |
| `mcpCustomInstructionsByServerId` | `{}` |
| `mcpDisabledToolsByServerId` | `{}` |
| `conciergeConsent` | `"unset"` |
| `settingsMigrations` | `["downgrade-persisted-max-fast"]` |

Optional fields (`inferenceProvider`, `inferenceRouterUsage`, `boxRuntime`, models, timezone, onboarding, sidebar) are omitted until first write. Getters apply in-memory defaults when the keys are absent.

## Featured persisted fields

### `inferenceProvider`

<ParamField body="inferenceProvider" type='"cursor" \| "claude-code" \| "codex" \| "openrouter"'>
Stored only when `isSandInferenceProvider` accepts the value. Getter default is `"cursor"`. Unknown strings are dropped on parse.
</ParamField>

Writes:

| Surface | Behavior |
| --- | --- |
| `window.desktop.agent.setInferenceRouter({ provider })` | Rejects unknown ids with `Unknown inference provider.`, then `setInferenceProvider` and `syncHostSettingsToBox({ inferenceProvider })`. Sync failure is swallowed; the local file still holds the provider. |
| `SettingsService.setHostSettings({ inferenceProvider })` | Same store write when the value is a known provider. |

Packaged Settings → Router reads `getInferenceRouter`, which prefers the desktop store provider and returns box usage when available. The `frontend/` workspace also has a client-persistence helper at key `settings.router-provider.v1`; packaged UI does not use that key for `settings.json`.

### `inferenceRouterUsage`

Local meter, `schemaVersion: 1`. Not a `HostSettingsUpdate` field. Host `getHostSettings()` returns it; turns append it through `recordInferenceUsage`.

<ResponseField name="schemaVersion" type="1">
Always reconstructed as `1` on parse. Incoming usage objects do not have to carry a version; counters are copied onto `emptySandInferenceRouterUsage()`.
</ResponseField>

<ResponseField name="providers" type="Record<SandInferenceProvider, usage row>">
One row per `cursor`, `claude-code`, `codex`, `openrouter`.
</ResponseField>

Each provider row:

| Field | Type | Parse / write rules |
| --- | --- | --- |
| `requests` | number | Safe integer ≥ 0, else `0`. Incremented by `1` per `recordInferenceUsage`. |
| `inputTokens` | number | Same. Added from finite ≥ 0 values, `Math.round`. |
| `outputTokens` | number | Same. |
| `cacheReadTokens` | number | Same. |
| `cacheWriteTokens` | number | Same. |
| `lastUsedAt` | string \| null | ISO timestamp on record; non-strings become `null`. |

Unknown provider keys in the JSON object are ignored. Missing usage loads `emptySandInferenceRouterUsage()` (all zeros, `lastUsedAt: null`).

### `boxRuntime`

<ParamField body="boxRuntime" type='"remote" \| "local-docker"'>
Getter default is `"remote"`. Unknown values are dropped. Not included in `HostSettingsUpdate` or `getHostSettings()`.
</ParamField>

`setBoxRuntime` persists first, then starts `grok-bot-local-vm` or stops it. On start/stop failure the store reverts (`local-docker` → `remote`, or the inverse) and the RPC throws. A successful switch then restarts the coordinator.

### MCP instruction maps

| Field | Shape | Constraints |
| --- | --- | --- |
| `mcpCustomInstructions` | `Record<string, string>` | Keys are connector display names. Values clamped to `MCP_CUSTOM_INSTRUCTIONS_MAX_LENGTH` (`500`). Empty strings are kept only when a default instruction exists for that name (today: `hex`); otherwise the key is deleted. |
| `mcpCustomInstructionsByServerId` | `Record<string, string>` | Keys must match `/^[1-9]\d*$/` (positive integer server ids, no leading zeros). Values clamped to 500 characters. |
| `mcpDisabledToolsByServerId` | `Record<string, string[]>` | Same numeric-id keys. Tool names are unique non-empty strings; empty lists are omitted. |
| `mcpCustomInstructionsAccountScope` | string | Non-empty string when present. Scopes MCP maps to an account. |
| `mcpBoxServers` | `string[]` | Unique non-empty names. |

`setMcpCustomInstructionByServerId` can mirror the clamped value into the legacy name map. `migrateMcpCustomInstructionToServerId` copies a legacy name into the id map only when the id is not already set.

Account switch (`scopeToAccount` with a different scope, or `clearAccountScope`) clears `mcpCustomInstructions`, `mcpCustomInstructionsByServerId`, and `mcpDisabledToolsByServerId`. A scope change also drops `autoReviewInstructions`, `agentDefaultModel`, `computerUseModel`, `localToolPermission`, and `localToolPermissionCeiling`.

## Other version-1 fields

| Field | Type | Notes |
| --- | --- | --- |
| `autoUpdateWhenIdleOptIn` | boolean | Default `false`. Stored only as exact `true`. |
| `egressTunnelEnabled` | boolean | Default `false`. |
| `webauthnProxyEnabled` | boolean | Default `true` unless JSON is exactly `false`. |
| `conciergeConsent` | `"unset"` \| `"allowed"` \| `"denied"` | Other values become `"unset"`. |
| `settingsMigrations` | string[] | Includes `downgrade-persisted-max-fast` after first load. |
| `hasSeenOnboarding` | boolean | Optional. |
| `hasSeenOnboardingAccountScope` | string | Optional; rewritten when onboarding is set. |
| `updateTrackOverride` | `"stable"` \| `"nightly"` \| `"dogfood"` | `nightly` is coerced to `stable` on read. |
| `themePreference` | `"system"` \| `"light"` \| `"dark"` | Getter default `"system"`. |
| `agentDefaultModel` | `{ modelId, maxMode, parameters[] }` | Reads force `maxMode: true`. Migration sets parameter `fast` to `"false"`. |
| `computerUseModel` | same selection shape | Stored as given. |
| `notifications` | object | Reads/writes collapse to `{ isEnabled: false }`. Host always returns `SAND_DISABLED_NOTIFICATION_CONFIG`. |
| `userTimeZone` | string | Detected IANA zone. Host accepts `""` to clear or a valid IANA id. |
| `userTimeZoneOverride` | string | Overrides detection. Effective zone is override ?? detected. |
| `autoReviewInstructions` | `{ isEnabled, allowInstructions, blockInstructions }` | Default enabled with empty lists. Lists capped at 20 unique entries, 1000 chars each. |
| `localToolPermission` | `"always"` \| `"ask"` \| `"never"` | Default `"ask"`. Effective value is `min(choice, ceiling)` by rank `never < ask < always`. |
| `localToolPermissionCeiling` | same enum | Optional admin cap. |
| `pinnedAgentIds` | string[] | Unique non-empty ids. |
| `sidebarSections` | `{ id, name, agentIds, isCollapsed? }[]` | Fold state is carried across updates. |

## Host update surface vs store

`HostSettingsUpdate` can set MCP maps, timezone, models, auto-review, local-tool permission, webauthn, pins, sidebar, onboarding, and `inferenceProvider`. It does **not** set `boxRuntime` or `inferenceRouterUsage`. `featureFlagOverrides` on the update object notify listeners only; they are not written to `settings.json`.

## Example document

<ResponseExample>

```json
{
  "version": 1,
  "mcpBoxServers": [],
  "autoUpdateWhenIdleOptIn": false,
  "egressTunnelEnabled": false,
  "webauthnProxyEnabled": true,
  "mcpCustomInstructions": {},
  "mcpCustomInstructionsByServerId": {
    "12": "Prefer CSV exports over screenshot charts."
  },
  "mcpDisabledToolsByServerId": {
    "12": ["browser_navigate"]
  },
  "conciergeConsent": "unset",
  "settingsMigrations": ["downgrade-persisted-max-fast"],
  "inferenceProvider": "codex",
  "inferenceRouterUsage": {
    "schemaVersion": 1,
    "providers": {
      "cursor": {
        "requests": 0,
        "inputTokens": 0,
        "outputTokens": 0,
        "cacheReadTokens": 0,
        "cacheWriteTokens": 0,
        "lastUsedAt": null
      },
      "claude-code": {
        "requests": 0,
        "inputTokens": 0,
        "outputTokens": 0,
        "cacheReadTokens": 0,
        "cacheWriteTokens": 0,
        "lastUsedAt": null
      },
      "codex": {
        "requests": 4,
        "inputTokens": 12000,
        "outputTokens": 3100,
        "cacheReadTokens": 0,
        "cacheWriteTokens": 0,
        "lastUsedAt": "2026-08-24T18:04:11.000Z"
      },
      "openrouter": {
        "requests": 0,
        "inputTokens": 0,
        "outputTokens": 0,
        "cacheReadTokens": 0,
        "cacheWriteTokens": 0,
        "lastUsedAt": null
      }
    }
  },
  "boxRuntime": "remote",
  "localToolPermission": "ask"
}
```

</ResponseExample>

## Load and write failures

<AccordionGroup>
<Accordion title="Corrupt or wrong-version file">
`load()` returns `emptySettings()` and does not throw. A later persist overwrites the file with a valid version-`1` document.
</Accordion>
<Accordion title="Unknown inference provider">
Desktop RPC throws `Unknown inference provider.` Store parse drops the key; getter returns `"cursor"`.
</Accordion>
<Accordion title="Unknown box runtime">
Desktop RPC throws `Unknown box runtime.` Store parse drops the key; getter returns `"remote"`. A failed Docker start/stop reverts the persisted mode before rethrowing.
</Accordion>
<Accordion title="MCP instruction overflow">
Values longer than 500 characters are sliced. Disabled-tool lists drop empty names and duplicates.
</Accordion>
<Accordion title="Notifications">
Any stored notification object is rewritten to `{ "isEnabled": false }` on read. Host never re-enables notifications from this file.
</Accordion>
</AccordionGroup>

<Warning>
Do not treat `settings.json` as a secrets store. OpenRouter keys go through `window.desktop.secrets` / `host-secrets.json`. Codex and Claude Code auth stay in those CLIs' own files.
</Warning>

## Related pages

<CardGroup>
<Card title="Inference router" href="/inference-router">
Provider ids, transcript store, and usage schemaVersion 1.
</Card>
<Card title="Box runtime" href="/box-runtime">
`remote` vs `local-docker`, coordinator restart, loopback gateway.
</Card>
<Card title="Desktop RPC" href="/desktop-rpc">
`getInferenceRouter`, `setInferenceRouter`, `getBoxRuntime`, `setBoxRuntime`.
</Card>
<Card title="Choose an inference provider" href="/choose-inference-provider">
Settings → Router persist path and provider credentials.
</Card>
<Card title="Enable the local Docker sandbox" href="/enable-local-docker">
Toggle that writes `boxRuntime` and starts `grok-bot-local-vm`.
</Card>
<Card title="Route Grok Bot plugin tools" href="/route-mcp-tools">
How MCP maps and routed tools are applied at turn time.
</Card>
<Card title="Environment variables" href="/environment-variables">
`SAND_DATA_ROOT`, `SAND_USER_DATA_DIR`, and related overrides.
</Card>
</CardGroup>
