# Settings

Selesai uses JSON settings files with project settings overriding global settings.

| Location | Scope |
|----------|-------|
| `~/.selesai/agent/settings.json` | Global (all projects) |
| `.selesai/settings.json` | Project (current directory) |

Edit directly or use `/settings` for common options. Add `"$schema": "https://pi.dev/schemas/settings.schema.json"` to `settings.json` for editor validation and completion.

## Project Trust

On interactive startup, Selesai asks before trusting a project folder that contains project-local settings, resources, or project `.agents/skills` and has no saved decision for the folder or a parent folder in `~/.selesai/agent/trust.json`. Trusting a project allows Selesai to load `.selesai/settings.json` and `.selesai` resources, install missing project packages, and execute project extensions.

Non-interactive modes (`-p`, `--mode json`, and `--mode rpc`) do not show a trust prompt. Without an applicable saved trust decision, they use `defaultProjectTrust` from global settings: `ask` (default) and `never` ignore those project resources, while `always` trusts them. Pass `--approve`/`-a` or `--no-approve`/`-na` to override project trust for one run.

If no extension or saved decision applies, `defaultProjectTrust` controls the fallback behavior. Set it to `"ask"`, `"always"`, or `"never"` in `~/.selesai/agent/settings.json`, or change it with `/settings`.

`selesai config` and package commands use the same project trust flow, except `selesai update` never prompts. Pass `--approve` to trust project-local settings for one command or `--no-approve` to ignore them.

Use `/trust` in interactive mode to save a project trust decision for future sessions, including trust for the immediate parent folder. It writes `~/.selesai/agent/trust.json` only; the current session is not reloaded, so restart Selesai for changes to take effect.

## All Settings

### Model & Thinking

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `defaultProvider` | string | - | Default provider (e.g., `"anthropic"`, `"openai"`) |
| `defaultModel` | string | - | Default model ID |
| `defaultThinkingLevel` | string | - | `"off"`, `"minimal"`, `"low"`, `"medium"`, `"high"`, `"xhigh"` |
| `hideThinkingBlock` | boolean | `false` | Hide thinking blocks in output |
| `thinkingBudgets` | object | - | Custom token budgets per thinking level |

#### thinkingBudgets

```json
{
  "thinkingBudgets": {
    "minimal": 1024,
    "low": 4096,
    "medium": 10240,
    "high": 32768
  }
}
```

### Sai advisory routing

The bundled `sai-deci` extension uses the Sai decisions model (TokenIn `jev-1.14` by
default) for bounded routing. Memory, ask, and tool-search routing are on by default;
recommendations and subagent routing remain opt-in. Routes share a default provider/model pair, which individual routes may override:

- `memory` — the enabled route automatically considers each eligible request with usable text,
  choosing `none` or one read-only local memory target without an explicit cue. Sai never sees memory
  contents; no memory is written. An accepted target triggers at most one bounded lookup (five results)
  and the classifier has a 750 ms ceiling. Blank, image-only, and code-fence-only requests have no
  usable query and skip classification and lookup. Set `saiAdvisory.routes.memory.enabled` to `false`
  to opt out.
- `recommendations` — opt-in skill/workflow or verification suggestions; nothing is loaded, started,
  or executed and required verification is unchanged. Memory requests with usable text plus images
  and eligible recommendation requests with images fall back without a text-only downgrade.
- `toolSearch` — ranks at most 16 BM25-shortlisted tools in one fitted request, with at most one
  transient retry and one operation deadline (`timeoutMs`, 6 s by default). Cancellation reaches
  transport; failure or unavailable Sai falls back to local BM25. Ranking does not load tools.
- `ask` — offers agent-invoked `ask_sai` and `sai_find`; neither is an approval gate. `ask_sai`
  classifies supplied evidence into a small explicit choice domain, score rubric, or probability.
  It accepts up to eight paths, one command, and eight questions; confidence may be absent, invalid
  answers are rejected per question and valid siblings retained. It returns answers and diagnostics,
  not file contents or command output. Paths must be inside the working directory and secret-named
  files are refused. The main agent owns investigation, correctness, root-cause debugging, verification,
  completion, and loop control; use code for exact error codes, exit codes, counts, and test results.
  Evidence and criteria are untrusted; answers are not proof or instructions. Handle missing answers
  and low confidence with a fallback.

  Any nonempty `imagePaths` refuses the whole call before file reads or commands; the schema permits
  up to four paths, but no images can currently be sent. Unsupported or malformed inputs may have
  different reasons; otherwise refusal is `image-budget-unavailable`. No text-only downgrade occurs.
  Known image-looking `data:image/` strings, `image_url` fields and `"type":"image"` parts in text
  or files are refused too, so `sai_find` batches containing them can fail closed. No arbitrary hidden
  encodings are decoded. Without credentials, the tools remain registered and nothing is read or run.

  `sai_find` uses ripgrep to discover candidates and Sai for bounded relevance ranking. Files mode
  returns ranked judged files; `output: "source"` also returns reading leads and accepted source with
  original line numbers. It allows at most 12 requests, 128 judged files, 16 KiB of source and a 30 s
  total deadline. It respects `.gitignore`, skips secret-named files, and falls back to ripgrep ranking
  and local keyword-window source if Sai cannot judge. Each eligible transient network or HTTP
  408/409/429/5xx failure may be retried once within the request budget and deadline; cancellation
  reaches requests, and a `Retry-After` that cannot fit is skipped. Weak results are not proof of
  absence: broaden discovery or read/grep directly.
- `subagent` — sets up a single-child `subagent` call from the parent agent (off by default). Sai only
  fills what the parent left open, and can only narrow: (1) when `agent` is omitted or generic
  (`genericAgents`, default `["delegate"]`), it picks an agent by function from enabled native agents
  inside the capability ceiling; (2) it may drop tools from that agent's declared `tools` for this run
  (never `read` or supervision tools, and never adds one); (3) when `tiers` is set and the parent
  passed no `model`, it picks `simple`, `complex`, or `reasoning` and launches that tier's model. Sai
  sees only the task and agent descriptions. Any abstention launches exactly what was asked; an
  agentless task that Sai cannot route is rejected with a request to name an agent.

The settings area is `saiAdvisory`; the former `jevAdvisory` key is not read and has no alias.

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `saiAdvisory.provider` | string | `"tokenin"` | Provider serving the Sai decisions deployment |
| `saiAdvisory.model` | string | `"jev-1.14"` | Default decisions model, overridable per route |
| `saiAdvisory.baseUrl` | string | inherited | Base URL override; defaults to any registered model of `provider` |
| `saiAdvisory.routes.memory.enabled` | boolean | `true` | Enable the memory-lookup route |
| `saiAdvisory.routes.recommendations.enabled` | boolean | `false` | Enable skill/workflow and verification recommendations |
| `saiAdvisory.routes.ask.enabled` | boolean | `true` | Offer the agent the `ask_sai` and `sai_find` tools; set `false` to turn both off |
| `saiAdvisory.routes.toolSearch.enabled` | boolean | `true` | Rank `tool_search` and codemode `searchTools()` results with Sai |
| `saiAdvisory.routes.toolSearch.model` | string | unset | Rank with this TokenIn decisions model instead of the shared Sai model |
| `saiAdvisory.routes.subagent.enabled` | boolean | `false` | Let Sai choose the agent, tools, and model tier of a subagent call |
| `saiAdvisory.routes.subagent.tiers` | object | unset | `{ "simple"?, "complex"?, "reasoning"? }` → `provider/id` model; tier routing needs at least two |
| `saiAdvisory.routes.subagent.genericAgents` | string[] | `["delegate"]` | Agents Sai may replace with a specialist |
| `<route>.timeoutMs` | number | `8000` | Route request timeout (ms); memory is capped at 750ms; `ask` defaults to `15000`, `toolSearch` to `6000`, `subagent` to `5000` |
| `<route>.minConfidence` | number | `0.6` | Below this Sai confidence the route abstains |
| `<route>.contextTurns` | number | `4` | Prior user turns sent as recommendation context; memory sends only the current bounded prompt |
| `<route>.contextChars` | number | `4000` | Character budget for recommendation context |
| `<route>.payloadBytes` | number | `8192` | Early serialized text cap; `ask` and `toolSearch` default to `32768`, `subagent` to `16384` |

`ask` and `toolSearch` ignore `minConfidence`, `contextTurns`, and `contextChars`. Existing route
`payloadBytes` settings remain early text bounds. The completed HTTP body is separately capped at
65,536 UTF-8 bytes inclusive after transformations and serialization. This wire-byte safety bound is
not a token-fit guarantee; gateway token accounting and live image evidence remain unknown.

Only idle, top-level, interactive or RPC prompts that carry text or images are routed: queued
steering/follow-up input, slash commands, extension-injected turns, and input with neither text nor
images are skipped, and each turn is routed at most once. Eligibility uses the current turn;
recommendations may include bounded prior text, never history images. Image-bearing Sai requests are
refused rather than judged. The memory route sends no
history to Sai; on an accepted target it runs one local, read-only lookup (at most five results,
bounded before injection). A missing Token-In subscription, a timeout, a malformed or low-confidence
answer, an unknown candidate, unavailable/empty local memory, or an oversized payload is an ordinary
abstention that leaves the existing memory policy and verification requirements in force.

```json
{
  "saiAdvisory": {
    "provider": "tokenin",
    "model": "jev-1.14",
    "routes": {
      "memory": { "enabled": true },
      "recommendations": { "enabled": true }
    }
  }
}
```

### Tools and skills

The bundled defaults use `"defaultTools": ["+codemode"]`: keep the normal tool selection and
add codemode. Plain names replace the selection; `+name`/`-name` adjust it. The same applies to `--tools` for one invocation: `selesai --tools +tool_search` adjusts the resolved defaults, while plain names replace them. On `/reload`, a tool removed with `-name` stays removed. Frequently used
owned tools remain direct; specialised tools use upstream deferred exposure. Discover deferred
tools through codemode `searchTools()` or `tool_search` when enabled. Loaded tools remain active
across settlement and resume; there is no gateway reset or legacy capability tie-breaking policy.

The bundled `skills-index` extension replaces the eager skill listing with compact guidance.
Use `skill_show` without a name to list eligible skills, `query` to filter their metadata, or an
exact `name` to load instructions before applying a skill. It never activates tools or chooses
skills automatically. If `skill_show` is excluded, the ordinary skill index is retained.
The retired gateway environment variable and settings have no effect in the parent runtime.

MCP OAuth uses Selesai-branded dynamic registration or a pre-registered `oauth.clientId`.
`oauth.clientRegistration: "cimd"` is rejected: Selesai has no hosted client metadata document
and must not identify itself through upstream Pi's document. OAuth browser page artwork comes
from the upstream dependency and is unchanged.

### UI & Display

The logo and Tools/Skills panels always appear on interactive startup and dismiss on your first message. Legacy `quietStartup` settings are ignored.

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `theme` | string | `"dark"` | Theme name (`"dark"`, `"light"`, or custom) |
| `defaultProjectTrust` | string | `"ask"` | Fallback project trust behavior: `"ask"`, `"always"`, or `"never"`. Global setting only |
| `collapseChangelog` | boolean | `false` | Show condensed changelog after updates |
| `enableInstallTelemetry` | boolean | `true` | Send an anonymous install/update version ping after first install or changelog-detected updates. This does not control update checks |
| `enableAnalytics` | boolean | `false` | Opt-in analytics data sharing. Currently only asked for during the experimental first-time setup (`PI_EXPERIMENTAL=1`) |
| `trackingId` | string | - | Analytics tracking identifier, generated when `enableAnalytics` is turned on |
| `doubleEscapeAction` | string | `"tree"` | Action for double-escape: `"tree"`, `"fork"`, or `"none"` |
| `treeFilterMode` | string | `"default"` | Default filter for `/tree`: `"default"`, `"no-tools"`, `"user-only"`, `"labeled-only"`, `"all"` |
| `editorPaddingX` | number | `0` | Horizontal padding for input editor (0-3) |
| `outputPad` | `0 \| 1` | `1` | Horizontal padding for the interactive transcript: messages, tool output, `!` command output, and summary blocks. Any value other than `0` reads as `1`. Configurable in `/settings` as "Output padding", which saves it to the global settings file and applies it immediately. Applies to the built-in transcript components; it does not change the HTML export, which uses fixed padding |
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
| `showHardwareCursor` | boolean | `false` | Use the terminal cursor instead of the drawn cursor. The cursor is still positioned for input methods |
| `tuiMode` | string | `"fullscreen"` | TUI layout: `"fullscreen"` (default) or `"regular"`. `fullscreen` uses Pi's native alternate-screen viewport with a fixed dock for the input editor, status, and widgets; set `"regular"` to opt out |
| `fullscreenScrollbar` | string | `"auto"` | Fullscreen transcript scrollbar: `"auto"`, `"always"`, or `"hidden"`; configurable in `/settings`. No effect in regular TUI mode |
| `fullscreenCopyOnSelect` | boolean | `true` | Automatically copy selected text in fullscreen mode; disable to copy selections with Ctrl+X. Configurable in `/settings`. No effect in regular TUI mode |

### Telemetry and update checks

`enableInstallTelemetry` only controls the anonymous install/update ping to `https://pi.dev/api/report-install`. Opting out of telemetry does not disable update checks; Pi can still fetch `https://pi.dev/api/latest-version` to look for the latest version.

Set `PI_SKIP_VERSION_CHECK=1` to disable the Pi version update check. Use `--offline` or `PI_OFFLINE=1` to disable all startup network operations described here, including update checks, package update checks, and install/update telemetry.

### Network

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `httpProxy` | string | - | HTTP proxy URL applied as `HTTP_PROXY` and `HTTPS_PROXY`. Global setting only. |

```json
{
  "httpProxy": "http://127.0.0.1:7890"
}
```

### Warnings

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `warnings.anthropicExtraUsage` | boolean | `true` | Show a warning when Anthropic subscription auth may use paid extra usage |

```json
{
  "warnings": {
    "anthropicExtraUsage": false
  }
}
```

### Compaction

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `compaction.enabled` | boolean | `true` | Enable auto-compaction |
| `compaction.reserveTokens` | number | `16384` | Tokens reserved for LLM response |
| `compaction.keepRecentTokens` | number | `20000` | Recent tokens to keep (not summarized) |

```json
{
  "compaction": {
    "enabled": true,
    "reserveTokens": 16384,
    "keepRecentTokens": 20000
  }
}
```

### Branch Summary

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `branchSummary.reserveTokens` | number | `16384` | Tokens reserved for branch summarization |
| `branchSummary.skipPrompt` | boolean | `false` | Skip "Summarize branch?" prompt on `/tree` navigation (defaults to no summary) |

### Retry

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `retry.enabled` | boolean | `true` | Enable automatic agent-level retry on transient errors |
| `retry.maxRetries` | number | `10` | Maximum agent-level retry attempts |
| `retry.baseDelayMs` | number | `2000` | Base delay for agent-level exponential backoff (2s, 4s, 8s) |
| `retry.provider.timeoutMs` | number | SDK default | Provider/SDK request timeout in milliseconds |
| `retry.provider.maxRetries` | number | `0` | Provider/SDK retry attempts |
| `retry.provider.maxRetryDelayMs` | number | `60000` | Max server-requested delay before failing (60s) |

When a provider requests a retry delay longer than `retry.provider.maxRetryDelayMs`, the request fails immediately with an informative error instead of waiting silently. Set it to `0` to disable the limit.

Keep `retry.provider.maxRetries` at `0` unless provider-level retries are explicitly needed. Setting it above `0` can make SDK/provider retries handle out-of-usage-limit errors before Pi sees them, which may block the agent until the provider quota resets in some circumstances.

```json
{
  "retry": {
    "enabled": true,
    "maxRetries": 10,
    "baseDelayMs": 2000,
    "provider": {
      "timeoutMs": 3600000,
      "maxRetries": 0,
      "maxRetryDelayMs": 60000
    }
  }
}
```

### Message Delivery

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `steeringMode` | string | `"one-at-a-time"` | How steering messages are sent: `"all"` or `"one-at-a-time"` |
| `followUpMode` | string | `"one-at-a-time"` | How follow-up messages are sent: `"all"` or `"one-at-a-time"` |
| `transport` | string | `"auto"` | Preferred transport for providers that support multiple transports: `"sse"`, `"websocket"`, `"websocket-cached"`, or `"auto"` |
| `httpIdleTimeoutMs` | number | `300000` | HTTP header/body idle timeout in milliseconds, also used by providers with explicit stream idle timeouts. Set to `0` to disable. |
| `websocketConnectTimeoutMs` | number | `15000` | WebSocket connect/open handshake timeout in milliseconds for providers that support WebSocket transports. Set to `0` to disable. |

### Terminal & Images

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `terminal.showImages` | boolean | `true` | Show images in terminal (if supported) |
| `terminal.imageWidthCells` | number | `60` | Preferred inline image width in terminal cells |
| `terminal.clearOnShrink` | boolean | `false` | Clear empty rows when content shrinks (can cause flicker) |
| `terminal.images` | string | `"auto"` | Terminal image protocol: `"kitty"`, `"iterm2"`, `"auto"`, or `false` to disable inline images |
| `terminal.trueColor` | boolean/string | `"auto"` | Pin true-color support (`true`/`false`) or keep `"auto"` detection |
| `terminal.hyperlinks` | boolean/string | `"auto"` | Pin hyperlink support (`true`/`false`) or keep `"auto"` detection |
| `images.autoResize` | boolean | `true` | Resize images to 2000x2000 max |
| `images.blockImages` | boolean | `false` | Block all images from being sent to LLM |
| `images.imageCaptionModel` | string | unset | Vision model (`provider/model`) to describe images when the active model cannot accept images; the caption text is used in place of the image (e.g. `tokenin/gemma-4`, `tokenin/kimi-k3`) |
| `images.imageCaptionContextTokens` | number | `16384` | Max token budget (approx) for the recent-conversation context sent to the caption model. Messages are kept whole (cut at message boundaries only); the user's current prompt is always included |

### Shell

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `shellPath` | string | - | Custom shell path (e.g., for Cygwin on Windows) |
| `shellCommandPrefix` | string | - | Prefix for every bash command (e.g., `"shopt -s expand_aliases"`) |
| `npmCommand` | string[] | - | Command argv used for npm package lookup/install operations (e.g., `["mise", "exec", "node@20", "--", "npm"]`) |

```json
{
  "npmCommand": ["mise", "exec", "node@20", "--", "npm"]
}
```

`npmCommand` is used for all npm package-manager operations, including installs, uninstalls, and dependency installs inside git packages. User-scoped npm packages install under `~/.selesai/agent/npm/`; project-scoped npm packages install under `.selesai/npm/`. Use argv-style entries exactly as the process should be launched. When `npmCommand` is configured, git package dependency installs use plain `install` to avoid npm-specific flags in wrappers or alternate package managers.

### Sessions

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `sessionDir` | string | - | Directory where session files are stored. Accepts absolute or relative paths, plus `~`. |
| `sessionShareDestination` | string | `"auto"` | Destination used by `/share`: `"auto"` uses Radius when authenticated and otherwise creates a GitHub gist, `"gist"` always uses a gist, and `"radius"` requires Radius authentication and never falls back |

```json
{ "sessionDir": ".selesai/sessions" }
```

When multiple sources specify a session directory, precedence is `--session-dir`, `SELESAI_CODING_AGENT_SESSION_DIR`, then `sessionDir` in settings.json.

### Model Cycling

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `enabledModels` | string[] | - | Model patterns for Ctrl+P cycling (same format as `--models` CLI flag) |

```json
{
  "enabledModels": ["claude-*", "gpt-4o", "gemini-2*"]
}
```

### Markdown

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `markdown.codeBlockIndent` | string | `"  "` | Indentation for code blocks |
| `markdown.mermaid` | string | `"streaming"` | Render Mermaid fences as Unicode diagrams: `"off"`, `"final"`, or `"streaming"`. Also configurable in `/settings`. Diagrams that cannot render or fit retain their source fence. |

### Resources

These settings define where to load extensions, skills, prompts, and themes from.

Paths in `~/.selesai/agent/settings.json` resolve relative to `~/.selesai/agent`. Paths in `.selesai/settings.json` resolve relative to `.selesai`. Absolute paths and `~` are supported.

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `packages` | array | `[]` | npm/git packages to load resources from |
| `extensions` | string[] | `[]` | Local extension file paths or directories |
| `skills` | string[] | `[]` | Local skill file paths or directories |
| `prompts` | string[] | `[]` | Local prompt template paths or directories |
| `themes` | string[] | `[]` | Local theme file paths or directories |

Arrays support glob patterns and exclusions. Use `!pattern` to exclude. Use `+path` to force-include an exact path and `-path` to force-exclude an exact path.

#### packages

String form loads all resources from a package:

```json
{
  "packages": ["pi-skills", "@org/my-extension"]
}
```

Object form filters which resources to load:

```json
{
  "packages": [
    {
      "source": "pi-skills",
      "skills": ["brave-search", "transcribe"],
      "extensions": []
    }
  ]
}
```

See [packages.md](packages.md) for package management details.

### extensionHost

When selesai reads extensions from both `~/.selesai/agent/extensions/` and `~/.pi/agent/extensions/`, name collisions are resolved per extension. Default winner: selesai. Override per name:

```json
{
  "extensionHost": {
    "pi-subagents": "pi",
    "copy-turn.ts": "selesai"
  }
}
```

Keys are top-level entry names (dir name for packaged extensions, file name for loose `.ts`). Values are `"selesai"` or `"pi"`. See [Shared Host Extensions](shared-host-extensions.md) for the full flow.

## Bundled extension settings

The `pi-hermes-memory` extension reads its options from the `hermesMemory` object in global `~/.selesai/agent/settings.json`:

```json
{
  "hermesMemory": {
    "llmThinkingOverride": "off",
    "consolidationTimeoutMs": 300000
  }
}
```

These settings load at startup and take precedence over the legacy `hermes-memory-config.json` fallback. Memory LLM operations always use `tokenin/deepseek-v4.1-flash`, independently of the active chat model. There are no model override or fallback settings. Token-In credentials and model access are required; failures never switch to the chat model. See the [memory extension configuration reference](../src/extensions/pi-hermes-memory/README.md#configuration) for all options.

## Example

```json
{
  "defaultProvider": "anthropic",
  "defaultModel": "claude-sonnet-4-20250514",
  "defaultThinkingLevel": "medium",
  "theme": "dark",
  "compaction": {
    "enabled": true,
    "reserveTokens": 16384,
    "keepRecentTokens": 20000
  },
  "retry": {
    "enabled": true,
    "maxRetries": 10
  },
  "enabledModels": ["claude-*", "gpt-4o"],
  "warnings": {
    "anthropicExtraUsage": true
  },
  "packages": ["pi-skills"]
}
```

## Project Overrides

Project settings (`.selesai/settings.json`) override global settings. Nested objects are merged:

```json
// ~/.selesai/agent/settings.json (global)
{
  "theme": "dark",
  "compaction": { "enabled": true, "reserveTokens": 16384 }
}

// .selesai/settings.json (project)
{
  "compaction": { "reserveTokens": 8192 }
}

// Result
{
  "theme": "dark",
  "compaction": { "enabled": true, "reserveTokens": 8192 }
}
```

## Factory Reset

Run `/settings-factory-reset` inside a session to restore the global settings file (`~/.selesai/agent/settings.json`) to the bundled factory defaults. The command warns before replacing the file, saves a backup as `settings.json.bak` next to it, and leaves credentials (`auth.json`), sessions, extensions, skills, and themes untouched. Settings take effect immediately after the reset (equivalent to `/reload`).
