# 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.

## 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
  }
}
```

### Jev Advisory Routing

The bundled `jev-advisory-routing` extension uses the Jev decisions model for bounded opt-in
routing. The host-side routes stay off until they are enabled in `jevAdvisory`; the agent's own
`ask` route is on by default. All routes share one Jev provider/model pair:

- `memory` — only after an explicit durable-memory cue (for example “the convention we
  decided” or “don't repeat the past failure”), Jev chooses one read-only local
  `memory_search` target. Jev never sees memory contents, and no memory is written.
- `recommendations` — one discovered skill or prompt workflow that fits, or a proportionate
  verification level. Nothing is loaded, started, or executed: the recommendation is context for the
  agent, and required project/workflow gates are unchanged.
- `ask` — the agent-callable `ask_jev` tool (on by default). The agent decides when to call it: it
  passes its own prose, file `paths`, and one `command`, and gets typed choice/score/noul answers back —
  never the file contents or command output, so a verdict about code costs a few lines of context
  instead of the files. Files must be inside the working directory, secret-named files (`.env`,
  `*.pem`, `id_rsa*`, `auth.json`, …) are refused, and the command runs through the bash tool's local
  shell. Whatever the agent passes is sent to Jev. Without Token-In credentials the tool is taken out
  of the agent's loadout before each run (it returns after `/tokenin add`, no reload needed), and a call
  that slips through reads no file and runs no command.

  The same route also offers `jev_find`, a behavior finder: ripgrep gathers candidate files (those
  matching `pattern`, or every file under `path` filtered by `glob`); on large trees Jev first judges
  directories and descends only into relevant ones, then judges files, then the declarations (source
  units) inside relevant files. The agent gets a ranked file list with reading leads plus the accepted
  units verbatim with original line numbers (at most 12 Jev requests and 16 KiB of source per call).
  It respects `.gitignore`, stays inside the working directory, skips secret-named files, retries a
  transport failure once, and still returns ripgrep-ranked candidates with keyword-window source when
  Jev is unavailable.
- `subagent` — sets up a single-child `subagent` call from the parent agent (off by default). Jev 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. Jev
  sees only the task and agent descriptions. Any abstention launches exactly what was asked; an
  agentless task that Jev cannot route is rejected with a request to name an agent.

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `jevAdvisory.provider` | string | `"tokenin"` | Provider serving the Jev decisions deployment |
| `jevAdvisory.model` | string | `"jev-1.13"` | Decisions model every route calls |
| `jevAdvisory.baseUrl` | string | inherited | Base URL override; defaults to any registered model of `provider` |
| `jevAdvisory.routes.memory.enabled` | boolean | `false` | Enable the memory-lookup route |
| `jevAdvisory.routes.recommendations.enabled` | boolean | `false` | Enable skill/workflow and verification recommendations |
| `jevAdvisory.routes.ask.enabled` | boolean | `true` | Offer the agent the `ask_jev` and `jev_find` tools; set `false` to turn both off |
| `jevAdvisory.routes.subagent.enabled` | boolean | `false` | Let Jev choose the agent, tools, and model tier of a subagent call |
| `jevAdvisory.routes.subagent.tiers` | object | unset | `{ "simple"?, "complex"?, "reasoning"? }` → `provider/id` model; tier routing needs at least two |
| `jevAdvisory.routes.subagent.genericAgents` | string[] | `["delegate"]` | Agents Jev may replace with a specialist |
| `<route>.timeoutMs` | number | `8000` | Route request timeout (ms); memory is capped at 750ms; `ask` defaults to `15000`, `subagent` to `5000` |
| `<route>.minConfidence` | number | `0.6` | Below this Jev 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` | Hard cap on the serialized decision request; `ask` defaults to `32768`, `subagent` to `16384` |

`ask` ignores `minConfidence`, `contextTurns`, and `contextChars`: it returns every confidence to the
agent, and its context is whatever the agent put in the request.

Only idle, top-level, interactive prompts are routed: queued steering/follow-up input, slash
commands, and extension-injected turns are skipped, and each turn is routed at most once. The memory
route sends no history to Jev; 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
{
  "jevAdvisory": {
    "provider": "tokenin",
    "model": "jev-1.13",
    "routes": {
      "memory": { "enabled": true },
      "recommendations": { "enabled": true }
    }
  }
}
```

### Capability Gateway (experimental)

The bundled `capability-gateway` extension keeps optional extension tools dormant until they are
needed: a compact `capability_catalog` lists them, `capability_discover` activates one for the
current run, and `capability_skill_show` loads one skill's full instructions. Set
`SELESAI_CAPABILITY_GATEWAY=0` to disable the gateway and keep every tool visible.

Routing has three rungs:

1. A deterministic router activates a tool when the prompt uniquely matches its name, alias, or
   discovery summary. Skills are never auto-loaded or auto-selected.
2. Default-on Jev tie-breaking: when the deterministic router returns an ambiguous lexical hint
   among optional tools, the Jev decisions model is asked which of two or three hinted tools (or
   `none`) should be exposed. Jev sees only the bounded current prompt and each hinted tool's compact
   discovery line — never conversation history or tool schemas. A prompt with no
   lexical signal, a unique activation, and a skill-only match never reach Jev. Token-In credentials
   are required; without them, no Jev request is sent and Selesai prompts you to run `/tokenin add`.
3. The agent-driven route: `capability_discover` also takes a `job` instead of a name. Tools and
   skills are separate decisions — a tool is callable code from an extension or MCP server that the
   agent may use many times, a skill is a written procedure it reads once — so one Jev request asks
   two questions, each from catalog metadata only (never a schema or skill body), each allowed to
   answer `none`. A chosen tool is activated for the run and its parameters are returned; a chosen
   skill is named for `capability_skill_show`. Tools are offered first, so only skills can be left out
   of an oversized catalog, and a `none` then says how many were not considered. A skill found in
   several sources is offered once. While Jev has no credential, the `job` route is left out of the
   agent's instructions and the agent is pointed at `capability_catalog` plus an exact `name`.

All Jev features share one warning per session, shown the first time one of them actually needs the
missing credential; a user without a subscription is not warned just for starting a session.

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `capabilityGateway.routing.jev.enabled` | boolean | `true` | Enable Jev tie-breaking for ambiguous tool hints; toggle it in `/settings` (requires Token-In credentials) |
| `capabilityGateway.routing.jev.provider` | string | `"tokenin"` | Provider serving the Jev decisions deployment |
| `capabilityGateway.routing.jev.model` | string | `"jev-1.13"` | Decisions model the tie-breaker calls |
| `capabilityGateway.routing.jev.baseUrl` | string | inherited | Base URL override; defaults to any registered model of `provider` |
| `capabilityGateway.routing.jev.timeoutMs` | number | `1000` | Pre-turn request timeout (ms); hard-capped at `2000` |
| `capabilityGateway.routing.jev.discoverTimeoutMs` | number | `5000` | Timeout (ms) for `capability_discover({ job })`; hard-capped at `15000` |
| `capabilityGateway.routing.jev.minConfidence` | number | `0.6` | Below this Jev confidence the tie-breaker abstains |
| `capabilityGateway.routing.jev.payloadBytes` | number | `32768` | Hard cap on the serialized decision request; the pre-turn tie-break uses a few hundred bytes of it |

This area is independent of `jevAdvisory`: gateway routing reads only
`capabilityGateway.routing.jev` and shares just the Jev provider/model deployment identity. A
failure, timeout, invalid answer, low confidence, missing Token-In credentials, or `none` is an
ordinary abstention that leaves deterministic behavior in place. Temporary activations reset when the run settles, and
content-free telemetry on the `capability-gateway` event channel records the route outcome and
whether an activated tool was actually invoked.

### UI & Display

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `theme` | string | `"dark"` | Theme name (`"dark"`, `"light"`, or custom) |
| `quietStartup` | boolean | `false` | Hide startup header |
| `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) |
| `autocompleteMaxVisible` | number | `5` | Max visible items in autocomplete dropdown (3-20) |
| `showHardwareCursor` | boolean | `false` | Show the terminal cursor while TUI positions it for IME support |
| `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 `~`. |

```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 |

### 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. `llmModelOverride` is optional and uses `provider/model` format (for example, `tokenin/deepseek-v4.1-flash` if your account has access); leave it unset to use the active 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`).
