# Config RPC reference

> `config/read`, `config/value/write`, `config/batchWrite`, `configRequirements/read`, `config/mcpServer/reload`, external-agent detect/import, and snake_case wire fields mirroring `config.toml`.

- Repository: openai/codex
- GitHub: https://github.com/openai/codex
- Human docs: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1
- Complete Markdown: https://grok-wiki.com/public/docs/openai-codex-c82680b15ec1/llms-full.txt

## Source Files

- `README.md`
- `src/request_processors/config_processor.rs`
- `src/request_processors/config_errors.rs`
- `src/config_manager.rs`
- `src/config/mod.rs`
- `src/request_processors/external_agent_config_processor.rs`
- `tests/suite/v2/config_rpc.rs`

---

---
title: "Config RPC reference"
description: "`config/read`, `config/value/write`, `config/batchWrite`, `configRequirements/read`, `config/mcpServer/reload`, external-agent detect/import, and snake_case wire fields mirroring `config.toml`."
---

App-server v2 exposes JSON-RPC methods under `config/*`, `configRequirements/read`, and `externalAgentConfig/*` so clients can read layered Codex settings, persist edits to `$CODEX_HOME/config.toml`, inspect enterprise constraints, reload MCP definitions without restarting the process, and migrate artifacts from other agent setups.

| Method | Purpose |
| --- | --- |
| `config/read` | Effective config after layer merge |
| `config/value/write` | Single-key write to user `config.toml` |
| `config/batchWrite` | Atomic multi-key write; optional thread hot-reload |
| `configRequirements/read` | Loaded `requirements.toml` / MDM constraints |
| `config/mcpServer/reload` | Re-read disk config and queue MCP refresh per loaded thread |
| `externalAgentConfig/detect` | Scan for migratable external-agent artifacts |
| `externalAgentConfig/import` | Apply selected migration items |

Implementation is split between `ConfigRequestProcessor` (RPC handlers), `ConfigManager` / `ConfigManagerService` (layer loading and disk writes), `McpRequestProcessor` + `mcp_refresh` (MCP reload), and `ExternalAgentConfigRequestProcessor` (detect/import).

## Wire naming

v2 config RPCs follow the usual split between **request envelopes** and **config payloads**:

| Surface | Casing | Examples |
| --- | --- | --- |
| Request/response wrappers | `camelCase` | `includeLayers`, `keyPath`, `mergeStrategy`, `reloadUserConfig`, `expectedVersion` |
| `config` object fields | `snake_case` (mirrors `config.toml`) | `sandbox_mode`, `model_reasoning_effort`, `forced_chatgpt_workspace_id` |
| `keyPath` segments | `config.toml` path syntax | `sandbox_mode`, `desktop.appearanceTheme`, `hooks.state` |
| Layer metadata | `camelCase` tagged union | `type: "user"`, `dotCodexFolder`, `enterpriseManaged` |
| Requirements (mostly) | `camelCase` | `allowedSandboxModes`, `allowManagedHooksOnly` |
| Managed hook event keys | PascalCase strings | `PreToolUse`, `SessionStart` |
| External-agent item types | `SCREAMING_SNAKE` strings | `CONFIG`, `MCP_SERVER_CONFIG`, `SESSIONS` |

<Note>
Pass `null` as a `value` to delete a key. Dotted `keyPath` values such as `desktop.someKey` use the same generic write surface as top-level keys.
</Note>

Unknown top-level config keys land in `config.additional` on read. The typed `Config` struct covers common fields; nested tables like `tools.web_search` deserialize into structured types when recognized.

## Config layering

Reads resolve a `ConfigLayerStack` (MDM → system → enterprise → user → project → session flags → legacy managed layers). Writes target only the **user** layer at `$CODEX_HOME/config.toml` (or an explicit `filePath` that resolves to the same canonical path).

```text
  MDM / system / enterprise / project layers
              │
              ▼ merge (higher precedence wins)
         effective config  ──►  config/read.config
              ▲
              │ write (user layer only)
         $CODEX_HOME/config.toml
```

<ParamField body="cwd" type="string">
Optional on `config/read`. When set, project-local `.codex/` layers between `cwd` and the repo root are included in the effective view.
</ParamField>

<ParamField body="includeLayers" type="boolean" default="false">
When `true`, returns the full layer list (highest precedence first) with per-layer `version` hashes for optimistic concurrency.
</ParamField>

### `config/read` response

<ResponseField name="config" type="object">
Effective configuration after layering. Field names use `snake_case`. Includes `desktop` as an opaque key/value map for client-specific settings.
</ResponseField>

<ResponseField name="origins" type="object">
Map from dotted config paths (e.g. `model`, `tools.web_search.context_size`) to `{ name, version }` describing which layer supplied each value. Use `origins["<path>"].version` as `expectedVersion` on writes.
</ResponseField>

<ResponseField name="layers" type="array | null">
Present when `includeLayers: true`. Each entry has `name`, `version`, `config` (raw JSON), and optional `disabledReason`.
</ResponseField>

After building the response, the server injects runtime feature flags under `config.additional.features.<key>` for: `apps`, `memories`, `mentions_v2`, `plugins`, `remote_control`, `remote_plugin`, `tool_suggest`, `tool_call_mcp_elicitation`. These reflect effective enablement after cloud requirements, CLI flags, `config.toml`, and in-process `experimentalFeature/enablement/set` overrides.

<RequestExample>

```json
{
  "method": "config/read",
  "id": 1,
  "params": {
    "includeLayers": true,
    "cwd": "/Users/me/my-repo"
  }
}
```

</RequestExample>

<ResponseExample>

```json
{
  "id": 1,
  "result": {
    "config": {
      "model": "gpt-5.1-codex",
      "sandbox_mode": "workspace-write",
      "tools": {
        "web_search": {
          "context_size": "low",
          "allowed_domains": ["example.com"]
        }
      }
    },
    "origins": {
      "model": {
        "name": { "type": "user", "file": "/Users/me/.codex/config.toml", "profile": null },
        "version": "sha256:abc..."
      }
    },
    "layers": [{ "name": { "type": "user", "file": "..." }, "version": "sha256:abc...", "config": {} }]
  }
}
```

</ResponseExample>

Some nested fields require `capabilities.experimentalApi` at `initialize` (for example `config.apps`, `config.approvals_reviewer`). Rejections follow the standard experimental gating messages.

## Writes

Both `config/value/write` and `config/batchWrite` persist atomically to the user config file, validate the post-merge effective config (including `feature_requirements` from requirements), and return a shared write result shape.

### Shared write parameters

<ParamField body="keyPath" type="string" required>
Dotted path into `config.toml`. Supports quoted segments for special characters (e.g. `profiles."team.prod".model` is rejected for writes — see restrictions below). Bare segments like `sample@catalog` remain valid.
</ParamField>

<ParamField body="value" type="JsonValue" required>
JSON value merged into TOML. Use `null` to clear a path.
</ParamField>

<ParamField body="mergeStrategy" type="\"replace\" | \"upsert\"" required>
`replace` overwrites the target path. `upsert` deep-merges objects and replaces scalars.
</ParamField>

<ParamField body="filePath" type="string">
Defaults to the resolved user `config.toml`. Only that path is writable; other paths return `configLayerReadonly`.
</ParamField>

<ParamField body="expectedVersion" type="string">
Optimistic lock against the user layer `version` from `origins` or a prior write response. Mismatch yields `configVersionConflict`.
</ParamField>

### Write response

<ResponseField name="status" type="\"ok\" | \"okOverridden\"">
`okOverridden` means a higher-precedence layer still controls the effective value; see `overriddenMetadata`.
</ResponseField>

<ResponseField name="version" type="string">
New user-layer version hash after the write.
</ResponseField>

<ResponseField name="filePath" type="string">
Canonical path written.
</ResponseField>

<ResponseField name="overriddenMetadata" type="object | null">
When `status` is `okOverridden`, explains which layer wins and the effective value clients will observe.
</ResponseField>

### Restrictions

Writes reject:

- `profile` and any `profiles.*` path (legacy profile tables; use `--profile <name>` with `<name>.config.toml` instead)
- `filePath` outside the user config (error code `configLayerReadonly`)
- Invalid TOML shapes or values that fail post-merge validation (`configValidationError`)

### Post-write side effects

Every successful `config/value/write` or `config/batchWrite` clears the in-process **plugins** and **skills** manager caches. Plugin enablement toggles may emit analytics events.

<ParamField body="reloadUserConfig" type="boolean" default="false">
**`config/batchWrite` only.** When `true`, rebuilds config and calls `refresh_runtime_config` on every loaded thread (same hot-reload path as `experimentalFeature/enablement/set`). Does not automatically refresh MCP server processes — use `config/mcpServer/reload` after MCP table edits.
</ParamField>

:::endpoint POST config/value/write Single-key user config write

<RequestExample>

```json
{
  "method": "config/value/write",
  "id": 2,
  "params": {
    "keyPath": "model",
    "value": "gpt-new",
    "mergeStrategy": "replace",
    "expectedVersion": "sha256:abc..."
  }
}
```

</RequestExample>

:::

:::endpoint POST config/batchWrite Atomic multi-key user config write

<RequestExample>

```json
{
  "method": "config/batchWrite",
  "id": 3,
  "params": {
    "edits": [
      {
        "keyPath": "hooks.state",
        "value": {
          "/Users/me/.codex/config.toml:pre_tool_use:0:0": { "enabled": false }
        },
        "mergeStrategy": "upsert"
      }
    ],
    "reloadUserConfig": true
  }
}
```

</RequestExample>

:::

```mermaid
sequenceDiagram
  participant Client
  participant ConfigProcessor as ConfigRequestProcessor
  participant ConfigSvc as ConfigManagerService
  participant Disk as config.toml
  participant Threads as Loaded threads

  Client->>ConfigProcessor: config/batchWrite
  ConfigProcessor->>ConfigSvc: apply_edits (atomic)
  ConfigSvc->>Disk: persist user layer
  ConfigSvc-->>ConfigProcessor: ConfigWriteResponse
  ConfigProcessor->>ConfigProcessor: clear plugin/skills cache
  alt reloadUserConfig true
    ConfigProcessor->>Threads: refresh_runtime_config each
  end
  ConfigProcessor-->>Client: result
```

## Write error codes

Failed writes return JSON-RPC invalid-request errors with `error.data.config_write_error_code`:

| Code | Typical cause |
| --- | --- |
| `configLayerReadonly` | `filePath` is not the user config |
| `configVersionConflict` | `expectedVersion` stale |
| `configValidationError` | Invalid value, forbidden path, or requirements violation |
| `configPathNotFound` | Target path missing when required |
| `configSchemaUnknownKey` | Unknown schema key |
| `userLayerNotFound` | Internal: user layer missing after update |

Config **load** failures (for example cloud requirements fetch) use a separate shape: `error.data.reason: "cloudRequirements"`, `errorCode`, optional `statusCode`, and `action: "relogin"` when auth is required.

## `configRequirements/read`

No parameters (`params` omitted or `undefined`). Returns `{ requirements: null }` when no `requirements.toml`, MDM, or cloud bundle constraints are loaded.

When present, `requirements` includes:

| Field | Meaning |
| --- | --- |
| `allowedApprovalPolicies` | Permitted approval policies |
| `allowedApprovalsReviewers` | Permitted reviewer routing (experimental) |
| `allowedSandboxModes` | Permitted sandbox modes (`externalSandbox` omitted from API) |
| `allowedWindowsSandboxImplementations` | `elevated` / `unelevated` |
| `allowedPermissions` | Permission profile id allow-list |
| `allowedWebSearchModes` | Web search modes (`disabled` always injected if missing) |
| `allowManagedHooksOnly` | Restrict hooks to managed sources |
| `allowAppshots` | App screenshot policy |
| `computerUse` | `allowLockedComputerUse` |
| `featureRequirements` | Pinned feature booleans |
| `hooks` | Managed hook directories and matcher groups (experimental) |
| `enforceResidency` | e.g. `us` |
| `network` | Domain/socket permissions, `managedAllowedDomainsOnly`, legacy allow/deny lists (experimental) |

<RequestExample>

```json
{ "method": "configRequirements/read", "id": 4 }
```

</RequestExample>

## `config/mcpServer/reload`

Reloads the latest config from disk, then queues `Op::RefreshMcpServers` on **every loaded thread** with per-thread MCP server snapshots. Refresh applies on each thread’s **next active turn**, not immediately mid-turn.

- Uses **strict** refresh: if any loaded thread cannot build refresh config, the RPC fails with an internal error (`failed to refresh MCP servers`).
- A separate **best-effort** refresh path exists internally for fault-tolerant scenarios; the public RPC is strict.

<RequestExample>

```json
{ "method": "config/mcpServer/reload", "id": 5 }
```

</RequestExample>

<ResponseExample>

```json
{ "id": 5, "result": {} }
```

</ResponseExample>

<Tip>
After hand-editing `config.toml` without `reloadUserConfig: true`, call `config/mcpServer/reload` so MCP servers pick up table changes without restarting app-server.
</Tip>

## External-agent migration

### `externalAgentConfig/detect`

<ParamField body="includeHome" type="boolean" default="false">
Scan home directories (`~/.claude`, `~/.codex`, etc.) for migratable artifacts.
</ParamField>

<ParamField body="cwds" type="string[]">
Optional repo roots for workspace-scoped detection.
</ParamField>

Returns `items[]` with `itemType`, `description`, `cwd` (`null` = home-scoped), and optional `details` (plugin marketplaces, session paths, MCP server names, hooks, subagents, commands).

| `itemType` | Migrates |
| --- | --- |
| `CONFIG` | Config files |
| `SKILLS` | Skill trees |
| `AGENTS_MD` | `AGENTS.md` style files |
| `PLUGINS` | Plugin marketplace entries |
| `MCP_SERVER_CONFIG` | MCP server definitions |
| `SUBAGENTS` | Subagent configs |
| `HOOKS` | Hook definitions |
| `COMMANDS` | Command definitions |
| `SESSIONS` | Session rollouts (imported as forked threads) |

### `externalAgentConfig/import`

Pass the `migrationItems` subset returned by detect (with matching `cwd` and `details` for plugins/sessions).

**Response timing:** Returns `{}` immediately. When the request includes migration items, the server emits `externalAgentConfig/import/completed` once all work finishes — synchronously if only fast paths run, or after background plugin/session imports complete.

**Runtime refresh:** Imports touching `CONFIG`, `SKILLS`, `MCP_SERVER_CONFIG`, `HOOKS`, `COMMANDS`, or `PLUGINS` clear plugin/skills caches (same as config writes). Plugin and session imports may continue in the background; session imports are serialized (semaphore limit 1).

**Sessions:** Each session path must have been seen during detect; unknown paths return invalid-params. Successful imports create threads via `thread/start`-equivalent forked history and record an import ledger entry.

<Steps>

<Step title="Detect">
Call `externalAgentConfig/detect` with `includeHome` and/or `cwds`, then present `items` to the user.
</Step>

<Step title="Import">
Call `externalAgentConfig/import` with selected `migrationItems` (preserve `details` for plugins/sessions).
</Step>

<Step title="Wait for completion">
Listen for `externalAgentConfig/import/completed`. If MCP servers changed, call `config/mcpServer/reload`.
</Step>

</Steps>

<RequestExample>

```json
{
  "method": "externalAgentConfig/detect",
  "id": 6,
  "params": { "includeHome": true, "cwds": ["/Users/me/project"] }
}
```

</RequestExample>

## Verification checklist

| Action | Expected signal |
| --- | --- |
| Read after edit | `config/read` shows updated `snake_case` fields |
| Optimistic lock | Stale `expectedVersion` → `configVersionConflict` |
| Batch hook disable | `hooks.state` upsert + `reloadUserConfig: true` → threads pick up on next turn settings |
| MCP edit on disk | `config/mcpServer/reload` returns `{}`; MCP status updates on next thread turn |
| Requirements present | `configRequirements/read.requirements` non-null with expected allow-lists |
| External import | `externalAgentConfig/import/completed` notification fires |

<Warning>
`config/batchWrite` does not reload MCP processes unless you also call `config/mcpServer/reload`. `reloadUserConfig` only refreshes thread runtime config snapshots.
</Warning>

## Related pages

<CardGroup>

<Card title="Account, auth, and config" href="/account-auth-and-config">
Login flows, requirements constraints in product context, and hot-reload behavior after writes.
</Card>

<Card title="Skills, plugins, and MCP" href="/skills-plugins-and-mcp">
`mcpServerStatus/list`, OAuth login, and MCP tool calls after config changes.
</Card>

<Card title="RPC methods reference" href="/rpc-methods">
Full v2 method catalog with stable vs experimental markers.
</Card>

<Card title="Schema generation" href="/schema-generation">
Generate TypeScript and JSON Schema fixtures that match your app-server binary version.
</Card>

<Card title="Experimental API" href="/experimental-api">
Opt in via `capabilities.experimentalApi` for gated config and requirements fields.
</Card>

</CardGroup>
