diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 0f868049d..e9c7a8fa4 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -2811,8 +2811,9 @@ reasoning contract, and all four membership ids omit generic sampling fields. Custom paths must be absolute; a relative value falls back to the user-global path so changing the launch directory cannot silently change the MCP pool. It is visible in `/config` and can be changed from the TUI. The new path is - used immediately by `/mcp`, but rebuilding the model-visible MCP tool pool - requires restarting the TUI. + used immediately by `/mcp`; run `/mcp reload` to rebuild the model-visible + MCP tool pool without restarting the TUI. A headless runtime still needs a + restart because its `ConfigReload` request does not refresh MCP connections. - `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and is used by the model-visible `note` tool. diff --git a/docs/MCP.md b/docs/MCP.md index 015dd191a..b0342f37b 100644 --- a/docs/MCP.md +++ b/docs/MCP.md @@ -5,7 +5,29 @@ In the terminal, `/mcp` (also `/mcps`) opens **Extensions → MCP**. Enter opens > 阅读简体中文版:[zh_hans/MCP.md](zh_hans/MCP.md) -codewhale can load additional tools via MCP (Model Context Protocol). MCP servers can be local stdio processes that the TUI starts, or remote URL-based servers that speak Streamable HTTP with legacy SSE fallback. +Codewhale can load additional tools via MCP (Model Context Protocol). MCP servers can be local stdio processes that Codewhale starts, or remote URL-based servers that speak Streamable HTTP with legacy SSE fallback. + +## Version scope + +This page describes the main candidate. As of November 7, 2026, the latest +published CLI release is [v0.10.2](https://github.com/codewhale-hq/Codewhale/releases/tag/v0.10.2). +For that installed release, use its [versioned MCP guide](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MCP.md). +Local stdio, remote HTTP/SSE, environment-backed authentication, direct remote +OAuth, and reviewed plugin-contributed MCP are already supported in v0.10.2. +Selected differences described below are candidate behavior: + +| Surface | Published v0.10.2 | Main candidate | +| --- | --- | --- | +| Default MCP client backend | Rust; SDK host is opt-in | Official MCP SDK 2.4.0 host; explicit Rust selection remains available | +| Native stdio server protocols | Legacy revisions through 2025-06-18 | Adds 2025-11-25 and modern 2026-07-28 discovery | +| Direct remote OAuth | Discovery, dynamic registration or a pre-registered public client | Adds issuer/resource-bound credentials and hosted client metadata support | +| Catalogue freshness and MCP Skills | No TTL/list-change refresh or MCP Skills extension | Bounded refresh and verified skill resource reads; no native skill activation | +| Package-runner connect budget | Default 30 seconds | At least 120 seconds unless the server has an explicit override | + +The candidate's default host needs a supported Node runtime (`^22.19 || >=24`); +Bun 1.4.0 or newer is opt-in. See [extension runtime requirements](EXTENSIONS.md). +An MCP server's own executable, dependencies, authentication and OS requirements +remain separate from Codewhale's transport support. Browsing note: - `Web` is the canonical, deferred built-in browsing tool; it provides @@ -84,7 +106,14 @@ stay consistent. A reviewed local plugin bundle may contribute MCP servers without creating a second transport or approval system. The servers use the same MCP manager, tool approval, resource, prompt, timeout, and network-policy paths documented -here, and appear under namespaced `-` identities. +here, and appear under collision-resistant +`plugin---` identities. + +Authentication differs from direct user-authored MCP configuration. Plugin +remotes may use `env_headers` or `bearer_token_env_var`, but plugin-contributed +MCP OAuth is disabled in both v0.10.2 and the main candidate. Declaring OAuth, +scopes or an OAuth resource in a plugin MCP manifest fails validation. To use +OAuth, configure the remote server directly in your own `mcp.json` instead. The bundle boundary is intentionally stricter than user-authored `mcp.json`: unknown fields and ambiguous transports fail closed; stdio environment values @@ -94,7 +123,7 @@ the normalized endpoint host set; and redirects remain on the reviewed origin. Reviewed plugin remotes also bypass ambient HTTP proxy configuration entirely; proxy credentials and proxy-observed traffic are not part of the v1 review. The plugin review discloses local host-user authority, structural argv, -environment provenance, endpoint, auth source names, scopes, and tool filters +environment provenance, endpoint, auth source names, and tool filters without reading or printing secret values. Trust stages reviewed content but does not enable it. Enablement attaches that @@ -133,6 +162,14 @@ codewhale mcp remove codewhale mcp validate ``` +Shell commands inspect their own process's MCP pool. In particular, +`codewhale mcp validate` starts and connects enabled, allowed servers and reports +connection/discovery failures; it is not a static configuration check and does +not attach those connections to a running TUI. In the TUI, `/mcp validate` +reports the last recorded snapshot without reconnecting; `/mcp reload` rebuilds +that session's live pool. `codewhale doctor` is the static diagnostic path +unless an explicit probe is requested. + `codewhale mcp logout ` (and `/mcp logout`) clears locally stored OAuth credentials only — the provider may keep its standing grant. The next login forces the consent screen, so the authorized account/workspace can @@ -259,8 +296,11 @@ ambient proxy authority. ## Remote HTTP Auth -URL-based MCP servers can use static headers, env-derived headers, bearer-token -env vars, or OAuth. Authorization precedence is conservative: +Direct URL-based MCP servers can use static headers, env-derived headers, +bearer-token env vars, or OAuth. Plugin-contributed remotes are limited to +environment-backed headers/bearer tokens and cannot use MCP OAuth; see +[Plugin-contributed MCP](#plugin-contributed-mcp). Authorization precedence for +direct servers is conservative: 1. `headers` and `env_headers` are applied first. 2. `bearer_token_env_var` adds `Authorization: Bearer ` when no @@ -371,16 +411,15 @@ shape looks like this: "servers": { "huggingface": { "url": "https://huggingface.co/mcp", - "headers": { - "Authorization": "Bearer ${HF_TOKEN}" - } + "bearer_token_env_var": "HF_TOKEN" } } } ``` -The placeholder above is not a runnable secret. Use the settings-generated -value in your private MCP config and never commit real Hugging Face tokens. +Set `HF_TOKEN` in the environment that launches Codewhale; the example contains +only its variable name. Literal `headers` values do not expand `${HF_TOKEN}`. +Adapt the settings-generated server entry and never commit real tokens. Interactive helpers: @@ -461,7 +500,20 @@ Default path: Overrides: - Config: `mcp_config_path = "/path/to/mcp.json"` -- Env: `DEEPSEEK_MCP_CONFIG=/path/to/mcp.json` +- Env: `CODEWHALE_MCP_CONFIG=/absolute/path/to/mcp.json` (takes precedence over + the legacy `DEEPSEEK_MCP_CONFIG` spelling) + +Custom paths must be absolute; a relative `mcp_config_path` falls back to the +user-global path rather than changing with the launch directory. + +Codewhale also reads `/.codewhale/mcp.json` after the workspace is +trusted in user-owned configuration. Project entries override same-named global +servers; project-local files cannot grant that trust themselves or redirect the +global `mcp_config_path`. Global and trusted-project entries take precedence +over a colliding plugin-contributed identity. Project stdio commands default +their working directory to the workspace. In the main candidate, +`always_allow_tools` is accepted only from the global server configuration, +not from project or plugin declarations. `codewhale mcp init` (and `codewhale setup --mcp`) writes to this resolved path. @@ -513,11 +565,26 @@ The CLI also exposes helper tools when MCP is enabled: } ``` -You can also use `mcpServers` instead of `servers` for compatibility with other clients. +You can also use `mcpServers` instead of `servers` for compatibility with other +clients. Use exactly one map key in a file: a file containing both fails to +parse. ## Running Codewhale as an MCP Server -You can register your local Codewhale binary as an MCP server so other Codewhale sessions (or any MCP client) can call its tools. +You can register your local Codewhale binary as a stdio MCP server for another +Codewhale session or a client supporting one of its advertised protocol +revisions. This exposes native tools; it is separate from connecting Codewhale +to external servers. + +### Client configuration vs server configuration + +`mcp.json` tells a client which servers to launch or contact. The native +`codewhale serve --mcp` process separately reads `[server]` from the retained +legacy path `~/.deepseek/mcp_server.toml`. By default it exposes only +`file_read` and `search`. Writing files or running commands requires naming +those tools in `expose_tools` and explicitly setting `require_approval = false` +in that server file. This stdio server has no interactive approval channel; +with approval required, writing/executing tools are withheld and refused. ### Quick Setup @@ -731,7 +798,8 @@ activation. Per-server settings: -- `command` (string, required) +- `command` (string, required for a local stdio server): executable to start. + Remote servers use `url` instead; a URL-only entry is valid. - `args` (array of strings, optional) - `env` (object, optional) - `connect_timeout`, `execute_timeout`, `read_timeout` (seconds, optional). A per-server value overrides the global `timeouts` block, and each budget is independent of the others: diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md index 4d8877fc6..1306b7eaa 100644 --- a/docs/PLUGINS.md +++ b/docs/PLUGINS.md @@ -12,9 +12,64 @@ format (`plugin.json`, compatible `kimi.plugin.json` or the trust/enable lifecycle — this document covers how bits get onto disk in the first place. +## Choose a plugin, skill, or MCP connection + +| What you need | Use | Starting point | +| --- | --- | --- | +| Reusable instructions that work through your existing tools | A skill | `/skills`, then `/skill `; [Skills](SKILLS.md) | +| A reviewed bundle of workflows, tools, commands or agent profiles | A plugin | `/plugin marketplace list`; [first-party catalog](https://github.com/codewhale-hq/codewhale-plugin-marketplace) | +| Tools or context from a particular local process or remote service | A direct MCP connection | `/mcp`; [MCP setup](MCP.md) | + +A plugin can combine skills, Markdown commands, Fleet agent profiles, hooks, +MCP servers and AI-provider declarations. Tools come from an MCP server or an +experimental native extension; there is no generic application-UI component in +the bundle manifest. Computer Use's Mac helper and Chromewhale's browser +extension have their own setup after plugin review. + +MCP has two connection kinds: a local process over stdio, or a remote URL over +Streamable HTTP. Legacy SSE is available when the server still supports it. +User-configured remote servers can use environment-backed credentials or the +host's OAuth login. Plugin-contributed remote servers require explicit +environment-backed credentials when authentication is needed; their interactive +OAuth path is not supported. Installing a skill or plugin does not connect an +account or establish that a service works. + +A local stdio server runs with your operating-system user's access. Requested +filesystem roots in a plugin manifest do not sandbox that process. + +For example, configure [Cloudflare's public documentation server](https://developers.cloudflare.com/changelog/post/2026-10-22-cloudflare-mcp-servers-retire-sse/) +directly, or install the `cloudflare-docs` plugin when you also want its skill: + +```sh +codewhale mcp add cloudflare-docs --url https://docs.mcp.cloudflare.com/mcp +codewhale mcp connect cloudflare-docs +codewhale mcp tools cloudflare-docs +``` + +Choose one setup for that server. `connect` is a diagnostic connection; it does +not attach tools to an already running session. In an existing terminal session, +run `/mcp reload` after changing configuration. + +## Bundle compatibility + +| Format | Supported boundary | +| --- | --- | +| Native `plugin.json` or legacy `plugin.toml` | Reviewed declarative adapters above; [manifest contract](PLUGIN_BUNDLES.md) | +| Claude `.claude-plugin/plugin.json` | Skills, commands, agent profiles and MCP subset; hooks, LSP, custom MCP paths and plugin-root expansion are rejected; [compatibility details](CLAUDE_PLUGIN_COMPAT.md) | +| `kimi.plugin.json` | Compatible skills, commands, agents and MCP declarations; unsupported runtime fields fail closed | +| Local DSH package | Explicit preview and exact-hash import through `/plugin import dsh`; experimental native runtime, not blanket DSH compatibility or release qualification; [current evidence and limits](DSH_PLUGIN_COMPAT.md) | + +Native TypeScript extensions require `extension_host = true` under `[features]` +and fresh capability review. LSP, requested filesystem roots and lifecycle mutation +remain inventory-only. Current DSH adapters include work beyond the published +v0.10.2 CLI; third-party DSH plugins' complete workflows are not established by +an import succeeding. DSH plugin UI is unsupported. Other applications' plugin +directories and credentials are not borrowed. + `/plugin suggest ` is a local, read-only companion: it ranks already installed bundles (name, keywords, description, bundled skill names, declared -hosts) and any marketplace catalogs you have added with `/plugin marketplace add`. +hosts) and available marketplace catalogs, including the bundled snapshot and +catalogs you have added with `/plugin marketplace add`. It explains the match and gives the next review, enable, or catalog-install step, but never installs, trusts, or enables a bundle on its own. @@ -34,8 +89,10 @@ Codewhale is helpful about plugins, not pushy. The rules: `request_plugin_install` once per session to ask you to review a plugin the task needs; a second call fails. Exec, ACP, and runtime-API sessions do not get the tool. -- **Built-ins are never advertised.** Bundled plugins such as Computer Use - appear only in `/plugin list` and Extensions. +- **Built-ins are never advertised.** Inspect existing bundles in `/plugin list` + and Extensions before installing another copy. The published v0.10.2 CLI + includes Computer Use; its layout differs from the current source candidate. + See [Computer Use across versions](PLUGIN_MARKETPLACE.md#computer-use-across-versions). - **Specific terms only.** Generic words (accessibility, browser, chrome, docs, screenshot, web, wiki, …) and Codewhale's own vocabulary (agent, code, data, MCP, model, session, tool) never trigger an offer, so asking why an @@ -57,7 +114,7 @@ Codewhale is helpful about plugins, not pushy. The rules: `/plugin marketplace list`, Extensions, and the browser guide below. Codewhale does not invent a remote plugin URL; missing plugins are suggested -only from catalogs you added. On-disk bundle changes still toast +only from available catalogs. On-disk bundle changes still toast `/plugin reload` on send and between turns. ## Browser: pick one @@ -69,7 +126,7 @@ what it can see. | --- | --- | --- | | `chrome-devtools` MCP (`/mcp recommendations`) | A Chrome it drives, which can include signed-in pages | DevTools-level inspection and performance work | | Playwright MCP (`/mcp recommendations`) | A fresh, isolated profile with `--isolated` | Scripted flows and testing without your identity | -| Computer Use `browser_*` tools (bundled, off until reviewed) | One it launches, in a profile of its own | Browser steps inside a wider desktop task | +| Computer Use browser tools (off until reviewed) | One it launches, in a profile of its own | Browser steps inside a wider desktop task | | Chromewhale (developer preview, `codewhale-hq/codewhale-plugin-marketplace`) | Yours, already open, in your own Chrome profile; load unpacked | Reading or acting on the tab you are looking at, one granted site at a time | None of these is offered to you proactively. Add the one that fits the job. @@ -168,10 +225,14 @@ matching and the plugin goes inactive until you review again. - `update` re-downloads the recorded source. Identical bytes are a no-op; a changed bundle is swapped atomically and its trust receipt is automatically invalidated (the hash no longer matches), so re-review is forced before the - plugin can activate again. Plugins installed from a local path cannot be - re-downloaded. To replace their installed copy, disable and uninstall it, - then run `/plugin install ` and review the new bundle; the original - source directory is left intact. See the [local authoring loop](PLUGIN_AUTHORING.md#4-iterate-and-review-changes). + plugin can activate again. For a local install, it stages a fresh copy of the + recorded source directory; keep that authoring directory available. Editing + the installed copy does not update its source. See the + [local authoring loop](PLUGIN_AUTHORING.md#4-iterate-and-review-changes). +- `update` retains the recorded repository revision, npm version, archive URL + and bundle selector. Refreshing a catalog does not retarget that source. To + select a newer catalog pin, follow + [Changing an installed pin](PLUGIN_MARKETPLACE.md#changing-an-installed-pin). - `uninstall` refuses enabled plugins (disable first), deletes the bundle directory, and removes its persisted trust/enablement entry. diff --git a/docs/PLUGIN_BUNDLES.md b/docs/PLUGIN_BUNDLES.md index a424be5d1..a5b64fa8d 100644 --- a/docs/PLUGIN_BUNDLES.md +++ b/docs/PLUGIN_BUNDLES.md @@ -420,8 +420,12 @@ and renders every candidate with an honest install plan: - Foreign policies are visibly ignored: a Codex `INSTALLED_BY_DEFAULT` entry is listed with a `NO_AUTO_INSTALL` warning and nothing is installed until an operator runs the install verb. -- Sources Codewhale cannot fetch (npm packages, `command:` sources, non-tarball - URLs) are listed as `not installable` with the reason. +- Catalog npm package declarations, `command:` sources and non-tarball URLs + are listed as `not installable` with the reason. Explicit + `/plugin install npm:package@1.2.3` and `codewhale install npm:package@1.2.3` + instead fetch an exact-version npm registry tarball containing a native plugin + manifest. They do not run npm lifecycle scripts or install dependencies; see + [install sources](PLUGINS.md#sources). - `install` routes through the same reviewed installer as `/plugin install`: the bundle lands disabled and untrusted, and enters the hash-bound trust review before anything activates. diff --git a/docs/PLUGIN_MARKETPLACE.md b/docs/PLUGIN_MARKETPLACE.md index 601f90eb2..d00305895 100644 --- a/docs/PLUGIN_MARKETPLACE.md +++ b/docs/PLUGIN_MARKETPLACE.md @@ -2,9 +2,10 @@ Codewhale includes an offline snapshot of the `codewhale` catalog in the same marketplace store consumed by the terminal, Extensions, recommendations, and -Runtime API. It lists Computer Use, WhaleWiki, Whalesong, Cloudflare Docs, the -Codewhale skill bundle, and Codewhale for Chrome (Chromewhale). Browsing does -not fetch or execute anything. +Runtime API. Browse the [public first-party marketplace](https://github.com/codewhale-hq/codewhale-plugin-marketplace) +for current entries and their prerequisites. Its contents can be newer than +the snapshot in your installed CLI. Browsing a catalog does not fetch or +execute a plugin. ```text /plugin marketplace list @@ -12,11 +13,11 @@ not fetch or execute anything. /plugin marketplace install codewhale whalewiki /plugin show whalewiki /plugin trust whalewiki -/plugin enable whalewiki -/plugin update whalewiki ``` -Review the manifest and capabilities before trust and enablement. Installation +The last command prints a review. Run its exact confirmation command containing +both full hashes, then `/plugin enable whalewiki`. Review the manifest and +capabilities before trust and enablement. Installation uses the existing size-limited, traversal-safe installer and starts disabled and untrusted. An update with changed bytes requires review again. Official catalog provenance grants no execution or network permission. Removing the @@ -30,22 +31,81 @@ ambiguous roots, links, oversized archives, and changed plugin identities are rejected. The install receipt preserves the source, including its selector, so `/plugin update` retains the same bundle selector and reviewed revision. -## Computer Use across upgrades +## Catalog freshness and pinned installs -Computer Use is an ordinary independently released plugin. Install it from -Connectors or `/plugin marketplace install codewhale computer-use`, review its -capabilities, then enable it. Install the signed Mac helper from -[Computer Use setup](https://codewhale.net/computer-use) for native permission -setup, trust and Pause/Stop controls. +The published v0.10.2 CLI contains the 19-entry catalog from marketplace revision +`2e3a6fba453c9b34d5b1444ab5bd6edc7b119189`. Current source and the public +marketplace include later additions, such as Office and Studio; that does not +make them part of the v0.10.2 snapshot. Use `/plugin marketplace show codewhale` +to inspect the catalog your session actually has. -The catalog uses the stable `computer-use.tar.gz` asset on the canonical -repository's latest release. `/plugin update computer-use` downloads that source; -changed bytes require review again. Later CU releases need no Engine rebuild. -Older Engine versions embedded a CU copy. Their snapshots and receipts remain -intact, but the current Engine does not discover them. Install and review the -ordinary plugin once; a built-in receipt cannot authorize a new downloaded -bundle. Existing manually installed or pinned bundles keep their normal source -and review rules. +The bundled catalog is a build-time snapshot. It does not poll the public +repository. To use a separately reviewed catalog revision, obtain a local copy +of the public marketplace, inspect its changes, then add its local document: + +```text +/plugin marketplace add codewhale /absolute/path/codewhale-plugin-marketplace/marketplace.json +/plugin marketplace show codewhale +``` + +Re-adding the same document refreshes its saved contents. If `codewhale` already +names a different local source, remove that catalog first with +`/plugin marketplace remove codewhale`, then add the intended document. Catalog +removal does not remove installed plugins or change their trust. + +Repository entries in the bundled snapshot pin an immutable marketplace commit. +Remote entries keep their declared URL and any archive checksum. The current +public Computer Use entry pins a versioned `computer-use.tar.gz` asset and its +SHA-256; it does not follow GitHub's latest-release address. `/plugin update` +re-reads the installed bundle's recorded source, including its pin. It does not +select a new source from a refreshed catalog. + +### Changing an installed pin + +For a user-scope bundle installed by Codewhale, retain its original source and +any local changes before replacing it: uninstall deletes the installed copy. +After refreshing and reviewing the catalog, this example selects its current +WhaleWiki entry: + +```text +/plugin show whalewiki +/plugin disable whalewiki +/plugin uninstall whalewiki +/plugin marketplace install codewhale whalewiki +/plugin trust whalewiki +``` + +Run the exact hash-bound confirmation printed by the new review, then +`/plugin enable whalewiki`. Do not apply this sequence to built-in or workspace +bundles: update and uninstall accept only user-scope installed bundles. A local +catalog's `path:` source records that local directory, so keep it available for +future `/plugin update` calls. A changed source still needs review and does not +silently inherit an earlier receipt. + +## Computer Use across versions + +For the current helper with published CLI v0.10.2, follow the [direct MCP setup](https://github.com/codewhale-hq/codewhale-cu-plugin#codewhale-cli-setup), which uses the app’s bundled runtime. Disable an enabled older built-in before using that connection. Direct MCP supplies tools separately from plugin commands and skills. + +The published v0.10.2 CLI embeds Computer Use 0.12.1. To inspect that older bundle, start with +`/plugin show computer-use`, review with `/plugin trust computer-use`, run the +printed hash-bound confirmation, then `/plugin enable computer-use`. A catalog +install of the same name reports that the built-in already exists; it does not +replace it with a newer release. Direct installation of a second bundle with +that name is refused. Built-in bundles cannot be updated or uninstalled through +the user-plugin commands. + +The current Engine source candidate no longer creates or discovers the embedded +copy. On that layout, install `computer-use` from the catalog as an ordinary +user plugin, then review, trust and enable it. Its old snapshots and receipts +remain intact; a built-in receipt cannot authorize the downloaded bundle. +This source transition does not establish a published CLI migration path. + +Computer Use has its own [published releases](https://github.com/codewhale-hq/codewhale-cu-plugin/releases) +and [contribution guide](https://github.com/codewhale-hq/codewhale-cu-plugin/blob/main/CONTRIBUTING.md). +Install the native Mac helper separately using the +[setup guide](https://codewhale.net/computer-use) for system permissions and +Pause/Stop controls. A helper release, a plugin bundle and an Engine release +have separate versions; their presence alone does not qualify a combination. ## Chromewhale @@ -63,7 +123,7 @@ until you review it. | --- | --- | --- | | Catalog, WhaleWiki, Whalesong, Cloudflare Docs | `codewhale-hq/codewhale-plugin-marketplace` | Core catalog snapshot | | Bundled skills | Core active catalog and `crates/tui/assets/skills` | Marketplace `skills` and `skills/upstream.json` | -| Computer Use | `codewhale-hq/codewhale-cu-plugin` | Stable release archive; no runtime mirrors | +| Computer Use | `codewhale-hq/codewhale-cu-plugin` | Versioned release archive and checksum in the catalog; no current runtime mirror | The `Marketplace connection` workflow validates the exact catalog revision on catalog changes. Its weekly and manual runs compare the current public @@ -94,9 +154,9 @@ npm test && npm run check:web Review the generated snapshot and rebuild Core. Its provenance records the exact marketplace commit. Local bundle URLs pin that immutable revision; remote -entries retain their declared source. Updating catalog metadata requires a new -Core snapshot; updating a remote plugin at its stable release URL does not. An -existing pinned install does not silently follow `main`. Push the +entries retain their declared source. Updating the bundled catalog requires a +new Core snapshot; users may explicitly add a reviewed local catalog without +rebuilding Core. Neither change retargets an existing pinned install. Push the reviewed marketplace revision before publishing a Core release that references it. Hosted CI must be green for the actual published revisions; local checks do not prove a public URL works. diff --git a/docs/zh_hans/MCP.md b/docs/zh_hans/MCP.md index 1c5bdeb80..a1d6c97a3 100644 --- a/docs/zh_hans/MCP.md +++ b/docs/zh_hans/MCP.md @@ -1,10 +1,10 @@ # MCP(外部工具服务器) -> 本文翻译自英文版 [MCP.md](../MCP.md),与英文源文件同步于 2026-09-29。 +> 本文依据英文版 [MCP.md](../MCP.md),包含 main 候选版本的行为。截至 2026 年 11 月 7 日,最新公开 CLI 为 [v0.10.2](https://github.com/codewhale-hq/Codewhale/releases/tag/v0.10.2)。基本 stdio、HTTP/SSE、直接配置的远程 OAuth 和插件 MCP 已在该版本提供;SDK 默认后端、现代协议、凭据绑定和目录扩展等差异请参阅英文版[版本对照](../MCP.md#version-scope)。使用已安装的 v0.10.2 时,可查阅[对应版本指南](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MCP.md)。 在终端中,`/mcp`(也可用 `/mcps`)会打开 **Extensions → MCP**。按 Enter 会对所选服务器执行恢复操作,或打开只读详情。清单为空时会提供服务器建议;浏览这些建议不会安装任何内容。`/mcp status`、`/mcp doctor`、`/mcp login`、`/mcp add` 等显式子命令的行为保持不变。 -codewhale 可以通过 MCP(Model Context Protocol,模型上下文协议)加载额外工具。MCP 服务器可以是 TUI 启动的本地 stdio 进程,也可以是使用 Streamable HTTP(带传统 SSE 回退)的远程基于 URL 的服务器。 +Codewhale 可以通过 MCP(Model Context Protocol,模型上下文协议)加载额外工具。MCP 服务器可以是 Codewhale 启动的本地 stdio 进程,也可以是使用 Streamable HTTP(带传统 SSE 回退)的远程基于 URL 的服务器。 浏览说明: - `Web` 是规范的、延迟加载的内置浏览工具;在网络策略允许时,它提供 `search`、`fetch` 和 `wait` 操作。 @@ -60,9 +60,11 @@ codewhale 可以通过 MCP(Model Context Protocol,模型上下文协议) ## 插件贡献的 MCP -通过审查的本地插件包可以贡献 MCP 服务器,而无需创建第二套传输或审批系统。这些服务器使用本文档记载的同一条 MCP 管理器、工具审批、资源、提示、超时和网络策略路径,并以命名空间化的 `-` 身份出现。 +通过审查的本地插件包可以贡献 MCP 服务器,而无需创建第二套传输或审批系统。这些服务器使用本文档记载的同一条 MCP 管理器、工具审批、资源、提示、超时和网络策略路径,并以避免身份冲突的 `plugin---` 命名。 -插件包边界有意比用户编写的 `mcp.json` 更严格:未知字段和模糊的传输方式默认失败关闭;stdio 环境变量值必须是精确的环境来源引用;远程字面量请求头和含机密信息的 URL 会被拒绝;声明的网络主机必须与规范化后的端点主机集合完全匹配;重定向保持在已审查的源站内。已审查的插件远程端点还会完全绕过环境中的 HTTP 代理配置;代理凭据和代理可见流量不属于 v1 审查范围。插件审查会披露本地主机用户权限、结构化 argv、环境来源、端点、认证来源名称、作用域和工具过滤器,但不会读取或打印机密值。 +认证范围与用户直接编写的 MCP 配置不同:插件远程服务器可使用 `env_headers` 或 `bearer_token_env_var`,但 v0.10.2 和 main 候选版本均不支持插件提供的 MCP OAuth。插件 MCP 清单声明 OAuth、作用域或 OAuth 资源时会验证失败。需要 OAuth 时,请直接在你自己的 `mcp.json` 中配置该远程服务器。 + +插件包边界有意比用户编写的 `mcp.json` 更严格:未知字段和模糊的传输方式默认失败关闭;stdio 环境变量值必须是精确的环境来源引用;远程字面量请求头和含机密信息的 URL 会被拒绝;声明的网络主机必须与规范化后的端点主机集合完全匹配;重定向保持在已审查的源站内。已审查的插件远程端点还会完全绕过环境中的 HTTP 代理配置;代理凭据和代理可见流量不属于 v1 审查范围。插件审查会披露本地主机用户权限、结构化 argv、环境来源、端点、认证来源名称和工具过滤器,但不会读取或打印机密值。 信任(Trust)会暂存已审查的内容,但不会启用它。启用操作会把该暂存快照附加到当前工作区的 MCP 池。禁用、撤销以及其他跨进程的代际变更会移除目录条目、取消进行中的操作,并终止插件的 stdio 子进程。在每次分派/目录边界之前,源码或暂存树的漂移都会被完全重新验证,并让下一个边界失败关闭;v0.9.1 不会在已经运行的调用期间持续对可变树做哈希,因此不承诺由漂移触发的调用中途取消。MCP 订阅不会通过插件包暴露。完整的生命周期契约请参阅[插件包](./PLUGIN_BUNDLES.md)。 @@ -92,6 +94,8 @@ codewhale mcp remove codewhale mcp validate ``` +终端中的 `codewhale mcp validate` 会在自己的进程中启动并连接已启用且获准的服务器,报告连接或发现失败;它不是静态检查,也不会把连接附加到正在运行的 TUI。TUI 中的 `/mcp validate` 只报告最近记录的快照,不会重新连接;`/mcp reload` 才会重建该会话的实时连接池。`codewhale doctor` 默认提供静态诊断,除非显式请求实际探测。 + `codewhale mcp logout `(以及 `/mcp logout`)只会清除本地存储的 OAuth 凭据——提供商可能仍保留其既有授权。下一次登录会强制显示授权确认页,因此可以更换所授权的账号/工作区;若要在远端彻底撤销授权,请在提供商的账号设置中撤销该应用。 ## TUI 内管理器 @@ -164,7 +168,7 @@ v0.9.10 的产品建议使用这些经过审查的固定版本定义。Plugins ## 远程 HTTP 认证 -基于 URL 的 MCP 服务器可以使用静态请求头、环境变量派生的请求头、bearer-token 环境变量或 OAuth。授权优先级是保守的: +直接配置的基于 URL 的 MCP 服务器可以使用静态请求头、环境变量派生的请求头、bearer-token 环境变量或 OAuth。插件远程服务器只支持基于环境变量的请求头或 bearer token,不支持 MCP OAuth;参阅[插件贡献的 MCP](#插件贡献的-mcp)。直接配置的服务器按以下优先级选择认证: 1. `headers` 和 `env_headers` 首先应用。 2. 若 `Authorization` 头尚未设置,`bearer_token_env_var` 会添加 `Authorization: Bearer `。 @@ -243,15 +247,13 @@ Codewhale 同时读取 `servers` 和 `mcpServers`,因此设置页生成的片 "servers": { "huggingface": { "url": "https://huggingface.co/mcp", - "headers": { - "Authorization": "Bearer ${HF_TOKEN}" - } + "bearer_token_env_var": "HF_TOKEN" } } } ``` -上面的占位符不是可运行的机密。请在私有 MCP 配置中使用设置页生成的值,并且永远不要提交真实的 Hugging Face 令牌。 +请在启动 Codewhale 的环境中设置 `HF_TOKEN`;上例只保存变量名。字面量 `headers` 值不会展开 `${HF_TOKEN}`。按需调整设置页生成的服务器条目,不要提交真实令牌。 交互式辅助命令: @@ -274,7 +276,11 @@ Codewhale 同时读取 `servers` 和 `mcpServers`,因此设置页生成的片 覆盖方式: - 配置项:`mcp_config_path = "/path/to/mcp.json"` -- 环境变量:`DEEPSEEK_MCP_CONFIG=/path/to/mcp.json` +- 环境变量:`CODEWHALE_MCP_CONFIG=/absolute/path/to/mcp.json`,优先于旧名 `DEEPSEEK_MCP_CONFIG`。 + +自定义路径必须是绝对路径;相对 `mcp_config_path` 会回退到用户全局路径,不会随启动目录改变。 + +工作区在用户拥有的配置中获准信任后,Codewhale 还会读取 `/.codewhale/mcp.json`。项目中的同名服务器覆盖全局条目;项目文件不能自行授予信任,也不能覆盖全局 `mcp_config_path`。全局或受信任项目中的显式条目优先于身份冲突的插件服务器。项目 stdio 命令默认以工作区为工作目录。main 候选版本中的 `always_allow_tools` 仅接受全局服务器配置,不接受项目或插件声明。 `codewhale mcp init`(以及 `codewhale setup --mcp`)会写入这个解析出的路径。 @@ -321,11 +327,15 @@ Codewhale 同时读取 `servers` 和 `mcpServers`,因此设置页生成的片 } ``` -为了与其他客户端兼容,你也可以使用 `mcpServers` 而不是 `servers`。 +为了与其他客户端兼容,你也可以使用 `mcpServers` 而不是 `servers`,但同一文件只能使用其中一个,同时出现会解析失败。`env_http_headers` 是 `env_headers` 的别名。 ## 将 Codewhale 作为 MCP 服务器运行 -你可以把本地 Codewhale 二进制注册为 MCP 服务器,这样其他 Codewhale 会话(或任何 MCP 客户端)就能调用它的工具。 +你可以把本地 Codewhale 二进制注册为 stdio MCP 工具服务器,供其他 Codewhale 会话或支持其声明协议版本的客户端使用。这与让 Codewhale 连接外部 MCP 服务器是两个方向。 + +### 客户端配置与服务器配置 + +`mcp.json` 告诉客户端启动或连接哪些服务器。原生 `codewhale serve --mcp` 则从保留的旧版路径 `~/.deepseek/mcp_server.toml` 读取 `[server]` 配置,默认仅暴露 `file_read` 和 `search`。写入文件或执行命令,需要在 `expose_tools` 中列出对应工具,并显式设置 `require_approval = false`。这个 stdio 服务器没有交互式审批通道;要求审批时,写入和执行工具会被隐藏并拒绝调用。 ### 快速设置 @@ -431,7 +441,7 @@ codewhale mcp tools codewhale 每个服务器的设置: -- `command`(字符串,必需) +- `command`(字符串,本地 stdio 服务器必需):要启动的程序。远程服务器改用 `url`,只有 URL 的条目同样有效。 - `args`(字符串数组,可选) - `env`(对象,可选) - `connect_timeout`、`execute_timeout`、`read_timeout`(秒,可选)。服务器级的值会覆盖全局 `timeouts` 块,三个预算彼此独立: diff --git a/docs/zh_hans/PLUGINS.md b/docs/zh_hans/PLUGINS.md index e29797879..1bafc197d 100644 --- a/docs/zh_hans/PLUGINS.md +++ b/docs/zh_hans/PLUGINS.md @@ -2,6 +2,7 @@ > 英文原文:[PLUGINS.md](../PLUGINS.md)。 > 最后与英文同步日期(last synced with English revision):2026-09-26。 +> 2026-11-07 定点更正:版本范围、目录与固定来源、信任确认、本地更新;未做全文重译。 想做一个插件包,先从[编写你的第一个 Codewhale 插件](./PLUGIN_AUTHORING.md) 和里面可运行的 Skills 示例入手。 @@ -12,9 +13,14 @@ 兼容的 `kimi.plugin.json`、`.claude-plugin/plugin.json`,或旧版 `plugin.toml`。 本文讲的是文件最初怎么落盘。 +版本范围:已发布的 CLI v0.10.2 内置 Computer Use 0.12.1;当前 Engine 源码候选版 +已改为单独安装的普通插件。刷新目录或安装新版 Mac 助手,不会替换旧 CLI 的内置包。 +完整的版本差异、目录时效和更换固定版本的步骤,见 +[英文市场指南](../PLUGIN_MARKETPLACE.md#catalog-freshness-and-pinned-installs)。 + `/plugin suggest ` 是一个本地、只读的配套命令:它会按名称、关键词、 -描述、所含 skill 名、声明的主机,给已安装的插件包排序,也给你用 -`/plugin marketplace add` 添加的市场目录排序。它还会说明匹配的原因, +描述、所含 skill 名、声明的主机,给已安装的插件包与可用目录里的候选项排序, +包括内置目录快照和你用 `/plugin marketplace add` 添加的目录。它还会说明匹配的原因, 并给出下一步:审查、启用,或从目录安装。但它自己从不安装、信任或启用插件包。 ## Codewhale 如何推荐插件 @@ -32,17 +38,17 @@ Codewhale 在插件上只帮忙,不推销。规则如下: 在交互式 TUI 里,模型每个会话可以调用一次 `request_plugin_install`, 请你审查任务需要的某个插件;第二次调用会失败。 Exec、ACP 和 runtime-API 会话拿不到这个工具。 -- **内置插件从不作为推荐出现。** Computer Use 这类内置插件, - 只会出现在 `/plugin list` 和 Extensions 里。 +- **内置插件从不作为推荐出现。** 先在 `/plugin list` 和 Extensions 检查已有插件。 + 在 v0.10.2 上,对 Computer Use 执行目录安装只会提示内置包已存在,不会升级它; + 应审查、信任并启用已有包。安装独立 Computer Use 包的方式适用于当前源码候选版的新布局。 - **只认具体词。** 泛化词(accessibility、browser、chrome、docs、screenshot、 web、wiki 等)和 Codewhale 自己的词汇(agent、code、data、MCP、model、 session、tool)永远不触发推荐,所以问 MCP 服务器为什么卡住,不会冒出某个 厂商的插件。有辨识度的名称、短语和域名照常匹配。匹配器和市场的 `check-marketplace.mjs` 共用同一份停用词表。 - **只推荐本机跑得起来的。** 插件的 `when.os` 若排除了当前操作系统,就不会被推荐。 -- **给出真正的下一步。** 模型请求的那一行会按插件的实际需要写成 Install、 - Review trust 或 Enable。只有那个按钮可点,它打开的是 `/plugin show `; - 它从不安装、信任或启用。 +- **审查后再操作。** 模型请求的那一行提供 Review,打开 Extensions 中的插件或市场目录。 + 点击本身不会安装、信任或启用;仍需选择目录中的显式操作并审查已安装的内容。 - **关闭可以撤销。** 按 Esc 时,如果草稿非空,先清空草稿,然后才隐藏这一行, 而且只对当前会话生效。“Don't suggest again”是你显式做出的选择, 会被持久保存。`/plugin dismissals` 会列出这两种; @@ -50,7 +56,7 @@ Codewhale 在插件上只帮忙,不推销。规则如下: - **新插件要靠自己找。** 想找新插件,就看这些文档、`/plugin marketplace list`、 Extensions,以及下面的浏览器指南。 -Codewhale 不会凭空编造远程插件 URL;缺失的插件只会从你添加过的目录里推荐。 +Codewhale 不会凭空编造远程插件 URL;缺失的插件只会从可用目录里推荐。 磁盘上的插件包有变化时,发送消息时和回合之间仍会弹出 `/plugin reload` 的 toast。 ## 浏览器:选一个 @@ -61,14 +67,16 @@ Codewhale 不会凭空编造远程插件 URL;缺失的插件只会从你添加 | --- | --- | --- | | `chrome-devtools` MCP(`/mcp recommendations`) | 它自己驱动的一个 Chrome,可以包含已登录的页面 | DevTools 级别的检查和性能分析 | | Playwright MCP(`/mcp recommendations`) | 带 `--isolated` 的全新隔离 profile | 不带你身份的脚本化流程和测试 | -| Computer Use 的 `browser_*` 工具(内置,审查前关闭) | 它自己启动的浏览器,用独立的 profile | 更宽泛的桌面任务里的浏览器步骤 | +| Computer Use 的浏览器工具(审查前关闭) | 它自己启动的浏览器,用独立的 profile | 更宽泛的桌面任务里的浏览器步骤 | | Chromewhale(开发者预览版,`codewhale-hq/codewhale-plugin-marketplace`) | 你自己的、已经打开的 Chrome profile;以 unpacked 方式加载 | 读取或操作你当前正看的标签页,每次授权一个站点 | 这些都不会主动推荐给你。按任务需要自己添加。 ## 来源 -`/plugin install ` 接受三种来源: +`/plugin install ` 的常用来源如下。完整来源和限制见 +[英文来源说明](../PLUGINS.md#sources),包括指定版本的 GitHub 与 npm tarball; +`codewhale install ` 是使用同一安装器的 shell 入口,不需要模型凭据。 ```text /plugin install ./path/to/bundle # local directory (copied) @@ -122,6 +130,9 @@ Claude 的 `http` 传输映射为 Streamable HTTP。 这里渲染的审查内容和确认 token 与 `/plugin trust ` 完全相同。 信任走的是严格的、绑定哈希的回执流程,不是一个仅供参考的标记。 +不带 token 的 `/plugin trust ` 只显示审查内容,不会授予信任。 +必须执行审查打印出的原样确认命令,保留内容哈希和能力哈希这两个完整值; +确认信任后,仍需单独执行 `/plugin enable `。 插件包的内容或声明的能力一旦变化,回执就不再匹配,插件随之失效,直到你重新审查。 ## 更新与卸载 @@ -134,10 +145,13 @@ Claude 的 `http` 传输映射为 Streamable HTTP。 - `update` 会重新下载记录在案的来源。字节完全相同就什么都不做; 有变化的插件包会被原子替换,其信任回执自动失效(哈希不再匹配), - 所以想让插件再次激活,必须重新审查。从本地路径安装的插件无法重新下载。 - 要替换已安装的副本,先停用再卸载,然后运行 `/plugin install ` - 并审查新的插件包;原来源目录保持不动。参见 + 所以想让插件再次激活,必须重新审查。本地安装则从记录的原来源目录重新暂存副本, + 因此应保留该目录;直接修改已安装副本不会修改原来源。参见 [本地编写循环](./PLUGIN_AUTHORING.md#4-修改并重新审查)。 +- `update` 保留记录中的仓库修订、npm 版本、归档 URL 和包选择路径。 + 刷新市场目录不会改变已安装包的来源。要换到更新的固定版本,见 + [英文更换步骤](../PLUGIN_MARKETPLACE.md#changing-an-installed-pin); + 该步骤仅适用于安装器管理的用户级插件,卸载前应保留原来源与本地修改。 - `uninstall` 拒绝卸载已启用的插件(先停用),删除插件包目录, 并移除它持久化的信任/启用记录。 diff --git a/web/app/[locale]/computer-use/page.tsx b/web/app/[locale]/computer-use/page.tsx index 3a40fabc0..15aabf9e9 100644 --- a/web/app/[locale]/computer-use/page.tsx +++ b/web/app/[locale]/computer-use/page.tsx @@ -1,3 +1,4 @@ +import Link from "next/link"; import { Icon } from "@/components/icon"; import { PageHeader, Section } from "@/components/page-header"; import { Status } from "@/components/status-badge"; @@ -44,17 +45,25 @@ export default async function ComputerUsePage({ params }: { params: Promise<{ lo {t.requirements} } - > - {release.status === "ready" && primary ? ( -
-
+ actions={ + <> + {release.status === "ready" && primary ? ( {t.download} - - v{release.version} · {Math.ceil(primary.size / 1024 / 1024)} MB - -
+ ) : null} + + {t.source} + + + + } + > + {release.status === "ready" && primary ? ( +
+ + v{release.version} · {Math.ceil(primary.size / 1024 / 1024)} MB +

{t.receipt} {release.dmg ? {t.downloadZip} : null} @@ -88,6 +97,22 @@ export default async function ComputerUsePage({ params }: { params: Promise<{ lo +

+

{t.connectVersion}

+

{t.connectLimit}

+

{t.connectRuntime}

+
{`APP="/Applications/Codewhale Computer Use.app"
+codewhale mcp add computer --command "$APP/Contents/MacOS/node" --arg "$APP/Contents/Resources/plugin/mcp/server.mjs"
+codewhale mcp tools computer`}
+

{t.connectReload}

+
{t.connectPrompt}
+

{t.connectExpected}

+

+ {t.connectGuide} + Computer Use +

+
+
@@ -119,13 +144,17 @@ export default async function ComputerUsePage({ params }: { params: Promise<{ lo

+ +
+

{t.contributeBody}

+ +
); diff --git a/web/app/[locale]/plugins/page.tsx b/web/app/[locale]/plugins/page.tsx index d3ba51bb1..dc007e68e 100644 --- a/web/app/[locale]/plugins/page.tsx +++ b/web/app/[locale]/plugins/page.tsx @@ -2,7 +2,7 @@ import Link from "next/link"; import { Icon } from "@/components/icon"; import { PageHeader, Section } from "@/components/page-header"; import { buildPageMetadata } from "@/lib/page-meta"; -import { PLUGINS_COPY, PLUGIN_COMMANDS } from "@/lib/content/plugins"; +import { PLUGINS_COPY, PLUGIN_COMMANDS, INTEGRATIONS_COPY, INTEGRATION_CHOICES, PLUGIN_EXAMPLES, PLUGIN_COMPATIBILITY } from "@/lib/content/plugins"; import type { LocalizedText } from "@/lib/content/vocabulary"; import { pickText } from "@/lib/i18n/dictionaries"; @@ -48,10 +48,20 @@ export default async function PluginsPage({ params }: { params: Promise<{ locale />
+
+
+ {INTEGRATION_CHOICES.map((choice) => ( + +

{t(choice.title)}

+

{t(choice.detail)}

+ + ))} +
+
-
{t(PLUGINS_COPY.cuInstall)}
+

{t(INTEGRATIONS_COPY.cuSetupNote)}

{t(PLUGINS_COPY.cuToolsNote)}

Computer Use @@ -59,7 +69,7 @@ export default async function PluginsPage({ params }: { params: Promise<{ locale
- {PLUGINS_COPY.cuFeatures.map((feature) => ( + {PLUGINS_COPY.cuFeatures.slice(0, 2).map((feature) => (
{t(feature.title)}
{t(feature.detail)}
@@ -72,14 +82,14 @@ export default async function PluginsPage({ params }: { params: Promise<{ locale
{t(PLUGINS_COPY.catalogBrowse)}
    - {PLUGINS_COPY.catalogEntries.map((entry) => ( -
  • - + {PLUGIN_EXAMPLES.map((entry) => ( +
  • + - {t(entry.title)} + {entry.title} {t(entry.detail)} - {entry.reference} + {`/plugin marketplace install codewhale ${entry.name}`} @@ -89,6 +99,25 @@ export default async function PluginsPage({ params }: { params: Promise<{ locale

    {t(PLUGINS_COPY.catalogNote)}

+
+
{`codewhale mcp add cloudflare-docs --url "https://docs.mcp.cloudflare.com/mcp"
+codewhale mcp tools cloudflare-docs`}
+ + {t(INTEGRATIONS_COPY.mcpLink)} + +
+ +
+
+ {PLUGIN_COMPATIBILITY.map((format) => ( +
+
{t(format.title)}
+
{t(format.detail)}
+
+ ))} +
+
+
    {PLUGINS_COPY.sources.map((source) => ( @@ -119,6 +148,13 @@ export default async function PluginsPage({ params }: { params: Promise<{ locale

    {t(PLUGINS_COPY.trustNote)}

+
+
+ {t(PLUGINS_COPY.docsCta)} + {t(INTEGRATIONS_COPY.catalogLink)} +
+
+
{PLUGIN_COMMANDS.join("\n")}
diff --git a/web/data/agent-docs.generated.json b/web/data/agent-docs.generated.json index 103d8da1c..84540b6ef 100644 --- a/web/data/agent-docs.generated.json +++ b/web/data/agent-docs.generated.json @@ -32,8 +32,8 @@ "sources": [ { "path": "docs/CONFIGURATION.md", - "sha256": "dccb4bcfb41e3d04abb256237817b6ef6ac310fc7c57f5f8829c62a9a7479092", - "content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get ` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify `,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\nNew Codewhale accounts are not open, and this guide does not recommend storing\nprovider keys in an account. Keep keys on your machine with `/provider` or\n`codewhale auth set --provider `; local keys and local models never\nneed an account.\n\nIf you already have an account, these commands manage keys saved there:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `codewhale account status`\ninspects it; `codewhale account logout` removes it for the selected `--profile`\nwithout removing your provider keys. Account sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `.bundle-backup--`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"\"` or pass\n`codewhale --provider `. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"\"`,\n`--provider `, or an AgentProfile `provider = \"\"` when the matching\n`[providers.]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save `) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change ` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers..model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers..model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions//runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`/.codewhale/config.toml`, or legacy\n `/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/.json` (or\n `$CODEWHALE_HOME/themes/.json`) and are selected with\n `/theme custom:`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open `), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set `.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `...` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers..auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove ` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest `, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`, but rebuilding the model-visible MCP tool pool\n requires restarting the TUI.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots///.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact `, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs ` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note ` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note ` | Append a note (legacy shorthand) |\n| `/note add ` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show ` | Show the full note at number `n` |\n| `/note edit ` | Replace note `n` with new text |\n| `/note remove ` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"\", \"event\": {…}}` with\n`Authorization: Bearer ` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `//control.sock` (mode `0600`;\n`` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach ` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach ` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n" + "sha256": "61fac67413aac35a4a67f410e174122a4ee09befbe2f2248fd3f9a6e4f08d99a", + "content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get ` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify `,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\nNew Codewhale accounts are not open, and this guide does not recommend storing\nprovider keys in an account. Keep keys on your machine with `/provider` or\n`codewhale auth set --provider `; local keys and local models never\nneed an account.\n\nIf you already have an account, these commands manage keys saved there:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `codewhale account status`\ninspects it; `codewhale account logout` removes it for the selected `--profile`\nwithout removing your provider keys. Account sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `.bundle-backup--`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"\"` or pass\n`codewhale --provider `. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"\"`,\n`--provider `, or an AgentProfile `provider = \"\"` when the matching\n`[providers.]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save `) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change ` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers..model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers..model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.
]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions//runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`/.codewhale/config.toml`, or legacy\n `/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/.json` (or\n `$CODEWHALE_HOME/themes/.json`) and are selected with\n `/theme custom:`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open `), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set `.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `...` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers..auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove ` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest `, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`; run `/mcp reload` to rebuild the model-visible\n MCP tool pool without restarting the TUI. A headless runtime still needs a\n restart because its `ConfigReload` request does not refresh MCP connections.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots///.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact `, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs ` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note ` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note ` | Append a note (legacy shorthand) |\n| `/note add ` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show ` | Show the full note at number `n` |\n| `/note edit ` | Replace note `n` with new text |\n| `/note remove ` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"\", \"event\": {…}}` with\n`Authorization: Bearer ` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `//control.sock` (mode `0600`;\n`` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach ` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach ` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n" }, { "path": "docs/CODEWHALE_AGENT.md", @@ -64,8 +64,8 @@ "sources": [ { "path": "docs/CONFIGURATION.md", - "sha256": "dccb4bcfb41e3d04abb256237817b6ef6ac310fc7c57f5f8829c62a9a7479092", - "content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get ` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify `,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\nNew Codewhale accounts are not open, and this guide does not recommend storing\nprovider keys in an account. Keep keys on your machine with `/provider` or\n`codewhale auth set --provider `; local keys and local models never\nneed an account.\n\nIf you already have an account, these commands manage keys saved there:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `codewhale account status`\ninspects it; `codewhale account logout` removes it for the selected `--profile`\nwithout removing your provider keys. Account sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `.bundle-backup--`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"\"` or pass\n`codewhale --provider `. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"\"`,\n`--provider `, or an AgentProfile `provider = \"\"` when the matching\n`[providers.]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save `) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change ` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers..model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers..model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.
]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions//runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`/.codewhale/config.toml`, or legacy\n `/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/.json` (or\n `$CODEWHALE_HOME/themes/.json`) and are selected with\n `/theme custom:`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open `), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set `.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `...` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers..auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove ` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest `, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`, but rebuilding the model-visible MCP tool pool\n requires restarting the TUI.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots///.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact `, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs ` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note ` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note ` | Append a note (legacy shorthand) |\n| `/note add ` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show ` | Show the full note at number `n` |\n| `/note edit ` | Replace note `n` with new text |\n| `/note remove ` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"\", \"event\": {…}}` with\n`Authorization: Bearer ` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `//control.sock` (mode `0600`;\n`` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach ` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach ` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n" + "sha256": "61fac67413aac35a4a67f410e174122a4ee09befbe2f2248fd3f9a6e4f08d99a", + "content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get ` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify `,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\nNew Codewhale accounts are not open, and this guide does not recommend storing\nprovider keys in an account. Keep keys on your machine with `/provider` or\n`codewhale auth set --provider `; local keys and local models never\nneed an account.\n\nIf you already have an account, these commands manage keys saved there:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `codewhale account status`\ninspects it; `codewhale account logout` removes it for the selected `--profile`\nwithout removing your provider keys. Account sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `.bundle-backup--`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"\"` or pass\n`codewhale --provider `. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"\"`,\n`--provider `, or an AgentProfile `provider = \"\"` when the matching\n`[providers.]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save `) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change ` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers..model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers..model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.
]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions//runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`/.codewhale/config.toml`, or legacy\n `/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/.json` (or\n `$CODEWHALE_HOME/themes/.json`) and are selected with\n `/theme custom:`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open `), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set `.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `...` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers..auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove ` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest `, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`; run `/mcp reload` to rebuild the model-visible\n MCP tool pool without restarting the TUI. A headless runtime still needs a\n restart because its `ConfigReload` request does not refresh MCP connections.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots///.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact `, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs ` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note ` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note ` | Append a note (legacy shorthand) |\n| `/note add ` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show ` | Show the full note at number `n` |\n| `/note edit ` | Replace note `n` with new text |\n| `/note remove ` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"\", \"event\": {…}}` with\n`Authorization: Bearer ` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `//control.sock` (mode `0600`;\n`` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach ` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach ` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n" }, { "path": "docs/LEGACY_PATHS.md", @@ -96,8 +96,8 @@ }, { "path": "docs/CONFIGURATION.md", - "sha256": "dccb4bcfb41e3d04abb256237817b6ef6ac310fc7c57f5f8829c62a9a7479092", - "content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get ` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify `,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\nNew Codewhale accounts are not open, and this guide does not recommend storing\nprovider keys in an account. Keep keys on your machine with `/provider` or\n`codewhale auth set --provider `; local keys and local models never\nneed an account.\n\nIf you already have an account, these commands manage keys saved there:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `codewhale account status`\ninspects it; `codewhale account logout` removes it for the selected `--profile`\nwithout removing your provider keys. Account sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `.bundle-backup--`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"\"` or pass\n`codewhale --provider `. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"\"`,\n`--provider `, or an AgentProfile `provider = \"\"` when the matching\n`[providers.]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save `) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change ` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers..model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers..model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.
]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions//runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`/.codewhale/config.toml`, or legacy\n `/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/.json` (or\n `$CODEWHALE_HOME/themes/.json`) and are selected with\n `/theme custom:`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open `), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set `.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `...` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers..auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove ` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest `, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`, but rebuilding the model-visible MCP tool pool\n requires restarting the TUI.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots///.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact `, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs ` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note ` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note ` | Append a note (legacy shorthand) |\n| `/note add ` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show ` | Show the full note at number `n` |\n| `/note edit ` | Replace note `n` with new text |\n| `/note remove ` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"\", \"event\": {…}}` with\n`Authorization: Bearer ` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `//control.sock` (mode `0600`;\n`` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach ` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach ` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n" + "sha256": "61fac67413aac35a4a67f410e174122a4ee09befbe2f2248fd3f9a6e4f08d99a", + "content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get ` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify `,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\nNew Codewhale accounts are not open, and this guide does not recommend storing\nprovider keys in an account. Keep keys on your machine with `/provider` or\n`codewhale auth set --provider `; local keys and local models never\nneed an account.\n\nIf you already have an account, these commands manage keys saved there:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `codewhale account status`\ninspects it; `codewhale account logout` removes it for the selected `--profile`\nwithout removing your provider keys. Account sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `.bundle-backup--`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"\"` or pass\n`codewhale --provider `. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"\"`,\n`--provider `, or an AgentProfile `provider = \"\"` when the matching\n`[providers.]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save `) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change ` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers..model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers..model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.
]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions//runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`/.codewhale/config.toml`, or legacy\n `/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/.json` (or\n `$CODEWHALE_HOME/themes/.json`) and are selected with\n `/theme custom:`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open `), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set `.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `...` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers..auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove ` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest `, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`; run `/mcp reload` to rebuild the model-visible\n MCP tool pool without restarting the TUI. A headless runtime still needs a\n restart because its `ConfigReload` request does not refresh MCP connections.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots///.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact `, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs ` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note ` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note ` | Append a note (legacy shorthand) |\n| `/note add ` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show ` | Show the full note at number `n` |\n| `/note edit ` | Replace note `n` with new text |\n| `/note remove ` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"\", \"event\": {…}}` with\n`Authorization: Bearer ` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `//control.sock` (mode `0600`;\n`` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach ` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach ` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n" } ] }, @@ -208,8 +208,8 @@ "sources": [ { "path": "docs/MCP.md", - "sha256": "a7413202ce16f0ea25de1a84acc5d1bdfdf6cd3f7f00ebbf6ccd7aa8a1dd493a", - "content": "# MCP (External Tool Servers)\n\nIn the terminal, `/mcp` (also `/mcps`) opens **Extensions → MCP**. Enter opens the selected server’s recovery action or read-only details. An empty inventory offers server suggestions; browsing them installs nothing. Explicit subcommands such as `/mcp status`, `/mcp doctor`, `/mcp login`, and `/mcp add` retain their existing behavior.\n\n\n> 阅读简体中文版:[zh_hans/MCP.md](zh_hans/MCP.md)\n\ncodewhale can load additional tools via MCP (Model Context Protocol). MCP servers can be local stdio processes that the TUI starts, or remote URL-based servers that speak Streamable HTTP with legacy SSE fallback.\n\nBrowsing note:\n- `Web` is the canonical, deferred built-in browsing tool; it provides\n `search`, `fetch`, and `wait` actions when network policy permits.\n- `web_search`, `fetch_url`, and `wait_for_dev_server` are hidden replay-only\n aliases. New prompts and integrations should use `Web`.\n\nServer mode note:\n- `codewhale serve --mcp` runs the MCP stdio server.\n- `codewhale serve --http` runs the runtime HTTP/SSE API (separate mode).\n- `codewhale mcp-server` is an equivalent stdio entrypoint on the same\n consolidated runtime.\n\nIn 0.10.1, the old child-server aggregation proxy is removed. `mcp-server`\nnow uses the same native tool server as `serve --mcp`; it does not launch the\nlegacy `mcp.server_definitions` list. Saved definitions are left intact. Configure\nexternal servers in `mcp.json` for the existing MCP client, or connect to those\nservers directly from your external client. Native server write tools remain\nwithheld unless the operator explicitly permits them in its server configuration.\n\nThe native stdio server supports MCP **2026-07-28** with required per-request\nmetadata and `server/discover`; no initialization handshake is needed for that\nrevision. It also supports the explicit legacy revisions **2024-11-05**,\n**2025-03-26**, **2025-06-18** and **2025-11-25** through `initialize`.\nInitialization never selects a modern revision; unsupported initialization\nversions fall back to 2025-11-25. Legacy callers should send\n`notifications/initialized`, but calls may proceed after a valid initialize\nresponse. This describes Codewhale as a server; outbound MCP client version\nsupport is configured separately.\n\n`resources/list` pages through the effective native skill instruction catalog,\nand `resources/read` returns the accepted instruction body for an exact listed\n`codewhale://skills/...` URI. Discovery follows the configured skill roots,\nprecedence and workspace trust; disabled skills are omitted. Reads supply inert\nMarkdown and never activate a skill. These are instruction-body resources, not\noriginal SKILL.md files or complete skill packages. Plugin skills, arbitrary\nfilesystem paths and session transcripts are not exposed by this catalog.\nThe old workspace/session entries, which had no resource reader, are removed.\nResource templates are empty. Lists and reads use private caching with a zero\nTTL; restart pagination if a changed catalog invalidates a cursor.\n\nOnly tools and resources are advertised. Subscriptions, list-change\nnotifications, the Skills extension, Apps, tasks and interactive approval are\nnot implemented by this server. Tool execution remains serialized through the\nRust registry, and request cancellation reaches the existing tool context.\nKeep stdin open until responses arrive; closing it cancels outstanding work.\nMalformed JSON and oversized input frames are discarded by the bounded SDK\ntransport, while well-formed invalid JSON-RPC receives an error.\n\n## Setup wizard vs manual MCP setup (#3407)\n\nThe `/setup` hub includes an optional **Tools and MCP**\nstep. That step is discovery/readiness only:\n\n| Wizard can do | Still requires manual / explicit action |\n| --- | --- |\n| Show configured servers as `healthy` / `needs_config` / `off` | Start or connect MCP servers |\n| Report config path presence (global + project) | Write or edit `mcp.json` contents |\n| Safe static health probe (missing command/url, broken absolute path, missing bearer env) | `codewhale mcp validate`, live connect, OAuth login |\n| Point at safe on-ramps (`/mcp`, `codewhale mcp init`, `codewhale doctor`) | Install community skills, trust skills, enable plugins |\n| Share Hotbar source counts from the same skill/MCP adapters (#3399) | Bind Hotbar slots (Hotbar step / `H`) |\n| Record optional/`needs_action` setup_state without blocking first-run | Anything that spawns processes or installs packages |\n\nEmpty inventory is **not** an error: first-run users see “nothing configured\nyet, that’s fine.” Failing or incomplete configured servers surface as\n`needs_config` with an actionable hint and never block setup completion.\nEnumeration never executes MCP/plugin commands beyond the static probe.\nSummaries redact commands, args, env, headers, and tokens.\n\n`codewhale doctor` reports MCP/skills/tools/plugins health with the same\noptional-surface intent (paths, counts, static checks) so wizard and doctor\nstay consistent.\n\n## Plugin-contributed MCP\n\nA reviewed local plugin bundle may contribute MCP servers without creating a\nsecond transport or approval system. The servers use the same MCP manager,\ntool approval, resource, prompt, timeout, and network-policy paths documented\nhere, and appear under namespaced `-` identities.\n\nThe bundle boundary is intentionally stricter than user-authored `mcp.json`:\nunknown fields and ambiguous transports fail closed; stdio environment values\nmust be exact environment-source references; remote literal headers and\nsecret-bearing URLs are rejected; declared network hosts must exactly match\nthe normalized endpoint host set; and redirects remain on the reviewed origin.\nReviewed plugin remotes also bypass ambient HTTP proxy configuration entirely;\nproxy credentials and proxy-observed traffic are not part of the v1 review.\nThe plugin review discloses local host-user authority, structural argv,\nenvironment provenance, endpoint, auth source names, scopes, and tool filters\nwithout reading or printing secret values.\n\nTrust stages reviewed content but does not enable it. Enablement attaches that\nstaged snapshot to the current workspace's MCP pool. Disable, revoke, and other\ncross-process generation changes remove catalog entries, cancel in-flight\noperations, and terminate plugin stdio children. Source or staged-tree drift is\nfully revalidated before each dispatch/catalogue boundary and fails the next\nboundary closed; v0.9.1 does not continuously hash mutable trees during an\nalready-running call and therefore does not promise drift-triggered mid-call\ncancellation. MCP subscriptions are not exposed through plugin bundles. See\n[Plugin bundles](PLUGIN_BUNDLES.md) for the complete lifecycle contract.\n\n## Bootstrap MCP Config\n\nCreate a starter MCP config at your resolved MCP path:\n\n```bash\ncodewhale mcp init\n```\n\n`codewhale setup --mcp` performs the same MCP bootstrap alongside skills setup.\n\nCommon management commands:\n\n```bash\ncodewhale mcp list\ncodewhale mcp tools [server]\ncodewhale mcp add --command \"\" --arg \"\"\ncodewhale mcp add --url \"http://localhost:3000/mcp\"\ncodewhale mcp add --url \"https://example.com/mcp\" --bearer-token-env-var MCP_TOKEN\ncodewhale mcp login \ncodewhale mcp logout \ncodewhale mcp enable \ncodewhale mcp disable \ncodewhale mcp remove \ncodewhale mcp validate\n```\n\n`codewhale mcp logout ` (and `/mcp logout`) clears locally stored\nOAuth credentials only — the provider may keep its standing grant. The next\nlogin forces the consent screen, so the authorized account/workspace can\nchange; to sever the grant remotely, revoke the app from the provider's\naccount settings.\n\n## In-TUI Manager\n\nInside the interactive TUI, `/mcp` opens a compact manager for the resolved\nMCP config path. It shows each configured server, whether it is enabled or\ndisabled, its transport, command or URL, timeout values, connection errors,\nand discovered tools/resources/prompts when discovery has been run.\n\nSupported in-TUI actions:\n\n```text\n/mcp init\n/mcp init --force\n/mcp import\n/mcp recommendations\n/mcp add recommended \n/mcp add stdio [args...]\n/mcp add http \n/mcp login [--scope scope]\n/mcp logout \n/mcp enable \n/mcp disable \n/mcp remove \n/mcp validate\n/mcp reload\n```\n\n### Suggested plugins and companion integrations\n\n`/mcp recommendations` is Codewhale's native, curated suggestions surface.\nThe entries are described as product plugins, with their component type and\nprovenance, but `/mcp add recommended ` still writes only the named MCP\nserver component. Viewing recommendations never fetches, installs, trusts, or\nenables anything. Adding one writes configuration; the server is first started\nonly after an explicit `/mcp restart`.\n\nThe v0.9.10 product suggestions use these reviewed, pinned definitions. The\nPlugins view is the product/install surface; MCP, Skills, and sandbox adapters\nare transparent component kinds and their own tabs remain operational and\ndiagnostic surfaces:\n\n| Plugin | Component | Pinned definition | Provenance and maturity | Installation boundary |\n| --- | --- | --- | --- | --- |\n| Chrome DevTools | MCP server (stdio) | `npx -y chrome-devtools-mcp@1.7.0` (`npx.cmd` on Windows) | [Official ChromeDevTools project](https://github.com/ChromeDevTools/chrome-devtools-mcp) | npm may download the pinned package when the user restarts MCP. |\n| Playwright | MCP server (stdio) | `npx -y @playwright/mcp@0.0.79 --isolated` (`npx.cmd` on Windows) | [Official Microsoft project](https://github.com/microsoft/playwright-mcp) | `--isolated` starts a fresh browser profile; npm may download the pinned package only after an explicit restart. |\n| Computer Use | First-party plugin (MCP + skill) | Install `computer-use` from the first-party catalog; updates independently | Codewhale; install, review and enable through Connectors or `/plugin` | This is the only computer-use integration Codewhale recommends. Third-party desktop-control MCPs are not listed here. |\n| Browser Use | Skill plus separately installed Python runtime | Skill/runtime release `0.13.8` | [Official browser-use project](https://github.com/browser-use/browser-use) | Optional companion: not an MCP server. Codewhale does not auto-run the upstream Skill installer or install its browser/runtime dependencies. |\n| Anthropic Sandbox Runtime | Sandbox adapter companion | `@anthropic-ai/sandbox-runtime@0.0.73` | [Official anthropic-experimental project](https://github.com/anthropic-experimental/sandbox-runtime); beta | Documentation-only adapter candidate in v0.9.10: not an MCP server and not an active Codewhale plugin adapter. It does not replace Codewhale's sandbox policy. |\n\n[Container Use](https://github.com/dagger/container-use) remains an additional\nexperimental suggestion with an MCP server component (`container-use stdio`).\nThe binary must be installed separately; `/mcp add recommended container-use`\nonly writes config and Codewhale never downloads it.\n\nThis presentation follows the same useful boundary found in the local\nGrokbuild extensions view (one product plugin may expose MCP or Skill\ncomponents while component tabs stay inspectable), the Kimi marketplace's\nexplicit display name/tier/source fields, and the Codex marketplace's explicit\nsource and install-policy fields.\nCodewhale keeps its stricter rule: provenance and foreign policy are display\nmetadata only, never inherited trust or automatic installation. For full\nbundle and marketplace semantics, see [Plugin bundles](PLUGIN_BUNDLES.md).\n\n`/mcp validate` (alias `/mcp doctor`) with no name reports each server's state\nfrom the last MCP snapshot the TUI recorded, in a pager over\n**Extensions → MCP**. That snapshot is usually the engine pool's latest report,\nbut some actions, such as `/mcp import`, replace it with a separate UI\ndiscovery pass that also connects `lazy` servers the engine has not started.\nValidate also re-reads the MCP config files without connecting: it\nreports a config load error first, lists configured servers the snapshot has\nnot observed, and says when a `/mcp reload` is pending. It does not reconnect\nanything or change the catalog the model gets; `/mcp reload` is the fresh\nprobe.\n\n`/mcp reload` (aliases `/mcp reconnect`, `/mcp restart`) is the hot-reload path.\nIt re-reads the MCP config sources and reconnects the servers session boot starts (see [Connection Lifecycle](#connection-lifecycle)) through the engine-owned pool,\nso the rebuilt catalog is the exact one the next model turn uses — no TUI\nrestart. Config edits made from the TUI are written immediately and the manager\nmarks the snapshot reload-required until you run it; a failed reload leaves the\nprevious live pool intact and says so.\n\nHeadless surfaces are the exception: the `ConfigReload` app-server request does\n**not** refresh MCP connections, so a headless runtime still needs a restart\nafter MCP config changes.\n\n## Remote network authority\n\nDirect HTTP/SSE requests to public hostnames validate every DNS answer and pin\nconnections to a public address. This also applies to configured servers,\nredirects, and OAuth HTTP requests. The configured network allow/deny policy\napplies to login and token refresh as well as MCP tool requests.\n\nA configured `localhost` name or private IP literal explicitly permits that\nlocal endpoint. For a private DNS name, opt in on the server configuration:\n\n```json\n{\n \"mcpServers\": {\n \"internal\": {\n \"url\": \"https://mcp.internal.example/mcp\",\n \"allow_private_network\": true\n }\n }\n}\n```\n\n`allow_private_network` defaults to false. This exception applies only to the\nconfigured origin (scheme, host, and port); it does not authorize a different\nredirect or OAuth origin. Servers added by the model during a session cannot\nuse this exception, even if their configuration contains the flag.\n\nOperator-configured servers continue to honor `HTTP_PROXY`, `HTTPS_PROXY`, and\n`NO_PROXY`. When a proxy is selected for the configured origin, destination DNS\nresolution and private-network filtering are delegated to that operator-chosen\nproxy; a local DNS pin cannot constrain a proxy's own resolution. A `NO_PROXY`\nmatch uses the direct guarded connection instead. Model-added servers,\nreviewed plugin remotes, and secondary redirect/OAuth origins do not inherit\nambient proxy authority.\n\n## Remote HTTP Auth\n\nURL-based MCP servers can use static headers, env-derived headers, bearer-token\nenv vars, or OAuth. Authorization precedence is conservative:\n\n1. `headers` and `env_headers` are applied first.\n2. `bearer_token_env_var` adds `Authorization: Bearer ` when no\n Authorization header was already set.\n3. Stored OAuth credentials are used only when no Authorization header exists.\n\nFor bearer-token auth, prefer env-backed config:\n\n```json\n{\n \"servers\": {\n \"remote\": {\n \"url\": \"https://example.com/mcp\",\n \"bearer_token_env_var\": \"EXAMPLE_MCP_TOKEN\"\n }\n }\n}\n```\n\nFor generic remote MCP OAuth, add the URL server and run login:\n\n```bash\ncodewhale mcp add remote --url \"https://example.com/mcp\"\ncodewhale mcp login remote\n```\n\nCodewhale discovers the server OAuth metadata, opens the authorization URL in\nyour browser, listens on a local callback, exchanges the code, and stores the\ntoken response through the Codewhale secrets backend. Stored OAuth tokens are\nlooked up by server name plus URL and refreshed when possible before requests.\nEach saved credential also records the exact discovered authorization issuer\nand resource. Reload compares both before installing credentials; a changed\nissuer or resource requires a new login. Existing records without this binding\nare preserved but cannot be reused: run `codewhale mcp login ` to replace\nthem after authorization.\nDuring login, the CLI prints the authorization URL and a waiting status while\nthe local callback listener is active. If a URL-based server returns 401 or\nUnauthorized during connect/discovery, `codewhale mcp connect ` reports\nthat OAuth authentication is required and points to\n`codewhale mcp login `. Resource helper listings also surface an\n`authentication_required` entry for auth-shaped failures instead of silently\nlooking empty.\n\nOptional OAuth fields:\n\n```json\n{\n \"servers\": {\n \"remote\": {\n \"url\": \"https://example.com/mcp\",\n \"scopes\": [\"tools/read\"],\n \"oauth\": {\n \"client_id\": \"public-client-id\",\n \"issuer\": \"https://auth.example.com\"\n },\n \"oauth_resource\": \"https://example.com/mcp\"\n }\n }\n}\n```\n\nA pre-registered `oauth.client_id` requires its exact `oauth.issuer`. The CLI\naccepts `--oauth-client-id` together with `--oauth-issuer`. The optional\n`oauth_resource` is an exact pin for the SDK-discovered resource; it does not\nappend or override a second audience value.\n\nAlternatively, configure `oauth.client_metadata_url` (CLI:\n`--oauth-client-metadata-url`) with the HTTPS URL of an operator-hosted client\nmetadata document. Codewhale uses it when the authorization server advertises\nCIMD support, otherwise tries advertised dynamic registration. Pre-registration\ntakes priority. Codewhale identifies this desktop/CLI client as `native`; a\nhosted document must describe that client and its actual callback URLs. No\nCodewhale-hosted metadata document is supplied by this setting. Servers must\npublish discoverable authorization metadata; guessed legacy OAuth endpoints\nare not used for login or credential reload.\n\nUser-level config can set callback behavior when the provider requires a fixed\nredirect:\n\n```toml\nmcp_oauth_callback_port = 1455\nmcp_oauth_callback_url = \"http://127.0.0.1:1455/callback\"\n```\n\nThese callback fields are ignored from project-scope config overlays.\n\n## Hugging Face MCP\n\nHugging Face provides a hosted MCP server for Hub resources, documentation,\ndatasets, Spaces, and community tools. Codewhale does not call Hugging Face's\nHub HTTP APIs from `/hf`; it only helps you inspect and set up the MCP config\nthat the regular MCP manager will load.\n\nThe recommended setup path is Hugging Face's settings-generated configuration:\n\n1. Visit while signed in.\n2. Choose the MCP client closest to your Codewhale config shape and copy the\n generated server snippet.\n3. Paste the Hugging Face server entry into your resolved MCP config file.\n4. Run `/mcp reload` to rebuild the live model-visible tool pool.\n\nCodewhale reads both `servers` and `mcpServers`, so settings-generated snippets\ncan be adapted without changing the rest of the MCP file. A placeholder-only\nshape looks like this:\n\n```json\n{\n \"servers\": {\n \"huggingface\": {\n \"url\": \"https://huggingface.co/mcp\",\n \"headers\": {\n \"Authorization\": \"Bearer ${HF_TOKEN}\"\n }\n }\n }\n}\n```\n\nThe placeholder above is not a runnable secret. Use the settings-generated\nvalue in your private MCP config and never commit real Hugging Face tokens.\n\nInteractive helpers:\n\n```text\n/hf mcp status\n/hf mcp setup\n/hf concepts\n```\n\n`/hf mcp status` checks the configured MCP file for common Hugging Face server\nnames or Hugging Face MCP URLs. `/hf concepts` explains the difference between\nthe Hugging Face provider route, Hugging Face MCP, and explicit Hub workflows.\n\nOfficial docs: \n\n## OpenDesign\n\n[OpenDesign](https://github.com/nexu-io/open-design) ships a stdio MCP server\n(`od mcp`) that lets an agent read files from local OpenDesign projects and\ncreate project-scoped artifacts. Its `od mcp install` has no Codewhale target,\nso add the entry by hand:\n\n1. Start OpenDesign so its daemon is running.\n2. Copy the launch spec your install generated, from **Settings → MCP server**\n in the desktop app or from `curl -s http://127.0.0.1:7456/api/mcp/install-info`\n (use your daemon's port). It has a `command` (the absolute path of\n OpenDesign's Node-compatible runtime), `args` (the absolute path of its CLI\n entry, then `mcp`, then `--daemon-url http://127.0.0.1:` unless the\n daemon runs as a sidecar) and an `env` map that pins `OD_DATA_DIR`.\n3. Put those three fields under one server name in your resolved MCP config,\n inside the server map the file already has. Files written by\n `codewhale mcp init` and `codewhale mcp add` use `servers`; OpenDesign's\n generic snippet uses `mcpServers`. Keep exactly one of the two keys: a file\n with both fails to parse.\n4. Run `/mcp reload`, or check from a shell with\n `codewhale mcp tools open-design`.\n\nA placeholder-only shape:\n\n```json\n{\n \"servers\": {\n \"open-design\": {\n \"command\": \"/absolute/path/to/node\",\n \"args\": [\"/absolute/path/to/open-design/cli.js\", \"mcp\", \"--daemon-url\", \"http://127.0.0.1:7456\"],\n \"env\": { \"OD_DATA_DIR\": \"/absolute/path/to/open-design-data\" }\n }\n }\n}\n```\n\nCopy every `env` entry the snippet gives you; a packaged app adds more, such\nas `ELECTRON_RUN_AS_NODE=1` when `command` is the app's own binary.\n`codewhale mcp add` cannot set `env`, so edit the file instead. By default the\n`od mcp` process exits after 30 idle minutes; set `OD_MCP_STDIO_IDLE_EXIT_MS`\nto `\"0\"` in `env` to keep it running until Codewhale disconnects.\n\nOpenDesign can also launch Codewhale as its agent. Its \"DeepSeek TUI\" runtime\nadapter runs `deepseek`, falling back to `codewhale`, as\n`exec --auto [--model ] `, reads plain-text output, and caps the\nprompt at 30,000 bytes of argv. A dedicated adapter could instead pipe the\nprompt into `codewhale exec --auto --output-format stream-json --prompt-file -`,\nlist models with `codewhale models --json`, and continue a run with\n`codewhale exec --resume `, taking the id from the `saved_session_id` field\nof the `session_capture` event; the terminal `metadata` event carries only a\nfingerprint. `--auto` enables Full Access by default (no Codewhale shell\nsandbox, trust mode on); adding `--sandbox workspace-write` keeps the shell\nsandbox and file-tool workspace restrictions, with trust mode off. These\ntool-offering exec runs are saved with either plain-text or streaming output.\n[Agent runtime](AGENT_RUNTIME.md#event-vocabulary) describes the event stream.\n\n## Config File Location\n\nDefault path:\n\n- `~/.codewhale/mcp.json` (`~/.deepseek/mcp.json` is still read when the Codewhale file is absent)\n\nOverrides:\n\n- Config: `mcp_config_path = \"/path/to/mcp.json\"`\n- Env: `DEEPSEEK_MCP_CONFIG=/path/to/mcp.json`\n\n`codewhale mcp init` (and `codewhale setup --mcp`) writes to this resolved path.\n\nThe interactive `/config` editor also exposes `mcp_config_path`. Changing it in\nthe TUI updates the path used by `/mcp` and marks the pool reload-required;\n`/mcp reload` then switches the live pool to the new config source.\n\nAfter editing the MCP file or changing `mcp_config_path`, run `/mcp reload`. No\nTUI restart is needed.\n\n## Tool Naming\n\nDiscovered MCP tools are exposed to the model as:\n\n- `mcp__`\n\nExample: a server named `git` with a tool named `status` becomes `mcp_git_status`.\n\nThe command palette includes MCP entries grouped by server. It shows disabled\nand failed servers instead of hiding them, and uses the same runtime tool names\nshown to the model.\n\n## Resource and Prompt Helpers\n\nThe CLI also exposes helper tools when MCP is enabled:\n\n- `list_mcp_resources` (optional `server` filter)\n- `list_mcp_resource_templates` (optional `server` filter)\n- `mcp_read_resource` / `read_mcp_resource` (aliases)\n- `mcp_get_prompt`\n\n## Minimal Example\n\n```json\n{\n \"timeouts\": {\n \"connect_timeout\": 30,\n \"execute_timeout\": 1800,\n \"read_timeout\": 120\n },\n \"servers\": {\n \"example\": {\n \"command\": \"node\",\n \"args\": [\"./path/to/your-mcp-server.js\"],\n \"env\": {},\n \"disabled\": false\n }\n }\n}\n```\n\nYou can also use `mcpServers` instead of `servers` for compatibility with other clients.\n\n## Running Codewhale as an MCP Server\n\nYou can register your local Codewhale binary as an MCP server so other Codewhale sessions (or any MCP client) can call its tools.\n\n### Quick Setup\n\n```bash\ncodewhale mcp add-self\n```\n\nThis resolves the current binary path, generates a config entry that runs\n`codewhale serve --mcp`, and writes it to your MCP config file. The default\nserver name is `codewhale`.\n\nOptions:\n\n- `--name ` — custom server name (default: `codewhale`)\n- `--workspace ` — workspace directory for the server\n\n### Manual Config\n\nEquivalent manual entry in `~/.codewhale/mcp.json`:\n\n```json\n{\n \"servers\": {\n \"codewhale\": {\n \"command\": \"/path/to/codewhale\",\n \"args\": [\"serve\", \"--mcp\"],\n \"env\": {}\n }\n }\n}\n```\n\nThe consolidated `codewhale` runtime supports `serve --mcp` directly and also\noffers the equivalent `codewhale mcp-server` stdio entrypoint. Release\ninstallers expose the same runtime as `codew`; `mcp add-self` automatically\nresolves the command that invoked it.\n\n### Prerequisites\n\n- The binary referenced in `command` must exist and be executable.\n- The MCP server runs as a child process via stdio — no network ports required.\n- Each MCP client session spawns its own server process.\n\n### Tool Naming\n\nTools from an MCP server follow the standard naming convention:\n\n- `mcp__`\n\nFor example, the `shell` tool from the default server (named `codewhale`)\nbecomes `mcp_codewhale_shell`.\n\n### MCP Server vs HTTP/SSE API vs ACP\n\n| | `codewhale serve --mcp` | `codewhale serve --http` | `codewhale serve --acp` |\n|---|---|---|---|\n| **Protocol** | MCP stdio | HTTP/SSE JSON-RPC | ACP stdio |\n| **Use case** | Tool server for MCP clients | Runtime API for apps | Editor agent for Zed/custom ACP clients |\n| **Config** | `~/.codewhale/mcp.json` entry | Direct URL connection | Editor `agent_servers` custom command |\n| **Lifecycle** | Spawned per client session | Long-running daemon | Spawned per editor agent session |\n\nUse `mcp add-self` when you want Codewhale tools available to other MCP clients.\nUse `serve --http` when building applications that consume the API directly.\nUse `serve --acp` when an editor wants to talk to Codewhale as an ACP agent.\n\n### Verification\n\nAfter adding, test the connection:\n\n```bash\ncodewhale mcp validate\ncodewhale mcp tools codewhale\n```\n\n## Connection Lifecycle\n\nAs a client, Codewhale connects servers through the official MCP SDK (2.4)\nbehind its Rust broker by default (`[extension_host] mcp_backend = \"host\"` in\nyour user config; `\"rust\"` selects the previous Rust client and stays available\nfor one release, and a host refusal never falls back to it). Servers that speak\nthe 2026-07-28 revision are discovered with `server/discover`. Other stdio and\nHTTP servers, including ones that answer the discovery probe with an error or\nan unrelated result, connect through the legacy `initialize` handshake, and\nconfigured SSE servers keep theirs. A server that rejects sign-in reports its\nreal 401 with the `/mcp login` hint, and one whose command cannot start says\nwhy. Rust keeps process, network, credential, catalog, approval and\ncancellation authority.\n\nSession boot connects in the background: every enabled, allowed server —\nuser-configured or contributed by an enabled plugin — is started at session\nstart, concurrently and without holding the first turn. A plugin server passes\nthe same review, trust and enablement checks at session start as it does on\nfirst use. Mark a heavy user-configured server `lazy = true` to keep it\nunstarted until something asks for it — a turn whose\n`allowed_tools`/`tools.always_load` selection covers its `mcp__*`\nnames, a model call that resolves to one of its tools, or an explicit\n`/mcp retry `. Servers marked `required` connect at boot even if\n`lazy`, so their failure surfaces before the first turn. A server that has not\nconnected yet shows as `not connected` in `/mcp`, never `connecting`; the\nconnecting label only describes handshakes actually in flight. After an OAuth\nlogin finishes, that one server reconnects automatically. `codewhale mcp tools`\nand `codewhale mcp connect` run in their own process, so a server they just\nreached from the shell still reads `not connected` inside a running session\nuntil that session connects it.\n\n`/new`, `/resume`, `/load`, switching sessions from the resume picker, and a\nprovider, profile or plugin change keep user-configured stdio servers running:\neach live process moves to the new conversation or engine without a restart,\nprovided its config, the workspace and the MCP backend are unchanged. Anything\nthe server holds in memory, such as a browser session and its logins, comes\nalong; run `/mcp reload` for fresh servers. HTTP/SSE servers reconnect, so they\npick up current credentials and `[network]` policy. Servers added at runtime\nand plugin-contributed servers are not handed over and close at each of these\nboundaries, even when the conversation continues; plugin-contributed servers\nconnect again with the next session boot, while a server added at runtime must\nbe started again. A workspace switch restarts\nevery server. If an MCP call from the previous conversation is still running\nat the boundary, nothing is handed over and the servers restart as before;\notherwise later calls from that conversation's background work fail instead of\nreconnecting.\n\nAn MCP-focused `tool_search` is also explicit discovery intent: search a\nconfigured server name (for example `engram`), an exact `mcp__...`\nname, or use `{\"query\":\"mcp_.*\",\"match\":\"regex\"}`. The current turn's\nallow/deny ceiling filters the configured servers before a batch of at most\neight connects; the existing five-second wait and cancellation remain in\nforce. General searches do not boot all optional servers. Only actual\n`tools/list` schemas enter the deferred catalogue; failed, disabled, or\nrevoked servers do not acquire fabricated tools. Narrow a broad search by\nserver name when more than eight configured servers match.\n\nBefore a model request, one bounded runtime note records the current connection\nobservations for permitted servers. It uses fixed states: `configured` (no\nconnection outcome recorded), `connecting`, `failed`, `authorization_required`,\n`unknown` and `connected`. Failures and authorization needs are listed first;\nat most 24 names are shown, with 64 characters per name and an omission count.\nNames are inert data. Raw errors, commands, URLs and credentials are excluded.\n\nThe note supersedes earlier snapshots and lives in session history, after the\ncached system prompt. Unchanged observations are recorded once and are recorded\nagain if compaction removes them. No server is started or schema loaded by this\nobservation. When `tool_search` is available, the model can search a permitted\nconfigured name to obtain actual tools or retry a relevant connection. Without\nit, only servers identified by their actually exposed MCP tools are included.\nDisabled servers and servers outside the current discovery ceiling are omitted.\n`connected` means a current authorized transport was observed; it does not\npromise available schemas, tool permissions or a successful call.\n\nThe same nonblocking capture supplies server guidance. A busy pool, changed\nconfiguration source or removed scope withdraws old guidance and status instead\nof replaying stale success. Restore withdraws saved MCP notes until the live\nruntime observes the current pool. Child, RLM, ACP and isolated hosts do not\nacquire new discovery authority from these notes. Observations happen at request\nboundaries; this is not continuous monitoring or semantic verification of server\nclaims. The existing MCP manager owns configuration and authorization recovery.\n\n`codewhale mcp connect`, `validate`, and `tools` inspect their own process's\npool. They do not attach transports to a running TUI or exec session. Use\nin-session discovery or explicit tool selection. In the TUI,\n`/mcp retry ` connects through the current session's pool;\n`/mcp reload` re-reads its MCP configuration.\n\n### Catalog freshness\n\nA server's tools, resources, resource templates, prompts and skills are kept\non its own live connection, so they are never shared between servers,\ncredentials, sessions or reconnects. Every reconnect, login and config reload\ndiscovers them again, and allow/deny rules and approvals are checked on each\ncall rather than cached with the catalog. Within one connection the catalog is\nrevalidated in place, on the five-second supervision sweep, when either:\n\n- a list page carried `ttlMs` and the earliest of those TTLs (held between\n 60 seconds and 24 hours) has passed, or\n- the server sent `notifications/tools/list_changed`,\n `notifications/resources/list_changed` or\n `notifications/prompts/list_changed`.\n\nA list without `ttlMs` stays valid until one of those events. Because nothing\nis shared beyond the connection, `cacheScope: \"public\"` and `\"private\"` are\nboth held to the stricter private rule. Pagination, item and byte bounds are\nthe same as at connect, and ordering stays sorted. When revalidation changes\nthe catalog, a tool call resolved against the previous catalog is refused with\n\"retry the call\", a removed tool can no longer run, and the next model request\ncarries the new tool list. When revalidation fails, the connection is retired\nand ordinary reconnect supervision takes over; the stale catalog is not served.\n\nKnown limits: the Rust backend reads list-change notices only while it waits\nfor a reply, so an idle stdio server's notice is acted on after its next\nrequest. The 2026-07-28 `subscriptions/listen` stream and `resources/subscribe`\nare not opened; modern servers are kept fresh through their required `ttlMs`.\n\n### Skills from MCP servers\n\nA server that advertises the published Skills extension\n(`capabilities.extensions[\"io.modelcontextprotocol/skills\"]`) has its\n`skills/list` read with the rest of its catalog, under the same bounds and\nfreshness rules. An entry is kept only when its `SKILL.md` URI is listed among\nits own resources, its frontmatter has a name and description, every file sits\nbeneath the skill's root without `.`/`..`, `%`, `?` or `#` segments, every\ndigest is `sha256:` plus 64 lowercase hex characters, and it stays within 512\nfiles and 16 MiB. `\"dynamic\"` skills cannot be verified and are skipped.\n\nKept skills appear in `list_mcp_resources` with a `skill` field naming their\nfiles. Listing a skill never loads or activates it. Reading one of its files\nwith `read_mcp_resource` checks that the server returned exactly that URI with\nthe listed size and digest; a mismatch is refused and nothing is loaded. MCP\nskills are not yet merged into the native skill catalog, `/skills` or skill\nactivation.\n\n## Server Fields\n\nPer-server settings:\n\n- `command` (string, required)\n- `args` (array of strings, optional)\n- `env` (object, optional)\n- `connect_timeout`, `execute_timeout`, `read_timeout` (seconds, optional). A per-server value overrides the global `timeouts` block, and each budget is independent of the others:\n - `connect_timeout` (default 30) covers spawn, `initialize` and the first `tools/list`, so a cold `uvx`/`npx` package download counts against it. A server launched through `npx`, `pnpx`, `bunx` or `uvx` without its own `connect_timeout` gets at least 120 seconds for that first download.\n - `execute_timeout` (default 1800) is the budget for each `tools/call` and `prompts/get`. An explicit shorter value is respected, and `read_timeout` never cuts a running tool short.\n - `read_timeout` (default 120) bounds each reply wait during the handshake and tool discovery, and is the budget for `resources/read`.\n - A request that runs out of budget fails with a timeout. The connection is kept, and a reply that arrives later is discarded instead of being handed to another call. Interrupting the turn stops the wait at once, but Codewhale does not send `notifications/cancelled` yet, so a server that handles one request at a time finishes the abandoned call before it answers the next one.\n - Streamable HTTP servers return the reply inside the POST itself; the request's own budget bounds that POST too. A request that expires while still sending closes the connection, which is rebuilt before the next call.\n- `disabled` (bool, optional)\n- `enabled` (bool, optional, default `true`)\n- `required` (bool, optional): startup/connect validation fails if this server cannot initialize.\n- `lazy` (bool, optional, default `false`): keep this server unstarted until a turn, tool call or `/mcp retry` asks for it. Without it, the server connects at session start. Plugin manifests have no `lazy` field: an enabled plugin's servers connect at session start, and disabling the plugin stops them.\n- `enabled_tools` (array, optional): allowlist of tool names for this server.\n- `disabled_tools` (array, optional): denylist applied after `enabled_tools`.\n- `always_allow_tools` (optional array): Exact tool names the person allows across workspaces on this computer. Set these in the global server configuration. Off takes precedence. Computer Use consent and scripts still require the person’s approval.\n- `url` (string, optional): Streamable HTTP endpoint for a remote MCP server.\n- `allow_private_network` (boolean, default false): operator opt-in for private DNS addresses on this configured origin; ignored for model-added servers.\n- `transport` (string, optional): set to `\"sse\"` for legacy SSE endpoints.\n- `headers` (object, optional): literal HTTP headers for URL-based servers.\n- `env_headers` or `env_http_headers` (object, optional): header names mapped to environment variable names.\n- `bearer_token_env_var` (string, optional): environment variable containing a bearer token.\n- `scopes` (array, optional): default OAuth scopes for `mcp login`.\n- `oauth.client_id` (string, optional): pre-registered OAuth client ID; requires `oauth.issuer`.\n- `oauth.issuer` (string, optional): exact authorization issuer pin; required for a pre-registered client.\n- `oauth.client_metadata_url` (string, optional): HTTPS URL with a non-root path for an operator-hosted client metadata document.\n- `oauth_resource` (string, optional): exact resource pin checked against SDK discovery.\n\n## Safety Notes\n\nMCP tools flow through the same approval framework as built-in tools. Read-only\nMCP helpers (resource/prompt listing and reads) can run without prompts in Ask\nand Auto-Review when policy permits, while side-effectful MCP tools require\napproval. Full Access does not bypass hard policy holds.\n\nYou should still only configure MCP servers you trust, and treat MCP server configuration as equivalent to running code on your machine.\nAvoid committing literal `Authorization` headers. Prefer `env_headers`,\n`bearer_token_env_var`, or OAuth login so secrets stay outside the MCP file.\n\n## Troubleshooting\n\n- Run `codewhale doctor` to confirm the MCP config path it resolved and whether it exists.\n- In the TUI, run `/mcp validate` to see each server's state from the last\n recorded MCP snapshot, plus any config load error and configured servers that\n have not connected.\n- If tools are missing from the model's catalog after a config or credential\n change, run `/mcp reload` — `/mcp validate` only reports the last recorded\n snapshot and never reconnects.\n- If the MCP config is missing, run `codewhale mcp init --force` to regenerate it.\n- If tools don’t appear, verify the server command works from your shell and that the server supports MCP `tools/list`.\n" + "sha256": "d6d2f83cc1149151a9fa91b7d15973cfdeb72e8a5f5c03d5c80b9962d346c0ae", + "content": "# MCP (External Tool Servers)\n\nIn the terminal, `/mcp` (also `/mcps`) opens **Extensions → MCP**. Enter opens the selected server’s recovery action or read-only details. An empty inventory offers server suggestions; browsing them installs nothing. Explicit subcommands such as `/mcp status`, `/mcp doctor`, `/mcp login`, and `/mcp add` retain their existing behavior.\n\n\n> 阅读简体中文版:[zh_hans/MCP.md](zh_hans/MCP.md)\n\nCodewhale can load additional tools via MCP (Model Context Protocol). MCP servers can be local stdio processes that Codewhale starts, or remote URL-based servers that speak Streamable HTTP with legacy SSE fallback.\n\n## Version scope\n\nThis page describes the main candidate. As of November 7, 2026, the latest\npublished CLI release is [v0.10.2](https://github.com/codewhale-hq/Codewhale/releases/tag/v0.10.2).\nFor that installed release, use its [versioned MCP guide](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MCP.md).\nLocal stdio, remote HTTP/SSE, environment-backed authentication, direct remote\nOAuth, and reviewed plugin-contributed MCP are already supported in v0.10.2.\nSelected differences described below are candidate behavior:\n\n| Surface | Published v0.10.2 | Main candidate |\n| --- | --- | --- |\n| Default MCP client backend | Rust; SDK host is opt-in | Official MCP SDK 2.4.0 host; explicit Rust selection remains available |\n| Native stdio server protocols | Legacy revisions through 2025-06-18 | Adds 2025-11-25 and modern 2026-07-28 discovery |\n| Direct remote OAuth | Discovery, dynamic registration or a pre-registered public client | Adds issuer/resource-bound credentials and hosted client metadata support |\n| Catalogue freshness and MCP Skills | No TTL/list-change refresh or MCP Skills extension | Bounded refresh and verified skill resource reads; no native skill activation |\n| Package-runner connect budget | Default 30 seconds | At least 120 seconds unless the server has an explicit override |\n\nThe candidate's default host needs a supported Node runtime (`^22.19 || >=24`);\nBun 1.4.0 or newer is opt-in. See [extension runtime requirements](EXTENSIONS.md).\nAn MCP server's own executable, dependencies, authentication and OS requirements\nremain separate from Codewhale's transport support.\n\nBrowsing note:\n- `Web` is the canonical, deferred built-in browsing tool; it provides\n `search`, `fetch`, and `wait` actions when network policy permits.\n- `web_search`, `fetch_url`, and `wait_for_dev_server` are hidden replay-only\n aliases. New prompts and integrations should use `Web`.\n\nServer mode note:\n- `codewhale serve --mcp` runs the MCP stdio server.\n- `codewhale serve --http` runs the runtime HTTP/SSE API (separate mode).\n- `codewhale mcp-server` is an equivalent stdio entrypoint on the same\n consolidated runtime.\n\nIn 0.10.1, the old child-server aggregation proxy is removed. `mcp-server`\nnow uses the same native tool server as `serve --mcp`; it does not launch the\nlegacy `mcp.server_definitions` list. Saved definitions are left intact. Configure\nexternal servers in `mcp.json` for the existing MCP client, or connect to those\nservers directly from your external client. Native server write tools remain\nwithheld unless the operator explicitly permits them in its server configuration.\n\nThe native stdio server supports MCP **2026-07-28** with required per-request\nmetadata and `server/discover`; no initialization handshake is needed for that\nrevision. It also supports the explicit legacy revisions **2024-11-05**,\n**2025-03-26**, **2025-06-18** and **2025-11-25** through `initialize`.\nInitialization never selects a modern revision; unsupported initialization\nversions fall back to 2025-11-25. Legacy callers should send\n`notifications/initialized`, but calls may proceed after a valid initialize\nresponse. This describes Codewhale as a server; outbound MCP client version\nsupport is configured separately.\n\n`resources/list` pages through the effective native skill instruction catalog,\nand `resources/read` returns the accepted instruction body for an exact listed\n`codewhale://skills/...` URI. Discovery follows the configured skill roots,\nprecedence and workspace trust; disabled skills are omitted. Reads supply inert\nMarkdown and never activate a skill. These are instruction-body resources, not\noriginal SKILL.md files or complete skill packages. Plugin skills, arbitrary\nfilesystem paths and session transcripts are not exposed by this catalog.\nThe old workspace/session entries, which had no resource reader, are removed.\nResource templates are empty. Lists and reads use private caching with a zero\nTTL; restart pagination if a changed catalog invalidates a cursor.\n\nOnly tools and resources are advertised. Subscriptions, list-change\nnotifications, the Skills extension, Apps, tasks and interactive approval are\nnot implemented by this server. Tool execution remains serialized through the\nRust registry, and request cancellation reaches the existing tool context.\nKeep stdin open until responses arrive; closing it cancels outstanding work.\nMalformed JSON and oversized input frames are discarded by the bounded SDK\ntransport, while well-formed invalid JSON-RPC receives an error.\n\n## Setup wizard vs manual MCP setup (#3407)\n\nThe `/setup` hub includes an optional **Tools and MCP**\nstep. That step is discovery/readiness only:\n\n| Wizard can do | Still requires manual / explicit action |\n| --- | --- |\n| Show configured servers as `healthy` / `needs_config` / `off` | Start or connect MCP servers |\n| Report config path presence (global + project) | Write or edit `mcp.json` contents |\n| Safe static health probe (missing command/url, broken absolute path, missing bearer env) | `codewhale mcp validate`, live connect, OAuth login |\n| Point at safe on-ramps (`/mcp`, `codewhale mcp init`, `codewhale doctor`) | Install community skills, trust skills, enable plugins |\n| Share Hotbar source counts from the same skill/MCP adapters (#3399) | Bind Hotbar slots (Hotbar step / `H`) |\n| Record optional/`needs_action` setup_state without blocking first-run | Anything that spawns processes or installs packages |\n\nEmpty inventory is **not** an error: first-run users see “nothing configured\nyet, that’s fine.” Failing or incomplete configured servers surface as\n`needs_config` with an actionable hint and never block setup completion.\nEnumeration never executes MCP/plugin commands beyond the static probe.\nSummaries redact commands, args, env, headers, and tokens.\n\n`codewhale doctor` reports MCP/skills/tools/plugins health with the same\noptional-surface intent (paths, counts, static checks) so wizard and doctor\nstay consistent.\n\n## Plugin-contributed MCP\n\nA reviewed local plugin bundle may contribute MCP servers without creating a\nsecond transport or approval system. The servers use the same MCP manager,\ntool approval, resource, prompt, timeout, and network-policy paths documented\nhere, and appear under collision-resistant\n`plugin---` identities.\n\nAuthentication differs from direct user-authored MCP configuration. Plugin\nremotes may use `env_headers` or `bearer_token_env_var`, but plugin-contributed\nMCP OAuth is disabled in both v0.10.2 and the main candidate. Declaring OAuth,\nscopes or an OAuth resource in a plugin MCP manifest fails validation. To use\nOAuth, configure the remote server directly in your own `mcp.json` instead.\n\nThe bundle boundary is intentionally stricter than user-authored `mcp.json`:\nunknown fields and ambiguous transports fail closed; stdio environment values\nmust be exact environment-source references; remote literal headers and\nsecret-bearing URLs are rejected; declared network hosts must exactly match\nthe normalized endpoint host set; and redirects remain on the reviewed origin.\nReviewed plugin remotes also bypass ambient HTTP proxy configuration entirely;\nproxy credentials and proxy-observed traffic are not part of the v1 review.\nThe plugin review discloses local host-user authority, structural argv,\nenvironment provenance, endpoint, auth source names, and tool filters\nwithout reading or printing secret values.\n\nTrust stages reviewed content but does not enable it. Enablement attaches that\nstaged snapshot to the current workspace's MCP pool. Disable, revoke, and other\ncross-process generation changes remove catalog entries, cancel in-flight\noperations, and terminate plugin stdio children. Source or staged-tree drift is\nfully revalidated before each dispatch/catalogue boundary and fails the next\nboundary closed; v0.9.1 does not continuously hash mutable trees during an\nalready-running call and therefore does not promise drift-triggered mid-call\ncancellation. MCP subscriptions are not exposed through plugin bundles. See\n[Plugin bundles](PLUGIN_BUNDLES.md) for the complete lifecycle contract.\n\n## Bootstrap MCP Config\n\nCreate a starter MCP config at your resolved MCP path:\n\n```bash\ncodewhale mcp init\n```\n\n`codewhale setup --mcp` performs the same MCP bootstrap alongside skills setup.\n\nCommon management commands:\n\n```bash\ncodewhale mcp list\ncodewhale mcp tools [server]\ncodewhale mcp add --command \"\" --arg \"\"\ncodewhale mcp add --url \"http://localhost:3000/mcp\"\ncodewhale mcp add --url \"https://example.com/mcp\" --bearer-token-env-var MCP_TOKEN\ncodewhale mcp login \ncodewhale mcp logout \ncodewhale mcp enable \ncodewhale mcp disable \ncodewhale mcp remove \ncodewhale mcp validate\n```\n\nShell commands inspect their own process's MCP pool. In particular,\n`codewhale mcp validate` starts and connects enabled, allowed servers and reports\nconnection/discovery failures; it is not a static configuration check and does\nnot attach those connections to a running TUI. In the TUI, `/mcp validate`\nreports the last recorded snapshot without reconnecting; `/mcp reload` rebuilds\nthat session's live pool. `codewhale doctor` is the static diagnostic path\nunless an explicit probe is requested.\n\n`codewhale mcp logout ` (and `/mcp logout`) clears locally stored\nOAuth credentials only — the provider may keep its standing grant. The next\nlogin forces the consent screen, so the authorized account/workspace can\nchange; to sever the grant remotely, revoke the app from the provider's\naccount settings.\n\n## In-TUI Manager\n\nInside the interactive TUI, `/mcp` opens a compact manager for the resolved\nMCP config path. It shows each configured server, whether it is enabled or\ndisabled, its transport, command or URL, timeout values, connection errors,\nand discovered tools/resources/prompts when discovery has been run.\n\nSupported in-TUI actions:\n\n```text\n/mcp init\n/mcp init --force\n/mcp import\n/mcp recommendations\n/mcp add recommended \n/mcp add stdio [args...]\n/mcp add http \n/mcp login [--scope scope]\n/mcp logout \n/mcp enable \n/mcp disable \n/mcp remove \n/mcp validate\n/mcp reload\n```\n\n### Suggested plugins and companion integrations\n\n`/mcp recommendations` is Codewhale's native, curated suggestions surface.\nThe entries are described as product plugins, with their component type and\nprovenance, but `/mcp add recommended ` still writes only the named MCP\nserver component. Viewing recommendations never fetches, installs, trusts, or\nenables anything. Adding one writes configuration; the server is first started\nonly after an explicit `/mcp restart`.\n\nThe v0.9.10 product suggestions use these reviewed, pinned definitions. The\nPlugins view is the product/install surface; MCP, Skills, and sandbox adapters\nare transparent component kinds and their own tabs remain operational and\ndiagnostic surfaces:\n\n| Plugin | Component | Pinned definition | Provenance and maturity | Installation boundary |\n| --- | --- | --- | --- | --- |\n| Chrome DevTools | MCP server (stdio) | `npx -y chrome-devtools-mcp@1.7.0` (`npx.cmd` on Windows) | [Official ChromeDevTools project](https://github.com/ChromeDevTools/chrome-devtools-mcp) | npm may download the pinned package when the user restarts MCP. |\n| Playwright | MCP server (stdio) | `npx -y @playwright/mcp@0.0.79 --isolated` (`npx.cmd` on Windows) | [Official Microsoft project](https://github.com/microsoft/playwright-mcp) | `--isolated` starts a fresh browser profile; npm may download the pinned package only after an explicit restart. |\n| Computer Use | First-party plugin (MCP + skill) | Install `computer-use` from the first-party catalog; updates independently | Codewhale; install, review and enable through Connectors or `/plugin` | This is the only computer-use integration Codewhale recommends. Third-party desktop-control MCPs are not listed here. |\n| Browser Use | Skill plus separately installed Python runtime | Skill/runtime release `0.13.8` | [Official browser-use project](https://github.com/browser-use/browser-use) | Optional companion: not an MCP server. Codewhale does not auto-run the upstream Skill installer or install its browser/runtime dependencies. |\n| Anthropic Sandbox Runtime | Sandbox adapter companion | `@anthropic-ai/sandbox-runtime@0.0.73` | [Official anthropic-experimental project](https://github.com/anthropic-experimental/sandbox-runtime); beta | Documentation-only adapter candidate in v0.9.10: not an MCP server and not an active Codewhale plugin adapter. It does not replace Codewhale's sandbox policy. |\n\n[Container Use](https://github.com/dagger/container-use) remains an additional\nexperimental suggestion with an MCP server component (`container-use stdio`).\nThe binary must be installed separately; `/mcp add recommended container-use`\nonly writes config and Codewhale never downloads it.\n\nThis presentation follows the same useful boundary found in the local\nGrokbuild extensions view (one product plugin may expose MCP or Skill\ncomponents while component tabs stay inspectable), the Kimi marketplace's\nexplicit display name/tier/source fields, and the Codex marketplace's explicit\nsource and install-policy fields.\nCodewhale keeps its stricter rule: provenance and foreign policy are display\nmetadata only, never inherited trust or automatic installation. For full\nbundle and marketplace semantics, see [Plugin bundles](PLUGIN_BUNDLES.md).\n\n`/mcp validate` (alias `/mcp doctor`) with no name reports each server's state\nfrom the last MCP snapshot the TUI recorded, in a pager over\n**Extensions → MCP**. That snapshot is usually the engine pool's latest report,\nbut some actions, such as `/mcp import`, replace it with a separate UI\ndiscovery pass that also connects `lazy` servers the engine has not started.\nValidate also re-reads the MCP config files without connecting: it\nreports a config load error first, lists configured servers the snapshot has\nnot observed, and says when a `/mcp reload` is pending. It does not reconnect\nanything or change the catalog the model gets; `/mcp reload` is the fresh\nprobe.\n\n`/mcp reload` (aliases `/mcp reconnect`, `/mcp restart`) is the hot-reload path.\nIt re-reads the MCP config sources and reconnects the servers session boot starts (see [Connection Lifecycle](#connection-lifecycle)) through the engine-owned pool,\nso the rebuilt catalog is the exact one the next model turn uses — no TUI\nrestart. Config edits made from the TUI are written immediately and the manager\nmarks the snapshot reload-required until you run it; a failed reload leaves the\nprevious live pool intact and says so.\n\nHeadless surfaces are the exception: the `ConfigReload` app-server request does\n**not** refresh MCP connections, so a headless runtime still needs a restart\nafter MCP config changes.\n\n## Remote network authority\n\nDirect HTTP/SSE requests to public hostnames validate every DNS answer and pin\nconnections to a public address. This also applies to configured servers,\nredirects, and OAuth HTTP requests. The configured network allow/deny policy\napplies to login and token refresh as well as MCP tool requests.\n\nA configured `localhost` name or private IP literal explicitly permits that\nlocal endpoint. For a private DNS name, opt in on the server configuration:\n\n```json\n{\n \"mcpServers\": {\n \"internal\": {\n \"url\": \"https://mcp.internal.example/mcp\",\n \"allow_private_network\": true\n }\n }\n}\n```\n\n`allow_private_network` defaults to false. This exception applies only to the\nconfigured origin (scheme, host, and port); it does not authorize a different\nredirect or OAuth origin. Servers added by the model during a session cannot\nuse this exception, even if their configuration contains the flag.\n\nOperator-configured servers continue to honor `HTTP_PROXY`, `HTTPS_PROXY`, and\n`NO_PROXY`. When a proxy is selected for the configured origin, destination DNS\nresolution and private-network filtering are delegated to that operator-chosen\nproxy; a local DNS pin cannot constrain a proxy's own resolution. A `NO_PROXY`\nmatch uses the direct guarded connection instead. Model-added servers,\nreviewed plugin remotes, and secondary redirect/OAuth origins do not inherit\nambient proxy authority.\n\n## Remote HTTP Auth\n\nDirect URL-based MCP servers can use static headers, env-derived headers,\nbearer-token env vars, or OAuth. Plugin-contributed remotes are limited to\nenvironment-backed headers/bearer tokens and cannot use MCP OAuth; see\n[Plugin-contributed MCP](#plugin-contributed-mcp). Authorization precedence for\ndirect servers is conservative:\n\n1. `headers` and `env_headers` are applied first.\n2. `bearer_token_env_var` adds `Authorization: Bearer ` when no\n Authorization header was already set.\n3. Stored OAuth credentials are used only when no Authorization header exists.\n\nFor bearer-token auth, prefer env-backed config:\n\n```json\n{\n \"servers\": {\n \"remote\": {\n \"url\": \"https://example.com/mcp\",\n \"bearer_token_env_var\": \"EXAMPLE_MCP_TOKEN\"\n }\n }\n}\n```\n\nFor generic remote MCP OAuth, add the URL server and run login:\n\n```bash\ncodewhale mcp add remote --url \"https://example.com/mcp\"\ncodewhale mcp login remote\n```\n\nCodewhale discovers the server OAuth metadata, opens the authorization URL in\nyour browser, listens on a local callback, exchanges the code, and stores the\ntoken response through the Codewhale secrets backend. Stored OAuth tokens are\nlooked up by server name plus URL and refreshed when possible before requests.\nEach saved credential also records the exact discovered authorization issuer\nand resource. Reload compares both before installing credentials; a changed\nissuer or resource requires a new login. Existing records without this binding\nare preserved but cannot be reused: run `codewhale mcp login ` to replace\nthem after authorization.\nDuring login, the CLI prints the authorization URL and a waiting status while\nthe local callback listener is active. If a URL-based server returns 401 or\nUnauthorized during connect/discovery, `codewhale mcp connect ` reports\nthat OAuth authentication is required and points to\n`codewhale mcp login `. Resource helper listings also surface an\n`authentication_required` entry for auth-shaped failures instead of silently\nlooking empty.\n\nOptional OAuth fields:\n\n```json\n{\n \"servers\": {\n \"remote\": {\n \"url\": \"https://example.com/mcp\",\n \"scopes\": [\"tools/read\"],\n \"oauth\": {\n \"client_id\": \"public-client-id\",\n \"issuer\": \"https://auth.example.com\"\n },\n \"oauth_resource\": \"https://example.com/mcp\"\n }\n }\n}\n```\n\nA pre-registered `oauth.client_id` requires its exact `oauth.issuer`. The CLI\naccepts `--oauth-client-id` together with `--oauth-issuer`. The optional\n`oauth_resource` is an exact pin for the SDK-discovered resource; it does not\nappend or override a second audience value.\n\nAlternatively, configure `oauth.client_metadata_url` (CLI:\n`--oauth-client-metadata-url`) with the HTTPS URL of an operator-hosted client\nmetadata document. Codewhale uses it when the authorization server advertises\nCIMD support, otherwise tries advertised dynamic registration. Pre-registration\ntakes priority. Codewhale identifies this desktop/CLI client as `native`; a\nhosted document must describe that client and its actual callback URLs. No\nCodewhale-hosted metadata document is supplied by this setting. Servers must\npublish discoverable authorization metadata; guessed legacy OAuth endpoints\nare not used for login or credential reload.\n\nUser-level config can set callback behavior when the provider requires a fixed\nredirect:\n\n```toml\nmcp_oauth_callback_port = 1455\nmcp_oauth_callback_url = \"http://127.0.0.1:1455/callback\"\n```\n\nThese callback fields are ignored from project-scope config overlays.\n\n## Hugging Face MCP\n\nHugging Face provides a hosted MCP server for Hub resources, documentation,\ndatasets, Spaces, and community tools. Codewhale does not call Hugging Face's\nHub HTTP APIs from `/hf`; it only helps you inspect and set up the MCP config\nthat the regular MCP manager will load.\n\nThe recommended setup path is Hugging Face's settings-generated configuration:\n\n1. Visit while signed in.\n2. Choose the MCP client closest to your Codewhale config shape and copy the\n generated server snippet.\n3. Paste the Hugging Face server entry into your resolved MCP config file.\n4. Run `/mcp reload` to rebuild the live model-visible tool pool.\n\nCodewhale reads both `servers` and `mcpServers`, so settings-generated snippets\ncan be adapted without changing the rest of the MCP file. A placeholder-only\nshape looks like this:\n\n```json\n{\n \"servers\": {\n \"huggingface\": {\n \"url\": \"https://huggingface.co/mcp\",\n \"bearer_token_env_var\": \"HF_TOKEN\"\n }\n }\n}\n```\n\nSet `HF_TOKEN` in the environment that launches Codewhale; the example contains\nonly its variable name. Literal `headers` values do not expand `${HF_TOKEN}`.\nAdapt the settings-generated server entry and never commit real tokens.\n\nInteractive helpers:\n\n```text\n/hf mcp status\n/hf mcp setup\n/hf concepts\n```\n\n`/hf mcp status` checks the configured MCP file for common Hugging Face server\nnames or Hugging Face MCP URLs. `/hf concepts` explains the difference between\nthe Hugging Face provider route, Hugging Face MCP, and explicit Hub workflows.\n\nOfficial docs: \n\n## OpenDesign\n\n[OpenDesign](https://github.com/nexu-io/open-design) ships a stdio MCP server\n(`od mcp`) that lets an agent read files from local OpenDesign projects and\ncreate project-scoped artifacts. Its `od mcp install` has no Codewhale target,\nso add the entry by hand:\n\n1. Start OpenDesign so its daemon is running.\n2. Copy the launch spec your install generated, from **Settings → MCP server**\n in the desktop app or from `curl -s http://127.0.0.1:7456/api/mcp/install-info`\n (use your daemon's port). It has a `command` (the absolute path of\n OpenDesign's Node-compatible runtime), `args` (the absolute path of its CLI\n entry, then `mcp`, then `--daemon-url http://127.0.0.1:` unless the\n daemon runs as a sidecar) and an `env` map that pins `OD_DATA_DIR`.\n3. Put those three fields under one server name in your resolved MCP config,\n inside the server map the file already has. Files written by\n `codewhale mcp init` and `codewhale mcp add` use `servers`; OpenDesign's\n generic snippet uses `mcpServers`. Keep exactly one of the two keys: a file\n with both fails to parse.\n4. Run `/mcp reload`, or check from a shell with\n `codewhale mcp tools open-design`.\n\nA placeholder-only shape:\n\n```json\n{\n \"servers\": {\n \"open-design\": {\n \"command\": \"/absolute/path/to/node\",\n \"args\": [\"/absolute/path/to/open-design/cli.js\", \"mcp\", \"--daemon-url\", \"http://127.0.0.1:7456\"],\n \"env\": { \"OD_DATA_DIR\": \"/absolute/path/to/open-design-data\" }\n }\n }\n}\n```\n\nCopy every `env` entry the snippet gives you; a packaged app adds more, such\nas `ELECTRON_RUN_AS_NODE=1` when `command` is the app's own binary.\n`codewhale mcp add` cannot set `env`, so edit the file instead. By default the\n`od mcp` process exits after 30 idle minutes; set `OD_MCP_STDIO_IDLE_EXIT_MS`\nto `\"0\"` in `env` to keep it running until Codewhale disconnects.\n\nOpenDesign can also launch Codewhale as its agent. Its \"DeepSeek TUI\" runtime\nadapter runs `deepseek`, falling back to `codewhale`, as\n`exec --auto [--model ] `, reads plain-text output, and caps the\nprompt at 30,000 bytes of argv. A dedicated adapter could instead pipe the\nprompt into `codewhale exec --auto --output-format stream-json --prompt-file -`,\nlist models with `codewhale models --json`, and continue a run with\n`codewhale exec --resume `, taking the id from the `saved_session_id` field\nof the `session_capture` event; the terminal `metadata` event carries only a\nfingerprint. `--auto` enables Full Access by default (no Codewhale shell\nsandbox, trust mode on); adding `--sandbox workspace-write` keeps the shell\nsandbox and file-tool workspace restrictions, with trust mode off. These\ntool-offering exec runs are saved with either plain-text or streaming output.\n[Agent runtime](AGENT_RUNTIME.md#event-vocabulary) describes the event stream.\n\n## Config File Location\n\nDefault path:\n\n- `~/.codewhale/mcp.json` (`~/.deepseek/mcp.json` is still read when the Codewhale file is absent)\n\nOverrides:\n\n- Config: `mcp_config_path = \"/path/to/mcp.json\"`\n- Env: `CODEWHALE_MCP_CONFIG=/absolute/path/to/mcp.json` (takes precedence over\n the legacy `DEEPSEEK_MCP_CONFIG` spelling)\n\nCustom paths must be absolute; a relative `mcp_config_path` falls back to the\nuser-global path rather than changing with the launch directory.\n\nCodewhale also reads `/.codewhale/mcp.json` after the workspace is\ntrusted in user-owned configuration. Project entries override same-named global\nservers; project-local files cannot grant that trust themselves or redirect the\nglobal `mcp_config_path`. Global and trusted-project entries take precedence\nover a colliding plugin-contributed identity. Project stdio commands default\ntheir working directory to the workspace. In the main candidate,\n`always_allow_tools` is accepted only from the global server configuration,\nnot from project or plugin declarations.\n\n`codewhale mcp init` (and `codewhale setup --mcp`) writes to this resolved path.\n\nThe interactive `/config` editor also exposes `mcp_config_path`. Changing it in\nthe TUI updates the path used by `/mcp` and marks the pool reload-required;\n`/mcp reload` then switches the live pool to the new config source.\n\nAfter editing the MCP file or changing `mcp_config_path`, run `/mcp reload`. No\nTUI restart is needed.\n\n## Tool Naming\n\nDiscovered MCP tools are exposed to the model as:\n\n- `mcp__`\n\nExample: a server named `git` with a tool named `status` becomes `mcp_git_status`.\n\nThe command palette includes MCP entries grouped by server. It shows disabled\nand failed servers instead of hiding them, and uses the same runtime tool names\nshown to the model.\n\n## Resource and Prompt Helpers\n\nThe CLI also exposes helper tools when MCP is enabled:\n\n- `list_mcp_resources` (optional `server` filter)\n- `list_mcp_resource_templates` (optional `server` filter)\n- `mcp_read_resource` / `read_mcp_resource` (aliases)\n- `mcp_get_prompt`\n\n## Minimal Example\n\n```json\n{\n \"timeouts\": {\n \"connect_timeout\": 30,\n \"execute_timeout\": 1800,\n \"read_timeout\": 120\n },\n \"servers\": {\n \"example\": {\n \"command\": \"node\",\n \"args\": [\"./path/to/your-mcp-server.js\"],\n \"env\": {},\n \"disabled\": false\n }\n }\n}\n```\n\nYou can also use `mcpServers` instead of `servers` for compatibility with other\nclients. Use exactly one map key in a file: a file containing both fails to\nparse.\n\n## Running Codewhale as an MCP Server\n\nYou can register your local Codewhale binary as a stdio MCP server for another\nCodewhale session or a client supporting one of its advertised protocol\nrevisions. This exposes native tools; it is separate from connecting Codewhale\nto external servers.\n\n### Client configuration vs server configuration\n\n`mcp.json` tells a client which servers to launch or contact. The native\n`codewhale serve --mcp` process separately reads `[server]` from the retained\nlegacy path `~/.deepseek/mcp_server.toml`. By default it exposes only\n`file_read` and `search`. Writing files or running commands requires naming\nthose tools in `expose_tools` and explicitly setting `require_approval = false`\nin that server file. This stdio server has no interactive approval channel;\nwith approval required, writing/executing tools are withheld and refused.\n\n### Quick Setup\n\n```bash\ncodewhale mcp add-self\n```\n\nThis resolves the current binary path, generates a config entry that runs\n`codewhale serve --mcp`, and writes it to your MCP config file. The default\nserver name is `codewhale`.\n\nOptions:\n\n- `--name ` — custom server name (default: `codewhale`)\n- `--workspace ` — workspace directory for the server\n\n### Manual Config\n\nEquivalent manual entry in `~/.codewhale/mcp.json`:\n\n```json\n{\n \"servers\": {\n \"codewhale\": {\n \"command\": \"/path/to/codewhale\",\n \"args\": [\"serve\", \"--mcp\"],\n \"env\": {}\n }\n }\n}\n```\n\nThe consolidated `codewhale` runtime supports `serve --mcp` directly and also\noffers the equivalent `codewhale mcp-server` stdio entrypoint. Release\ninstallers expose the same runtime as `codew`; `mcp add-self` automatically\nresolves the command that invoked it.\n\n### Prerequisites\n\n- The binary referenced in `command` must exist and be executable.\n- The MCP server runs as a child process via stdio — no network ports required.\n- Each MCP client session spawns its own server process.\n\n### Tool Naming\n\nTools from an MCP server follow the standard naming convention:\n\n- `mcp__`\n\nFor example, the `shell` tool from the default server (named `codewhale`)\nbecomes `mcp_codewhale_shell`.\n\n### MCP Server vs HTTP/SSE API vs ACP\n\n| | `codewhale serve --mcp` | `codewhale serve --http` | `codewhale serve --acp` |\n|---|---|---|---|\n| **Protocol** | MCP stdio | HTTP/SSE JSON-RPC | ACP stdio |\n| **Use case** | Tool server for MCP clients | Runtime API for apps | Editor agent for Zed/custom ACP clients |\n| **Config** | `~/.codewhale/mcp.json` entry | Direct URL connection | Editor `agent_servers` custom command |\n| **Lifecycle** | Spawned per client session | Long-running daemon | Spawned per editor agent session |\n\nUse `mcp add-self` when you want Codewhale tools available to other MCP clients.\nUse `serve --http` when building applications that consume the API directly.\nUse `serve --acp` when an editor wants to talk to Codewhale as an ACP agent.\n\n### Verification\n\nAfter adding, test the connection:\n\n```bash\ncodewhale mcp validate\ncodewhale mcp tools codewhale\n```\n\n## Connection Lifecycle\n\nAs a client, Codewhale connects servers through the official MCP SDK (2.4)\nbehind its Rust broker by default (`[extension_host] mcp_backend = \"host\"` in\nyour user config; `\"rust\"` selects the previous Rust client and stays available\nfor one release, and a host refusal never falls back to it). Servers that speak\nthe 2026-07-28 revision are discovered with `server/discover`. Other stdio and\nHTTP servers, including ones that answer the discovery probe with an error or\nan unrelated result, connect through the legacy `initialize` handshake, and\nconfigured SSE servers keep theirs. A server that rejects sign-in reports its\nreal 401 with the `/mcp login` hint, and one whose command cannot start says\nwhy. Rust keeps process, network, credential, catalog, approval and\ncancellation authority.\n\nSession boot connects in the background: every enabled, allowed server —\nuser-configured or contributed by an enabled plugin — is started at session\nstart, concurrently and without holding the first turn. A plugin server passes\nthe same review, trust and enablement checks at session start as it does on\nfirst use. Mark a heavy user-configured server `lazy = true` to keep it\nunstarted until something asks for it — a turn whose\n`allowed_tools`/`tools.always_load` selection covers its `mcp__*`\nnames, a model call that resolves to one of its tools, or an explicit\n`/mcp retry `. Servers marked `required` connect at boot even if\n`lazy`, so their failure surfaces before the first turn. A server that has not\nconnected yet shows as `not connected` in `/mcp`, never `connecting`; the\nconnecting label only describes handshakes actually in flight. After an OAuth\nlogin finishes, that one server reconnects automatically. `codewhale mcp tools`\nand `codewhale mcp connect` run in their own process, so a server they just\nreached from the shell still reads `not connected` inside a running session\nuntil that session connects it.\n\n`/new`, `/resume`, `/load`, switching sessions from the resume picker, and a\nprovider, profile or plugin change keep user-configured stdio servers running:\neach live process moves to the new conversation or engine without a restart,\nprovided its config, the workspace and the MCP backend are unchanged. Anything\nthe server holds in memory, such as a browser session and its logins, comes\nalong; run `/mcp reload` for fresh servers. HTTP/SSE servers reconnect, so they\npick up current credentials and `[network]` policy. Servers added at runtime\nand plugin-contributed servers are not handed over and close at each of these\nboundaries, even when the conversation continues; plugin-contributed servers\nconnect again with the next session boot, while a server added at runtime must\nbe started again. A workspace switch restarts\nevery server. If an MCP call from the previous conversation is still running\nat the boundary, nothing is handed over and the servers restart as before;\notherwise later calls from that conversation's background work fail instead of\nreconnecting.\n\nAn MCP-focused `tool_search` is also explicit discovery intent: search a\nconfigured server name (for example `engram`), an exact `mcp__...`\nname, or use `{\"query\":\"mcp_.*\",\"match\":\"regex\"}`. The current turn's\nallow/deny ceiling filters the configured servers before a batch of at most\neight connects; the existing five-second wait and cancellation remain in\nforce. General searches do not boot all optional servers. Only actual\n`tools/list` schemas enter the deferred catalogue; failed, disabled, or\nrevoked servers do not acquire fabricated tools. Narrow a broad search by\nserver name when more than eight configured servers match.\n\nBefore a model request, one bounded runtime note records the current connection\nobservations for permitted servers. It uses fixed states: `configured` (no\nconnection outcome recorded), `connecting`, `failed`, `authorization_required`,\n`unknown` and `connected`. Failures and authorization needs are listed first;\nat most 24 names are shown, with 64 characters per name and an omission count.\nNames are inert data. Raw errors, commands, URLs and credentials are excluded.\n\nThe note supersedes earlier snapshots and lives in session history, after the\ncached system prompt. Unchanged observations are recorded once and are recorded\nagain if compaction removes them. No server is started or schema loaded by this\nobservation. When `tool_search` is available, the model can search a permitted\nconfigured name to obtain actual tools or retry a relevant connection. Without\nit, only servers identified by their actually exposed MCP tools are included.\nDisabled servers and servers outside the current discovery ceiling are omitted.\n`connected` means a current authorized transport was observed; it does not\npromise available schemas, tool permissions or a successful call.\n\nThe same nonblocking capture supplies server guidance. A busy pool, changed\nconfiguration source or removed scope withdraws old guidance and status instead\nof replaying stale success. Restore withdraws saved MCP notes until the live\nruntime observes the current pool. Child, RLM, ACP and isolated hosts do not\nacquire new discovery authority from these notes. Observations happen at request\nboundaries; this is not continuous monitoring or semantic verification of server\nclaims. The existing MCP manager owns configuration and authorization recovery.\n\n`codewhale mcp connect`, `validate`, and `tools` inspect their own process's\npool. They do not attach transports to a running TUI or exec session. Use\nin-session discovery or explicit tool selection. In the TUI,\n`/mcp retry ` connects through the current session's pool;\n`/mcp reload` re-reads its MCP configuration.\n\n### Catalog freshness\n\nA server's tools, resources, resource templates, prompts and skills are kept\non its own live connection, so they are never shared between servers,\ncredentials, sessions or reconnects. Every reconnect, login and config reload\ndiscovers them again, and allow/deny rules and approvals are checked on each\ncall rather than cached with the catalog. Within one connection the catalog is\nrevalidated in place, on the five-second supervision sweep, when either:\n\n- a list page carried `ttlMs` and the earliest of those TTLs (held between\n 60 seconds and 24 hours) has passed, or\n- the server sent `notifications/tools/list_changed`,\n `notifications/resources/list_changed` or\n `notifications/prompts/list_changed`.\n\nA list without `ttlMs` stays valid until one of those events. Because nothing\nis shared beyond the connection, `cacheScope: \"public\"` and `\"private\"` are\nboth held to the stricter private rule. Pagination, item and byte bounds are\nthe same as at connect, and ordering stays sorted. When revalidation changes\nthe catalog, a tool call resolved against the previous catalog is refused with\n\"retry the call\", a removed tool can no longer run, and the next model request\ncarries the new tool list. When revalidation fails, the connection is retired\nand ordinary reconnect supervision takes over; the stale catalog is not served.\n\nKnown limits: the Rust backend reads list-change notices only while it waits\nfor a reply, so an idle stdio server's notice is acted on after its next\nrequest. The 2026-07-28 `subscriptions/listen` stream and `resources/subscribe`\nare not opened; modern servers are kept fresh through their required `ttlMs`.\n\n### Skills from MCP servers\n\nA server that advertises the published Skills extension\n(`capabilities.extensions[\"io.modelcontextprotocol/skills\"]`) has its\n`skills/list` read with the rest of its catalog, under the same bounds and\nfreshness rules. An entry is kept only when its `SKILL.md` URI is listed among\nits own resources, its frontmatter has a name and description, every file sits\nbeneath the skill's root without `.`/`..`, `%`, `?` or `#` segments, every\ndigest is `sha256:` plus 64 lowercase hex characters, and it stays within 512\nfiles and 16 MiB. `\"dynamic\"` skills cannot be verified and are skipped.\n\nKept skills appear in `list_mcp_resources` with a `skill` field naming their\nfiles. Listing a skill never loads or activates it. Reading one of its files\nwith `read_mcp_resource` checks that the server returned exactly that URI with\nthe listed size and digest; a mismatch is refused and nothing is loaded. MCP\nskills are not yet merged into the native skill catalog, `/skills` or skill\nactivation.\n\n## Server Fields\n\nPer-server settings:\n\n- `command` (string, required for a local stdio server): executable to start.\n Remote servers use `url` instead; a URL-only entry is valid.\n- `args` (array of strings, optional)\n- `env` (object, optional)\n- `connect_timeout`, `execute_timeout`, `read_timeout` (seconds, optional). A per-server value overrides the global `timeouts` block, and each budget is independent of the others:\n - `connect_timeout` (default 30) covers spawn, `initialize` and the first `tools/list`, so a cold `uvx`/`npx` package download counts against it. A server launched through `npx`, `pnpx`, `bunx` or `uvx` without its own `connect_timeout` gets at least 120 seconds for that first download.\n - `execute_timeout` (default 1800) is the budget for each `tools/call` and `prompts/get`. An explicit shorter value is respected, and `read_timeout` never cuts a running tool short.\n - `read_timeout` (default 120) bounds each reply wait during the handshake and tool discovery, and is the budget for `resources/read`.\n - A request that runs out of budget fails with a timeout. The connection is kept, and a reply that arrives later is discarded instead of being handed to another call. Interrupting the turn stops the wait at once, but Codewhale does not send `notifications/cancelled` yet, so a server that handles one request at a time finishes the abandoned call before it answers the next one.\n - Streamable HTTP servers return the reply inside the POST itself; the request's own budget bounds that POST too. A request that expires while still sending closes the connection, which is rebuilt before the next call.\n- `disabled` (bool, optional)\n- `enabled` (bool, optional, default `true`)\n- `required` (bool, optional): startup/connect validation fails if this server cannot initialize.\n- `lazy` (bool, optional, default `false`): keep this server unstarted until a turn, tool call or `/mcp retry` asks for it. Without it, the server connects at session start. Plugin manifests have no `lazy` field: an enabled plugin's servers connect at session start, and disabling the plugin stops them.\n- `enabled_tools` (array, optional): allowlist of tool names for this server.\n- `disabled_tools` (array, optional): denylist applied after `enabled_tools`.\n- `always_allow_tools` (optional array): Exact tool names the person allows across workspaces on this computer. Set these in the global server configuration. Off takes precedence. Computer Use consent and scripts still require the person’s approval.\n- `url` (string, optional): Streamable HTTP endpoint for a remote MCP server.\n- `allow_private_network` (boolean, default false): operator opt-in for private DNS addresses on this configured origin; ignored for model-added servers.\n- `transport` (string, optional): set to `\"sse\"` for legacy SSE endpoints.\n- `headers` (object, optional): literal HTTP headers for URL-based servers.\n- `env_headers` or `env_http_headers` (object, optional): header names mapped to environment variable names.\n- `bearer_token_env_var` (string, optional): environment variable containing a bearer token.\n- `scopes` (array, optional): default OAuth scopes for `mcp login`.\n- `oauth.client_id` (string, optional): pre-registered OAuth client ID; requires `oauth.issuer`.\n- `oauth.issuer` (string, optional): exact authorization issuer pin; required for a pre-registered client.\n- `oauth.client_metadata_url` (string, optional): HTTPS URL with a non-root path for an operator-hosted client metadata document.\n- `oauth_resource` (string, optional): exact resource pin checked against SDK discovery.\n\n## Safety Notes\n\nMCP tools flow through the same approval framework as built-in tools. Read-only\nMCP helpers (resource/prompt listing and reads) can run without prompts in Ask\nand Auto-Review when policy permits, while side-effectful MCP tools require\napproval. Full Access does not bypass hard policy holds.\n\nYou should still only configure MCP servers you trust, and treat MCP server configuration as equivalent to running code on your machine.\nAvoid committing literal `Authorization` headers. Prefer `env_headers`,\n`bearer_token_env_var`, or OAuth login so secrets stay outside the MCP file.\n\n## Troubleshooting\n\n- Run `codewhale doctor` to confirm the MCP config path it resolved and whether it exists.\n- In the TUI, run `/mcp validate` to see each server's state from the last\n recorded MCP snapshot, plus any config load error and configured servers that\n have not connected.\n- If tools are missing from the model's catalog after a config or credential\n change, run `/mcp reload` — `/mcp validate` only reports the last recorded\n snapshot and never reconnects.\n- If the MCP config is missing, run `codewhale mcp init --force` to regenerate it.\n- If tools don’t appear, verify the server command works from your shell and that the server supports MCP `tools/list`.\n" } ] }, @@ -224,8 +224,8 @@ }, { "path": "docs/CONFIGURATION.md", - "sha256": "dccb4bcfb41e3d04abb256237817b6ef6ac310fc7c57f5f8829c62a9a7479092", - "content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get ` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify `,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\nNew Codewhale accounts are not open, and this guide does not recommend storing\nprovider keys in an account. Keep keys on your machine with `/provider` or\n`codewhale auth set --provider `; local keys and local models never\nneed an account.\n\nIf you already have an account, these commands manage keys saved there:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `codewhale account status`\ninspects it; `codewhale account logout` removes it for the selected `--profile`\nwithout removing your provider keys. Account sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `.bundle-backup--`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"\"` or pass\n`codewhale --provider `. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"\"`,\n`--provider `, or an AgentProfile `provider = \"\"` when the matching\n`[providers.]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save `) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change ` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers..model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers..model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.
]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions//runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`/.codewhale/config.toml`, or legacy\n `/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/.json` (or\n `$CODEWHALE_HOME/themes/.json`) and are selected with\n `/theme custom:`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open `), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set `.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `...` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers..auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove ` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest `, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`, but rebuilding the model-visible MCP tool pool\n requires restarting the TUI.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots///.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact `, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs ` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note ` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note ` | Append a note (legacy shorthand) |\n| `/note add ` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show ` | Show the full note at number `n` |\n| `/note edit ` | Replace note `n` with new text |\n| `/note remove ` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"\", \"event\": {…}}` with\n`Authorization: Bearer ` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `//control.sock` (mode `0600`;\n`` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach ` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach ` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n" + "sha256": "61fac67413aac35a4a67f410e174122a4ee09befbe2f2248fd3f9a6e4f08d99a", + "content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get ` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify `,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\nNew Codewhale accounts are not open, and this guide does not recommend storing\nprovider keys in an account. Keep keys on your machine with `/provider` or\n`codewhale auth set --provider `; local keys and local models never\nneed an account.\n\nIf you already have an account, these commands manage keys saved there:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `codewhale account status`\ninspects it; `codewhale account logout` removes it for the selected `--profile`\nwithout removing your provider keys. Account sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `.bundle-backup--`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"\"` or pass\n`codewhale --provider `. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"\"`,\n`--provider `, or an AgentProfile `provider = \"\"` when the matching\n`[providers.]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save `) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change ` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers..model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers..model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.
]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions//runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`/.codewhale/config.toml`, or legacy\n `/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/.json` (or\n `$CODEWHALE_HOME/themes/.json`) and are selected with\n `/theme custom:`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open `), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set `.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `...` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers..auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove ` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest `, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`; run `/mcp reload` to rebuild the model-visible\n MCP tool pool without restarting the TUI. A headless runtime still needs a\n restart because its `ConfigReload` request does not refresh MCP connections.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots///.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact `, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs ` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note ` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note ` | Append a note (legacy shorthand) |\n| `/note add ` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show ` | Show the full note at number `n` |\n| `/note edit ` | Replace note `n` with new text |\n| `/note remove ` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"\", \"event\": {…}}` with\n`Authorization: Bearer ` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `//control.sock` (mode `0600`;\n`` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach ` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach ` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n" } ] }, @@ -256,8 +256,8 @@ }, { "path": "docs/CONFIGURATION.md", - "sha256": "dccb4bcfb41e3d04abb256237817b6ef6ac310fc7c57f5f8829c62a9a7479092", - "content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get ` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify `,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\nNew Codewhale accounts are not open, and this guide does not recommend storing\nprovider keys in an account. Keep keys on your machine with `/provider` or\n`codewhale auth set --provider `; local keys and local models never\nneed an account.\n\nIf you already have an account, these commands manage keys saved there:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `codewhale account status`\ninspects it; `codewhale account logout` removes it for the selected `--profile`\nwithout removing your provider keys. Account sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `.bundle-backup--`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"\"` or pass\n`codewhale --provider `. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"\"`,\n`--provider `, or an AgentProfile `provider = \"\"` when the matching\n`[providers.]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save `) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change ` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers..model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers..model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.
]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions//runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`/.codewhale/config.toml`, or legacy\n `/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/.json` (or\n `$CODEWHALE_HOME/themes/.json`) and are selected with\n `/theme custom:`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open `), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set `.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `...` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers..auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove ` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest `, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`, but rebuilding the model-visible MCP tool pool\n requires restarting the TUI.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots///.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact `, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs ` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note ` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note ` | Append a note (legacy shorthand) |\n| `/note add ` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show ` | Show the full note at number `n` |\n| `/note edit ` | Replace note `n` with new text |\n| `/note remove ` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"\", \"event\": {…}}` with\n`Authorization: Bearer ` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `//control.sock` (mode `0600`;\n`` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach ` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach ` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n" + "sha256": "61fac67413aac35a4a67f410e174122a4ee09befbe2f2248fd3f9a6e4f08d99a", + "content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get ` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate from native subtotals and may overlap one another.\n\nInspection uses the existing session-size bound (16 MiB per record/frame and\nfor each retained query index). Unreadable, linked, oversized or incomplete\nrecords create visible gaps; an exhausted native index refuses the query\nwithout evicting evidence. Use a narrower time window where possible. Reading\nstatistics does not create a store, repair history, refresh pricing, start a\nmodel call or retire accounting receipts. Existing state-root overrides remain\nthe read boundary.\n\n`/cache calls [count]` shows recent retained native source receipts in the TUI;\nit defaults to 10 and displays at most 50. A source correction updates its row.\nThe view keeps frozen USD/CNY estimates and known subtotals separate from missing\nusage or prices; absent or contradictory cache classes show unknown. The header\ncounts retained sources, not physical requests or complete historical calls.\nThe command does not issue a model request or remove accounting receipts.\n\nPublished prices are frozen for DeepSeek's root, `/v1`, `/beta` and Anthropic\nendpoint variants, and for other providers' default endpoints. Each quote stays\nbound to the exact endpoint, identity, model and dispatch time when saved or replayed.\nOperator endpoint overrides do not become published pricing authorities, and\nsubscription or token-plan routes remain outside money coverage. Signed cloud\nfacts keep their separate route-scoping rules. Recognition as an official\ncredential endpoint alone does not authorize sharing prices across regions or plans.\n\nMissing request outcomes, omitted provider usage, unverified legacy usage and\nusage corrections have distinct cost reasons. Known limitation: saved receipts\nwithout a frozen price are not repriced from today's catalog; their complete\ncost remains unknown even after updating the binary.\n\n## Constitution, project instructions, and repo authority\n\nCodewhale has several instruction surfaces. They are deliberately separate so a\npersonal constitution, repo policy, project instructions, and runtime security\ncontrols do not blur together.\n\n- **Bundled global Constitution** — the compiled base law in the binary. It is\n the default floor for every session.\n- **User-global constitution** — the normal guided setup output. Manage it with\n `/constitution` or `/setup`; Codewhale stores structured data at\n `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`)\n and renders it into a separate `` prose block.\n This can express preferences and stop conditions, but it does not change\n runtime approval policy, sandbox, shell, network, trust, or MCP permissions.\n- **Repo-local constitution** — optional project policy in\n `.codewhale/constitution.json`, described below.\n- **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the\n canonical file for \"how should an agent work in this repo.\" Run `/init` to\n scaffold one. Instruction files written for other tools (`CLAUDE.md`,\n `.claude/instructions.md`, `.cursorrules`, `.github/copilot-instructions.md`\n and similar) are read only when you opt in with the top-level\n `project_instruction_imports` key, for example `[\"claude\"]`, `[\"cursor\"]` or\n `[\"all\"]` (see `config.example.toml`); Codewhale warns when it finds one it\n did not load.\n\n Codewhale reads one instruction file per directory, from the repository\n root down to the workspace, and puts your global instruction file (such as\n `~/.codewhale/AGENTS.md`) in front of them. The nearest scope reads last\n and wins: the workspace file overrides its parents, and project files\n override the global file. Global and project instructions are budgeted\n separately in estimated tokens (see [Instruction budget](#instruction-budget-instruction_budget-6526)\n below): a scope over its budget drops whole files from the broadest end\n first, and a single file larger than the remaining budget keeps its head\n with a marker that names the bytes and tokens dropped. A file keeps at most\n its first 100 KiB, and a file larger than `file_max_bytes` (8 MiB, lowerable\n only) is skipped with a notice. Every loss — trimmed, skipped, or refused —\n reaches the model and `/context report`.\n- **Memory and handoffs** — recalled state. Useful, but lower authority than\n constitutions and project instructions.\n\n### Managing the user-global constitution (`/setup` and `/constitution`)\n\nThe bundled **working agreement** is the safe default and no longer adds a\nrequired first-run screen. Customize it later through `/constitution` or the\nprogressive `/setup` guide. Provider/model readiness, workspace trust, and\nruntime posture stay separate from this guidance.\n\nOn the **Working agreement** (constitution) step:\n\n- **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again\n ratifies and saves a fresh structured `constitution.json`.\n- **`A`** (shown only when a provider is configured) asks your first configured\n model to draft the constitution. Drafting is **not** saving: the draft is\n rendered through the same preview and you still press **`G`** to ratify\n before anything persists.\n- **`K`** keeps your existing loaded constitution unchanged (shown only when a\n valid file is already present).\n- **`U`** (or `/constitution bundled`) records the bundled/default law.\n\n`/constitution` (alias `/law`) is the primary management surface once you are\nset up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the\nrepo-local law block), `explain`, `base`, `suggestions`, `ratify `,\n`migrate`, `rollback`, `edit`/`guided`, `repair`, `posture`, and `bundled`.\nManaging the constitution never changes runtime approval, sandbox,\nshell, network, trust, default mode, or MCP authority — those stay in runtime\nposture/config.\n\nEach repo can carry two distinct, complementary files:\n\n- **`AGENTS.md`** — ordinary project working instructions.\n- **`.codewhale/constitution.json`** — Codewhale-specific **repo authority /\n prioritization policy**: when local sources conflict, which should Codewhale\n trust first, and what to verify before claiming a task is done. `.codewhale/`\n lives inside the repo (like `.github/`). Example:\n\n ```json\n {\n \"schema_version\": 1,\n \"authority\": [\n \"current user request\",\n \"live code and tests\",\n \"GitHub issue/PR details\",\n \"AGENTS.md\",\n \"memory\",\n \"old handoffs\"\n ],\n \"protected_invariants\": [\n \"do not break old-session transcript replay\"\n ],\n \"branch_policy\": \"PRs target the integration branch, not main\",\n \"verification_policy\": {\n \"before_claiming_done\": [\"run focused tests\", \"read changed files back\"]\n },\n \"escalate_when\": [\n \"a destructive action was not explicitly authorized\"\n ]\n }\n ```\n\n All fields are optional. When present, the file is rendered into the system\n prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files\n are ignored and reported as migration-only diagnostics.\n\n Each `protected_invariants` entry may be either a plain string (advisory\n prose, the historical shape) or an object carrying path globs, which is\n additionally **mechanically enforced** in the tool gate. See\n [Enforced repo-law invariants](#enforced-repo-law-invariants) below.\n\n This is the **repo-local law** layer. When guidance conflicts, the\n **Whose word wins** section of the effective base constitution owns the\n ordering; the bundled source is\n [`BASE_PROMPT`](../crates/engine/src/prompts/text.rs). Use `/constitution base`\n to inspect the effective base, including any opted-in expert override.\n The order in which these files are described or assembled is not an\n authority ranking. Runtime policy (permissions, sandbox and cost limits\n enforced in code) is separate from prompt guidance; editing a constitution\n does not grant permissions.\n\n> **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`.\n> Codewhale no longer reads `WHALE.md` as project or global context. If one is\n> present, setup/context diagnostics report it as ignored so you can migrate it.\n> Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority\n> policy to `.codewhale/constitution.json`. Personal standing guidance belongs\n> in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global\n> Codewhale Constitution shipped in the model prompt is a separate thing and is\n> unaffected.)\n\n### Enforced repo-law invariants\n\nBy default a `protected_invariants` entry is advisory prose: it is rendered into\nthe prompt as guidance the agent should honor, but nothing stops a write. An\nentry written as an **object with `paths`** is different — it compiles into a\nmechanical write hold that the engine's tool gate evaluates before the write\nruns. The law becomes mechanism, not just a request.\n\nAn enforced entry has this shape:\n\n```json\n{\n \"schema_version\": 1,\n \"protected_invariants\": [\n \"Keep DeepSeek support first-class.\",\n {\n \"text\": \"The wire format is frozen; protocol changes need a human.\",\n \"paths\": [\"crates/protocol/**\"],\n \"action\": \"block\"\n },\n {\n \"text\": \"Release notes need human review.\",\n \"paths\": [\"CHANGELOG.md\"],\n \"action\": \"ask\"\n }\n ]\n}\n```\n\n- `text` — required. The reason surfaced on the hold. An empty `text` is skipped.\n- `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`,\n `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays\n advisory-only despite the object shape.\n- `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and\n Auto-Review; in Full Access it denies the protected write without opening a\n modal. `block` **denies the write outright** in every posture.\n\nSemantics:\n\n- **Tighten-only.** The schema has no allow/widen shape, so law can only *add*\n holds — a crafted constitution can never grant authority or weaken a gate\n above it.\n- **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold\n force-prompts in Ask and Auto-Review. Full Access never opens approval\n modals, so the same hold fails closed as a hard block; `block` always denies.\n Mode cannot turn a hold off.\n- **Repo-local only.** Only the repo's `.codewhale/constitution.json`\n participates. The user-global constitution stays advisory prose and never\n reaches this mechanism.\n- **Fails safe.** A missing file, parse error, or invalid glob degrades to\n fewer or zero rules — never a hold on unprotected paths and never a poisoned\n gate. Across matches the strongest action wins, so `block` outranks `ask`.\n- **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit\n event naming the invariant, the matched path, and the source file; the\n approval/denial reason names the invariant too.\n\n**Coverage is deliberately limited.** Holds are evaluated only for the write\ntools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only\nagainst the filesystem targets named in their inputs (`path`/`target`/\n`destination`/`file_path`, `changes[].path`, and unified-diff /\n`apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by\nrepo law — those writes are still governed by the ordinary approval, sandbox,\nand shell-write gates, not by this mechanism.\n\n### Expert full base-prompt override (#3638)\n\nThe global Constitution (the base system prompt, normally compiled in from\n`crates/engine/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user\nwithout rebuilding. This is\nan expert escape hatch, not the normal `/constitution` guided setup output.\nBecause this is a prompt trust boundary, it takes **two deliberate steps** — a\nfile alone is not enough:\n\n1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under\n `$CODEWHALE_HOME` when set).\n2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1`\n (`true`/`on`/`yes` also accepted).\n\nIf the file exists but the flag is unset, the override is **ignored** (with a\nlog line pointing to the flag) and the bundled Constitution stays in place.\nThis is intended for repurposing the TUI beyond software engineering — e.g.\nlong-form writing or document review — where the engineering-oriented base\nprompt is a poor fit. It is loaded once at startup; a **missing or empty file\nis a no-op**, so existing installs keep the bundled prompt.\n\nScope is deliberately narrow: only the byte-stable **base prompt segment** is\noverridable. Mode deltas, the approval policy, the tool taxonomy, Context\nManagement, and the Compaction Relay are still owned by Codewhale's runtime\nassembly, so an override **cannot remove safety-relevant guidance** (sandbox,\napprovals) — it only swaps the task/voice framing. To customize ordinary\npersonal behavior, prefer `/constitution`; to customize per-repo behavior,\nprefer `AGENTS.md` + `.codewhale/constitution.json` above.\n\n## Where It Looks\n\nDefault config path:\n\n- `~/.codewhale/config.toml`\n- Legacy fallback: `~/.deepseek/config.toml`\n\nOverrides:\n\n- CLI: `codewhale --config /path/to/config.toml`\n- Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml`\n- Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml`\n\nIf both are set, `--config` wins. Environment variable overrides are applied after the file is loaded.\n\n### TUI editability audit\n\nInside the TUI, run `/config audit` to see which documented keys can be changed\nfrom the current session, which ones can also be persisted, and which ones stay\nfile-only or restart-only. The audit includes current values for the high-impact\nruntime controls such as `approval_policy`, `allow_shell`,\n`stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the\n`[subagents]` concurrency/depth/timeout keys.\n\nUse the command's \"Command / reason\" column as the source of truth before\nediting by hand. For example, `/config approval_mode on-request --save` writes\ntop-level `approval_policy = \"on-request\"`, while provider base URLs are saved\nbut still require restarting the model client.\n\n### User workspace entries\n\nInteractive Agent sessions expose shell tools by default with approval gating\nunless you explicitly disable them. For a shell opt-in that should live in the\nuser's global config for noninteractive or durable-task profiles rather than in\nthe repository, add a workspace-scoped entry:\n\n```toml\n[workspace.'/absolute/path/to/project']\nallow_shell = true\n```\n\nThe entry applies only when the launched workspace path matches the table key.\nThe legacy `[projects.\"/absolute/path/to/project\"]` table is also accepted for\nthis user-owned override.\n\nIn interactive mode, the per-project overlay\n`/.codewhale/config.toml` is applied after this user entry. A\nproject-level `allow_shell = false` can still tighten the session; project-level\n`allow_shell = true` is ignored.\n\n### Per-project overlay (#485)\n\nWhen the TUI starts in a workspace that contains a regular-file\n`/.codewhale/config.toml`, the safe values declared in that file are\nmerged on top of the global config. Legacy\n`/.deepseek/config.toml` files are still read when the Codewhale path\nis absent. Symlinked project config files are rejected. This lets a repo suggest\na model or tighten local safety posture without touching the user's\n`~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for\none launch.\n\nSupported keys in the project overlay (top-level fields only):\n\n| Key | Effect |\n|---|---|\n| `model` | override `default_text_model` |\n| `reasoning_effort` | force `\"high\"` / `\"max\"` for a complex repo |\n| `approval_policy` | only values that tighten the user's current permission posture |\n| `sandbox_mode` | only values that tighten the user's current sandbox posture |\n| `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) |\n| `allow_shell` | `false` can disable shell access; `true` is ignored |\n\nThe overlay is intentionally narrow — it covers the fields a repo\nmaintainer is most likely to want to standardize across contributors.\nCredential, endpoint, provider-selection, MCP config, hooks, skills,\nretry, hotbar bindings, and `instructions = [...]` settings stay user-global.\nIf a repo-local config declares `api_key`, `base_url`, `providers`, `provider`,\n`mcp_config_path`, `notes_path`, `hotbar`, `allow_shell = true`, or `instructions`,\nCodewhale ignores that key and keeps the user's global setting.\n\nThe consolidated `codewhale` runtime uses `~/.codewhale/config.toml` for\nprovider and model settings. `codewhale auth set --provider deepseek` saves\nthe key in the local secret store (a private file under `~/.codewhale/secrets/`\nby default; the OS keychain is an explicit option), rather than writing it\ninto that config file. Legacy `~/.deepseek/config.toml` is migrated on first\nlaunch when needed. `codewhale --model deepseek-v4-flash` is forwarded to the\nTUI as `CODEWHALE_MODEL`. The dispatcher no longer writes `DEEPSEEK_*` twins;\na value you set yourself remains a legacy alias when its `CODEWHALE_*`\nequivalent is unset.\n\n### Account provider keys\n\nNew Codewhale accounts are not open, and this guide does not recommend storing\nprovider keys in an account. Keep keys on your machine with `/provider` or\n`codewhale auth set --provider `; local keys and local models never\nneed an account.\n\nIf you already have an account, these commands manage keys saved there:\n\n```bash\ncodewhale account keys list # configured status, never secret values\ncodewhale account keys remove deepseek # remove the account's saved key\n```\n\nLogin never uploads local keys, and removing an account key does not change\nlocal copies or revoke the key at the provider.\n\nThe account session is separate from provider keys. `codewhale account status`\ninspects it; `codewhale account logout` removes it for the selected `--profile`\nwithout removing your provider keys. Account sessions use the private `0600`\nCodewhale secrets file, scoped to the profile and account API origin; they do\nnot use the OS keychain. The older `codewhale cloud ...` spelling remains an\nalias for account commands.\n\n### Portable config bundles\n\n`codewhale config export --portable [--project] [--out FILE]` writes a\nportable, secret-free bundle of your configuration: sorted TOML with\ncredential and machine-specific keys (API keys, base URLs, socket paths)\ndropped, never a redacted placeholder in their place. Without `--out` the\nbundle goes to stdout. Typed tables, arrays, numbers, booleans, and datetimes\nremain typed. Machine-bound authority is deliberately non-portable: project\ntrust overlays, credential readers, auto-running hooks, executable LSP\ndefinitions, and local path bindings are omitted rather than copied to a new\nhost. So is trust posture (`yolo`, `allow_shell`, `approval_policy`,\n`sandbox_mode`, `sandbox_network_access`, and the sandbox read-denylist\npaths) and local endpoints or executables (`[lifecycle_outbox]`,\n`[extension_host]`, `[control_socket]`). URLs that carry a credential —\nuserinfo, a token-named query parameter, or a Slack/Discord/Teams webhook\npath — are treated as credentials.\n\n`codewhale config import [--dry-run] [--yes] [--project]`\napplies a bundle. The envelope is strict (`schema_version = 1`, kind\n`codewhale.portable-config`; unknown fields fail). Import prints a\ndeterministic plan — added / changed / skipped / conflicting / rejected —\nthen asks for consent unless `--yes` is given; headless use requires it.\nCredential-shaped entries are rejected by key name and by value shape;\nrejections name the field, never a value. Remote bundles come from HTTPS\nonly (loopback http excepted) with a 5 MiB cap. Application backs up the\ntarget document to `.bundle-backup--`, rolls back on any\nfailure, and re-importing an applied bundle changes nothing.\n\nImport also rejects the non-portable authority classes omitted by export,\nincluding nested/camel/dotted credential keys and cookie headers. This keeps a\nhand-authored or remote bundle from reintroducing machine trust, local\ncredential access, or automatically executable commands that a local export\nwould refuse to carry. Structured tables are deep-merged: a portable model or\npreference update does not erase target-local provider credentials, endpoints,\nhooks, or executable definitions that were deliberately omitted from the\nbundle. Arrays and scalar values still replace the corresponding portable\nvalue.\n\nSections map to scope: `[project]` entries land only in the workspace\ndocument (`--project`, which must target an actual workspace config),\n`[global]` only in the user-global one; `preferences`, `profiles`, and\n`plugins` apply at either scope. A global bundle operation refuses a workspace\ndocument just as a project operation refuses the user-global document.\n\n### Credential read precedence (#5197)\n\nCredential reads are **folder-independent by default**: every layer below is\nuser-global or process-scoped, so a key saved in one repo resolves identically\nin every other repo. Repo-local config never carries credential material — the\nproject overlay above reads only its allowlisted keys and ignores `api_key`,\nand a credential write aimed at a workspace-scoped config is rescoped to the\nuser-global `~/.codewhale/config.toml` (#5045, #5193).\n\nFor the active provider, the runtime resolves the API key in this exact order\n(first match wins):\n\n1. **Route-specific auth contract.** Routes whose `auth_mode` disables API\n keys stop here with no credential. OAuth routes use their explicitly\n granted token: the official `openai-codex` plan route accepts only\n Codewhale-owned credentials from `codewhale auth chatgpt`, bound to its\n issued registration and verified account. Imported Codex CLI and process\n tokens cannot authorize this route;\n `[providers.xai] auth_mode = \"oauth\"` reads Codewhale's own xAI\n device-login store (or a consent-granted Grok CLI file).\n2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins\n over every saved slot.\n3. **Config file `api_key`.** The `[providers.] api_key` table slot for\n the active provider. `deepseek-CN` also reads `[providers.deepseek]`. An\n older top-level `api_key` is read as `[providers.deepseek] api_key` (see\n [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key)).\n File-owned keys stay bound to their file-owned endpoint: when the\n environment replaces the route's base URL with a custom host, the saved\n key is not sent there.\n4. **`api_key_env` binding.** `[providers.] api_key_env = \"VAR\"` reads\n the named environment variable. For custom providers an unset or empty\n binding is a loud error, not a silent fallback (#5104).\n5. **Secret store.** The durable per-provider slot written by\n `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by\n default; the OS keyring only when explicitly selected). Skipped for named\n custom routes, self-hosted providers, custom endpoints other than an\n explicitly authenticated loopback, and routes whose `auth_mode` needs no\n key.\n6. **Ambient environment.** The provider's own variable\n (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …).\n Ambient keys are only ever sent to the provider's official endpoint and\n are skipped under the same conditions as the secret store.\n7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run\n with no credential; every other route fails with provider-specific setup\n guidance.\n\nLegacy compatibility: `~/.deepseek/config.toml` is migrated into\n`~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment\nvariables remain accepted aliases for the `CODEWHALE_*` forms, and\n`DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`.\n\nRun `codewhale auth status` to inspect the active provider's config\nfile, OS keyring backend, environment variable, winning source, and last-four\nlabel without printing the key itself. The command only probes the active\nprovider's keyring entry.\n\nFor hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native\nAnthropic providers, set `provider = \"\"` or pass\n`codewhale --provider `. The canonical provider IDs are `deepseek`,\n`nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`,\n`openrouter`, `orcarouter`, `xiaomi-mimo`, `novita`, `fireworks`,\n`siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`,\n`ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`,\n`anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`,\n`sakana`, `longcat`, `opencode-go`, `opencode-zen`, `meta`, `xai`,\n`mistral`, `telecomjs`, `modelstudio-token-plan`, `google`,\n`edenai`, `concentrate`, `codewhale`, and `custom` (a user-defined OpenAI-compatible endpoint via\n`[providers.]`).\nFor the provider-by-provider registry, including wire protocol, auth variables,\ndefault base URLs, model IDs, and capability metadata, see\n[PROVIDERS.md](PROVIDERS.md).\nThe facade saves provider credentials in the local secret store and forwards\nthe resolved key, base URL, provider, and model to the TUI process. Use\n`codewhale auth set --provider nvidia-nim --api-key \"YOUR_NVIDIA_API_KEY\"` or\n`codewhale auth set --provider openai --api-key \"YOUR_OPENAI_COMPATIBLE_API_KEY\"` or\n`codewhale auth set --provider atlascloud --api-key \"YOUR_ATLASCLOUD_API_KEY\"` or\n`codewhale auth set --provider wanjie-ark --api-key \"YOUR_WANJIE_API_KEY\"` or\n`codewhale auth set --provider xiaomi-mimo --api-key \"YOUR_XIAOMI_KEY\"` or\n`codewhale auth set --provider fireworks --api-key \"YOUR_FIREWORKS_API_KEY\"` or\n`codewhale auth set --provider siliconflow --api-key \"YOUR_SILICONFLOW_API_KEY\"` or\n`codewhale auth set --provider arcee --api-key \"YOUR_ARCEE_API_KEY\"` or the\nmatching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys\nthrough the facade. The generic `openai` provider defaults\nto `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to\n`gpt-5.6`. A custom OpenAI-compatible gateway can still select its own model\nexplicitly. `atlascloud` defaults to\n`https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses\n`deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets\nWanjie Ark's OpenAI-compatible endpoint at\n`https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`,\nand passes model IDs through unchanged because Wanjie model access is\naccount-scoped. SGLang, vLLM, and Ollama are\nself-hosted and can run without an API key by default. Ollama defaults to\n`http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b`\nor `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom\nURLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store\nunless API-key auth is explicitly requested; use an env var or config-file key\nwhen a local server does require bearer auth.\nOllama Cloud is the separate hosted `ollama-cloud` provider. It defaults to\n`https://ollama.com/v1` and `gpt-oss:120b`; save its key with\n`codewhale auth set --provider ollama-cloud`. Ambient auth reads\n`OLLAMA_CLOUD_API_KEY` first, then Ollama's official `OLLAMA_API_KEY`.\nSiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts\n`SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default.\n`provider = \"siliconflow-CN\"` selects the China regional default\n`https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and\n`SILICONFLOW_API_KEY` credential slot.\nArcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`,\nand uses `trinity-large-thinking` by default for Codewhale agent work.\n`trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's\n`arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while\nthe direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct\nArcee large-model API calls are tracked as 256K-context BF16 serving; Thinking\nis reasoning-capable, while Preview is not marked as a thinking model.\n\n### OpenRouter vendor pinning\n\nOpenRouter serves each model through several upstream vendors, and Codewhale\ncan pin requests to a vendor with `[providers.openrouter] vendor` (#6007):\n\n```toml\nprovider = \"openrouter\"\n[providers.openrouter]\nmodel = \"deepseek/deepseek-v4-pro\"\nvendor = \"deepinfra\" # copy the vendor slug from the model's OpenRouter page\n```\n\nThis sends `\"provider\": {\"order\": [\"deepinfra\"], \"allow_fallbacks\": false}`\non OpenRouter requests. A base slug can match multiple endpoint variants;\ncopy a full slug such as `deepinfra/turbo` to select one variant. An unavailable\npin fails at OpenRouter. Codewhale's separate `fallback_providers` setting can\nstill switch the whole route after a recoverable error.\n\nThe pin applies across OpenRouter models, including auxiliary requests on that\nroute. Set `vendor = \"\"` to clear it. Reload config or restart to apply edits;\nrequests already in flight keep their captured route. Other providers do not\ninherit the pin. `/preview-request` shows the primary request's routing fields.\n\nModel strings still pass through verbatim: `:floor` sorts by price, `:nitro`\nsorts by throughput, and `@preset/my-team-preset` references an account preset.\nSee OpenRouter's [provider routing](https://openrouter.ai/docs/guides/routing/provider-selection)\nand [presets](https://openrouter.ai/docs/guides/features/presets) documentation.\nCodewhale does not fetch per-vendor endpoint prices or availability; pinned\nusage reports a routing-dependent unknown cost instead of a catalog estimate.\n\n### Custom OpenAI-Compatible Gateways\n\nFor a single third-party service that implements the OpenAI Chat Completions\nAPI, the simplest setup is the built-in `openai` provider name pointed at the\ngateway:\n\n```toml\nprovider = \"openai\"\ndefault_text_model = \"your-model-id\"\n\n[providers.openai]\napi_key = \"YOUR_OPENAI_COMPATIBLE_API_KEY\"\nbase_url = \"https://your-gateway.example/v1\"\n```\n\nPut the endpoint under `[providers.openai]`, not the legacy top-level\n`base_url`, so the OpenAI-compatible provider receives it. `default_text_model`\nis the model ID sent to the gateway; `[providers.openai].model` can be used as\nthe OpenAI-provider-specific override.\n\nIf you keep several OpenAI-compatible gateways, or need a stable name for an\nAgentProfile provider pin, define a user-named custom provider table:\n\n```toml\nprovider = \"lm-studio\"\n\n[providers.lm-studio]\nkind = \"openai-compatible\"\nbase_url = \"http://127.0.0.1:1234/v1\"\napi_key = \"lm-studio\"\nmodel = \"qwen-2.5-7b\"\n```\n\nCustom provider names may be selected with `provider = \"\"`,\n`--provider `, or an AgentProfile `provider = \"\"` when the matching\n`[providers.]` table exists.\n\nA custom table sends Chat Completions requests by default. Add\n`wire = \"responses\"` to send OpenAI Responses requests to\n`/responses` instead, or `wire = \"anthropic\"` for Anthropic Messages:\n\n```toml\n[providers.my-gateway]\nkind = \"openai-compatible\"\nbase_url = \"https://gateway.example/v1\"\napi_key_env = \"MY_GATEWAY_API_KEY\"\nmodel = \"gpt-5.6\"\nwire = \"responses\"\n```\n\nStepFun has a first-class provider entry, so keep Coding Plan credentials and\nbase URL scoped to `[providers.stepfun]`:\n\n```toml\nprovider = \"stepfun\"\n\n[providers.stepfun]\napi_key = \"YOUR_STEPFUN_API_KEY\"\nbase_url = \"https://api.stepfun.ai/step_plan/v1\"\nmodel = \"step-3.7-flash\"\n```\n\n`/provider` setup asks which StepFun billing route the key belongs to —\npay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription\n(`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the\nendpoint you pick before saving it. The answer is written to\n`[providers.stepfun].base_url` and nowhere else. If that key already holds a\nbase URL Codewhale does not recognize as one of those two routes, the question\nis skipped and your value is left untouched.\n\nAlibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI\nprovider shape:\n\n```toml\nprovider = \"openai\"\n\n[providers.openai]\napi_key = \"YOUR_DASHSCOPE_API_KEY\"\nbase_url = \"https://dashscope-intl.aliyuncs.com/compatible-mode/v1\"\nmodel = \"qwen-plus\"\ncontext_window = 1000000\n```\n\nUse the regional DashScope `compatible-mode/v1` base URL that matches the\nregion of your API key. Codewhale keeps `qwen-plus` scoped to the `openai`\nprovider route and does not infer a different provider from the model prefix.\nThe same rule applies to all provider-prefixed model strings: a prefix such as\n`deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the\nselected provider, not an automatic switch to the DeepSeek provider.\nSet `context_window` to the gateway/model's real total context window when it\ndiffers from Codewhale's static model metadata. See\n[Context length (context window)](#context-length-context-window) for the full\nresolution order and for how to check which value is in effect.\n\nIf the gateway accepts `POST /chat/completions` but rejects\n`/v1/chat/completions`, set a provider-local `path_suffix`:\n\n```toml\n[providers.openai]\nbase_url = \"https://your-gateway.example/v1\"\npath_suffix = \"/chat/completions\"\n```\n\nThe suffix applies only to chat-completion requests. Model listing and\nDeepSeek beta paths keep their built-in routing so a generic gateway override\ndoes not accidentally rewrite `/models` or `/beta/completions`.\n\nFor private gateways with broken or intercepted certificates, use\n`SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key\n`insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can\nreport stale configs, but provider clients reject it instead of disabling TLS\ncertificate verification.\n\nLocal HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default\nwhen they use localhost or loopback addresses. For a non-local `http://`\ngateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network:\n\n```bash\nDEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale\n```\n\nThird-party OpenAI-compatible gateways that need extra request headers can set\n`http_headers = { \"X-Model-Provider-Id\" = \"your-model-provider\" }` at the top\nlevel or under a provider table such as `[providers.deepseek]`. When configured,\ncodewhale sends those custom headers on model API requests. The equivalent\nenvironment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated\n`name=value` pairs such as\n`X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization`\nand `Content-Type` are managed by the client and are not overridden by this\nsetting.\n\n### Vision Model\n\nCodewhale's chat provider and `image_analyze` tool are configured separately.\nThe main chat path remains the selected text/tool provider; image analysis runs\nthrough `[vision_model]` when the `vision_model` feature is enabled.\n\nXiaomi's current image-understanding docs include `mimo-v2.5` for image input.\nTo use MiMo for `image_analyze`, configure the vision model explicitly:\n\n```toml\n[features]\nvision_model = true\n\n[vision_model]\nmodel = \"mimo-v2.5\"\napi_key = \"YOUR_XIAOMI_KEY\"\nbase_url = \"https://api.xiaomimimo.com/v1\"\n```\n\nThe example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint.\nIf you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set\n`base_url` explicitly because this generic OpenAI-compatible block does not\nauto-select MiMo endpoints. Use\n`https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts,\n`https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or\n`https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts.\n\n### Auto Model Routing (`[auto]`, `[auto.router]`)\n\nWith `model = \"auto\"`, each turn runs on your **declared default model** unless\nyou have opted into something else. Auto never guesses a cheaper or stronger\nmodel from how a request is worded; the old keyword-and-length heuristic was\nremoved (`auto_route_declared_fallback` in `crates/engine/src/model_routing.rs`).\nTwo optional layers change that:\n\n- an `[auto.router]` classifier, which you write down, that picks a model per\n turn; and\n- `[auto] cost_saving`, which prefers the active provider's fast sibling.\n\nWith neither set, Auto is local and free: the turn uses the default model and\nno classifier call is made.\n\n**There is no default classifier.** With `[auto.router]` unset, no classifier\ncall happens, whatever keys you hold. Holding a DeepSeek key used to elect\n`deepseek-v4-flash` automatically. That was removed because it spent tokens on\na route the user never chose and privileged one provider over the rest\n(`AutoRouterConfig` in `crates/tui/src/config.rs`). Electing a network\nclassifier is now something you write down.\n\nPoint the classifier at any configured provider with `[auto.router]`:\n\n```toml\n[auto.router]\nprovider = \"zai\"\nmodel = \"glm-5-turbo\"\nthinking = \"off\" # optional; defaults to off\ntimeout_secs = 4 # optional; default 4, 0 = default, capped at 300\n```\n\nA classifier call happens only when `[auto.router]` names both `provider` and\n`model` *and* that provider has a key:\n`router_available = router_configured && has_api_key_for(...)` in\n`ModelInventory::from_config` (`crates/engine/src/model_inventory.rs`). If either\ncondition fails, or the classifier call errors or times out, the local\nfallback decides: the default model, or the fast sibling under `cost_saving`.\nThe turn's route receipt (Turn Inspector, Ctrl+Alt+O or `/turn inspect`, \"Model\nroute + tokens/cost\") records which path was taken, and a\nrouter you configured that cannot run or fails (missing key, HTTP error,\ntimeout, invalid answer) is shown as `Auto router: failing — …` rather than\nsilently ignored.\n\n#### Set up model routing\n\n`/router` (also `/model router`) opens one Router setup view with these\npresets. Each writes only `[auto.router]`; none is ever chosen for you.\n\n| Preset | What it writes | Cost and privacy |\n| --- | --- | --- |\n| `/router jev` | Jev, TypeSafe's decision model, over OpenRouter (`typesafe/jev-1.13`) or TypeSafe direct, whichever key you have | About $0.00002 per turn ($0.042 per million input tokens, output free). Your latest request and up to six recent context lines go to OpenRouter → TypeSafe (or TypeSafe). |\n| `/router fast` | The active provider's runnable fast tier with thinking off | Your existing key; the classifier sees the same request text. |\n| `/router off` | Removes `[auto.router]` | No router call; Auto turns use the default model, or the fast tier while `[auto] cost_saving = true` (Off leaves that setting alone). |\n| `/router custom` | Nothing; prints the TOML to edit | — |\n\nChoosing Jev or Fast makes **one test call** with a fixed sample request and\nshows the tier it picked, the probabilities and confidence, the latency and the\nprovider-reported cost. `Enter` (or `/router save `) then saves through\nthe normal config writer; `Esc` discards it. TypeSafe paused new signups on\n2026-09-22, so OpenRouter is the default route for new users. A TypeSafe key is\nread from `TYPESAFE_API_KEY`, the `typesafe` secret-store entry, or\n`[providers.typesafe] api_key` / `api_key_env`.\n\n#### Decision routers (`kind = \"decision\"`)\n\nA decision router asks a non-generative decision model typed Choice questions\nfor the active provider's `fast` and `strong` tiers and thinking level, then\nreads calibrated probabilities. No prose is parsed.\n\n```toml\n[auto.router]\nkind = \"decision\" # default \"chat\"\nprovider = \"openrouter\" # or \"typesafe\"\nmodel = \"typesafe/jev-1.13\" # \"~typesafe/jev-latest\" also works; TypeSafe direct: \"jev-latest\"\ntimeout_secs = 2\nmin_confidence = 0.5 # default 0.5, clamped to 0..1\n```\n\n- The router is called only when the active provider has a runnable strong/fast\n pair; otherwise there is no call and no spend.\n- An answer below `min_confidence` takes the local fallback. Under\n `[auto] cost_saving`, a `strong` answer also needs a probability of at least\n 0.75, or the turn stays on the fast tier.\n- An unknown `kind`, or a decision `provider` other than `openrouter` /\n `typesafe`, leaves the router unconfigured and shown as failing.\n- `[auto.router] thinking` is ignored for decision routers. Both routes settle tokens through\n the originating session's routed-usage ledger. TypeSafe is a named Custom route\n with unknown billing; its price is never borrowed from the active chat provider.\n Provider-reported cost remains verbatim on the decision receipt.\n- OpenRouter uses `POST /api/alpha/decisions`; TypeSafe uses `POST /v1/systemone`.\n The shared client validates Choice, Noul and Score against the offered questions\n and bounds responses to 256 KiB. Malformed answers fail open with usage retained.\n\n#### Shadow Decision Gate (experimental, off by default)\n\nThe Superfast Decision Gate asks a decision model three typed questions about\nthe latest user message — does it need a tool, can it be answered from the\nconversation, and what is its intent — and logs a conservative recommendation.\nIt is shadow-only: it never changes routing, never skips or delays the model\ncall, and fails open on any error, timeout or malformed answer. It uses the same\nSystem One client as the decision router above; there is no separate HTTP\nclient. It is configured from the environment and reads it when a turn starts:\n\n```sh\nSUPERFAST_ENABLED=1 # off unless set\nSUPERFAST_PROVIDER=typesafe # or openrouter; required when enabled\nSUPERFAST_BASE_URL=http://localhost:8000/v1 # optional TypeSafe-route base, e.g. self-hosted\nSUPERFAST_MODEL=jev-latest # default jev-latest / ~typesafe/jev-latest\nSUPERFAST_TIMEOUT_MS=150 # 1..=10000, default 150\n```\n\n- Enabling the gate never picks an endpoint by itself: without\n `SUPERFAST_PROVIDER` nothing is sent and a warning is logged.\n- The key comes from the same place the decision router reads it. The TypeSafe\n route always authenticates, so a self-hosted server that ignores auth still\n needs a placeholder `TYPESAFE_API_KEY`.\n- Only the latest user message is sent, truncated to 4,000 characters and\n redacted of configured secrets. The log (target `superfast`) carries the\n route, failure class and latency, never prompt text.\n- The gate retains the originating turn's accounting owner and cancellation.\n Its tokens settle through the shared ledger; missing usage or a cancelled/timed\n out request after dispatch creates an explicit coverage gap. Unknown TypeSafe\n pricing is recorded as unpriced rather than free.\n- Provider-reported cost survives rejected answers and late responses in bounded\n receipts on the originating turn or session. These are diagnostic evidence;\n unknown pricing never becomes an authoritative dollar total. Incomplete token\n counters record a coverage gap while preserving the reported raw evidence.\n\nThe wire contracts are documented in [TypeSafe's OpenAPI schema](https://api.typesafe.ai/openapi.json)\nand [OpenRouter's Decisions examples](https://openrouter.ai/blog/insights/what-is-jev/).\n\nThe Decision Gate concept and reference implementation are by Andrea Bruno,\nreleased under CC BY 4.0:\n[harness-superfast](https://github.com/Andrea-Bruno/harness-superfast).\n\nTwo `[auto]` keys shape routing (`AutoConfig` in `crates/tui/src/config.rs`):\n\n```toml\n[auto]\ncost_saving = false # default false\ncross_provider = false # default false\n```\n\n- **`cost_saving`** (default `false`). Without a classifier, Auto pins the\n active provider's validated fast sibling instead of the default model. A\n provider with no runnable fast sibling stays on the default. With a\n classifier, the classifier is told to prefer the fast tier for routine or\n ambiguous work and to pick the strong tier only for clearly agentic,\n multi-step, architecture, security or debugging work. Cost-saving never\n switches provider just to save money.\n- **`cross_provider`** (default `false`). Auto stays on the provider the session\n is configured to use. The classifier is only shown that provider's models,\n and the fallback never leaves it. Setting `cross_provider = true` lets the\n classifier choose among every runnable provider. There is no interactive\n toggle for `cross_provider`; it has to be set in config. A decision router\n always chooses within the active provider.\n\nTo bootstrap MCP and skills directories at their resolved paths, run `codewhale setup`.\nTo only scaffold MCP, run `codewhale mcp init`.\n\nNote: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`,\n`review`, and `eval` are all available from the installed `codewhale` command.\nThe consolidated dispatcher also provides `auth`, `config`, `model`, `thread`, `sandbox`,\n`app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`,\n`metrics`, `update`, `lane`, `workflow`, and `web`. Plain prompts enter the\nin-process TUI runtime. Release installers expose the same bytes as `codew`.\n\n### Startup Update Checks\n\nBy default, the TUI starts a background check for the latest stable Codewhale\nrelease and shows a short toast only when a newer release is available and the\nofficial release assets are complete. The check never blocks startup, never\nblocks a turn, and fails silently when offline.\n\nDisable the startup check entirely for air-gapped, corporate-proxy, or managed\ndesktop environments:\n\n```toml\n[update]\ncheck_for_updates = false\n```\n\n#### Automatic updates\n\n`mode` decides what happens when a newer release is found (release-binary\ninstalls only; package-manager and desktop-app copies always just notify):\n\n```toml\n[update]\nmode = \"auto\" # auto (default): download in the background, apply at next start\n # notify: tell me only; off: no checks\n```\n\n`codewhale config set update.mode notify` writes it. `CODEWHALE_UPDATE_MODE`\noverrides it, and an organization policy file overrides both; see\n[INSTALL.md](INSTALL.md#automatic-updates) for the full flow, signing, the\npolicy file and `codewhale update --rollback`.\n\n#### Throttling\n\nThe answer is cached in `~/.codewhale/update-check.json` and reused for\n`check_interval_hours` (default `6`). Only the *network request* is throttled —\nthe notice still appears on every launch while an update is outstanding. Set `0`\nto check on every launch.\n\n```toml\n[update]\ncheck_interval_hours = 6\n```\n\nA failed check keeps the last answer (so the notice stays) and retries about an\nhour later rather than on every launch, which keeps offline and rate-limited\nmachines quiet.\n\n#### Automatic suppression\n\nChecks are skipped, without contacting the network, when any of these is set to\na non-falsey value:\n\n| Variable | Why |\n| --- | --- |\n| `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. |\n| `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. |\n| `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. |\n\nValues of `\"\"`, `0`, `false`, `no`, and `off` do not count as set, so a\n`CI=false` export does not disable checks for ordinary users.\n\n#### Which update command is offered\n\nCodewhale never installs anything on its own — it only tells you an update\nexists. The command it names depends on how the running binary was installed,\ndetected from its path:\n\n| Install | Command offered |\n| --- | --- |\n| GitHub release binary (including Termux) | `codewhale update` |\n| npm (`node_modules` on the path) | `npm install -g codewhale@latest` |\n| Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade codewhale` |\n| `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` |\n\nFor package-managed installs the notice also warns against `codewhale update`:\nreplacing a binary Homebrew or npm owns leaves the manager describing a version\nthat is no longer on disk, and the next upgrade silently reverts you.\n\nOverride the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary`\nif you relocated the binary somewhere the path heuristics cannot read.\n\nTo redirect the startup check, set `update_uri` to an internal endpoint that\nreturns GitHub-compatible latest-release JSON. Minimal mirror metadata with a\n`tag_name` field is accepted; if `assets` are present, Codewhale requires the\nsame uploaded asset set as the official release before showing the toast.\n\n```toml\n[update]\ncheck_for_updates = true\nupdate_uri = \"https://internal.mirror.example/codewhale/releases/latest\"\n```\n\nWhen `update_uri` is not set, startup checks honor release mirror environment\nvariables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the\nofficial GitHub API endpoint. If a configured `update_uri` cannot be fetched or\nparsed and a release mirror env var is set, the TUI falls back to that mirror\ninstead of failing startup.\n\n#### What's new after an update\n\nThe first time Codewhale starts after its version goes up, it says so once and\nlinks that version's release notes on codewhale.net:\n\n```text\nUpdated to 0.10.2. Got 12 features, 31 fixes and 8 other changes.\nRelease notes: https://codewhale.net/en/changelog/0.10.2\n```\n\nThe counts come from the changelog compiled into the binary. Entries under\nAdded or New count as features; Fixed, Security and Latest fixes count as\nfixes; Contributors, Thanks, Community and Known limits or issues are not\ncounted; every other section counts as other changes. The link uses your UI\nlanguage where the site has it (`zh` for Chinese) and English otherwise.\n\nThe TUI shows the notice on the startup card and moves it into the transcript\nwhen the card leaves (at once when the session opens from `--resume`,\n`--continue` or a prompt), adding `/change ` for reading the notes in\nplace. The `doctor`, `models`,\n`sessions`, `receipts`, `setup`, `init`, `install`, `features`,\n`integrations`, `mcp`, `config`, `model`, `metrics`, `providers`, `account`,\n`login`, `logout` and `update` commands print it to stderr instead, when\nstdout and stderr are both terminals and no JSON output is requested. `exec`,\nservers, JSON output and non-terminal runs never show it; the next start that\ncan show it does.\n\nNothing is shown on a fresh install or after a downgrade. The version that\nlast started is recorded in `~/.codewhale/last-launch.json`. The notice reads\nonly local files, so it is independent of `check_for_updates`. To turn it off:\n\n```toml\n[update]\nshow_whats_new = false\n```\n\n## Workshop output budgets\n\nBy default an oversized tool result uses bounded spillover: a result\nunder the byte threshold stays inline, a larger one gets a head/tail\npreview plus a session artifact the model can read back. There is no\nsynthesis sub-agent and no per-call `raw = true` escape.\n\n`[workshop] large_output_threshold_tokens` and\n`[workshop.per_tool_thresholds]` (exact model-visible tool names such as\n`bash`) only take effect when the process opts in to adaptive evidence\nrouting with `CODEWHALE_ADAPTIVE_OUTPUT_ROUTING=1`; they set where a result\nstops being inline and becomes handle-only evidence.\n\nTwo optional byte ceilings (#5367) apply either way. They raise the\nmodel-visible floor and never lower it:\n\n- `read_result_max_bytes` — cap for a single `read` / `read_file`\n result. Absent keeps the compile-time defaults (100000 bytes for\n `read`, which has no line cap; 16KiB / 500 lines for `read_file`).\n For `read` this is the middle of a three-layer budget: the model's\n own per-call `max_bytes` (hard maximum 500000) raises the budget for\n one call, this setting raises the floor for the whole process, and\n either way 2MiB is the absolute ceiling. Highest wins; neither layer\n can lower a budget the other granted.\n- `tool_result_max_bytes` — cap for a generic tool result after\n spillover. Absent keeps the 12K-character compact floor (48K on\n windows ≥500K tokens). Hard cap is 2MiB.\n\n## Context length (context window)\n\nAlso called context size, context limit, max context, or window. This is the\ntotal token window Codewhale budgets against, and it drives the header/footer\ncontext percent, the auto-compaction trigger, context-pressure checks, and the\nrequest output cap. If Codewhale compacts at 128K on a model you know serves a\n1M window, this is the setting to change (#5134).\n\n**See what is in effect, and where the value came from.** Every one of these\nprints the resolved window *and* its source:\n\n- `/status` — a `Context window:` row with the percent and token counts, and a\n `Window source:` row naming the provenance and the exact key that overrides\n it.\n- `/config` → Provider — `Context window` (your override, or `(not set)`) and\n `Effective context window` (`1048576 tokens · configured`). Typing\n `context length` in the `/config` filter jumps straight to them.\n- `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`.\n- `/context json` — machine-readable `context_window_tokens` and\n `context_window_source`.\n\n**Change it** with the provider-table key `context_window`:\n\n```toml\n[providers.moonshot]\ncontext_window = 1048576\n```\n\nor from the CLI:\n\n```bash\ncodewhale config set providers.moonshot.context_window 1048576\ncodewhale config unset providers.moonshot.context_window # back to automatic\n```\n\nUse the table for the provider you are actually on (`providers.openai`,\n`providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The\nvalue is a positive token count for the route's *total* window.\n\nWhen one gateway fronts models with heterogeneous windows, scope the override\nto an exact wire model id with `[providers..model_context_windows]`:\n\n```toml\n[providers.command_code]\ncontext_window = 204800\n\n[providers.command_code.model_context_windows]\n\"MiniMaxAI/MiniMax-M2.5\" = 204800\n\"google/gemini-3.1-flash-lite\" = 1000000\n```\n\nKeys are the exact wire model ids the route sends (dotted and `org/model`\nspellings both work as TOML keys when quoted); each value must be a positive\ntoken count. A matching entry beats the provider-level `context_window` for\nthat model only — every other model on the provider still resolves against\n`context_window` and the rungs below. From the CLI:\n\n```bash\ncodewhale config set 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"' 204800\ncodewhale config unset 'providers.command_code.model_context_windows.\"MiniMaxAI/MiniMax-M2.5\"'\n```\n\n### How the effective window is resolved\n\nFirst match wins, and the source label each surface prints is exactly this\nrung:\n\n1. `configured (per-model)` — a `[providers..model_context_windows]`\n entry keyed by the route's exact wire model id. A hard override for that\n model only; it never rewrites another model's window.\n2. `configured` — `[providers.] context_window` in `config.toml`. A hard\n override for every model on the provider: nothing below it can raise or\n lower the result. Read-time aliases:\n `contextWindow`, `context_window_tokens`, `contextWindowTokens`,\n `context_length`, `contextLength`.\n3. `provider-reported` — route-scoped 1M metadata a provider actually reported\n for the Kimi Code `k3` route, when it was observed within the last 24 hours.\n4. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships,\n because 1M access is plan-gated (Allegretto and above).\n5. `catalog` — the bundled route catalog (hand-curated offerings first, then\n the bundled Models.dev rows). The official `openai-codex` roster supplies\n account-specific model choices, not context-window metadata. Refresh it\n with `codewhale models --update --provider openai-codex`; Codewhale does\n not read `$CODEX_HOME` for this route.\n6. `model-name hint` — an `_Nk` suffix parsed from the model name itself\n (`qwen3-32b-256k` → 256,000), vendor-agnostic. A naming convention the\n serving engine may not honor is not a fact about the route, so this rung\n sits *below* the catalog: any catalog row for the same id beats it (#5441).\n7. `fallback` — the static per-provider capability table: 200,000 for\n Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama,\n otherwise Codewhale's static per-model metadata, and finally 128,000 when\n the model is unknown.\n\n### What \"(unverified)\" means\n\nThe `model-name hint` and `fallback` rungs still drive real budgets — the\ncompaction trigger, the context meter, and the output reservation all use the\nnumber — but they are guesses, not capabilities anyone checked. Every surface\nthat renders one of these windows appends `(unverified)` to its source label\n(the status line, the context-pressure message, `/status`, `/config`, and the\nmodel picker chip), so a window you did not configure and no provider reported\ncan never read as a verified limit (#5239, #5441). The `context_window` and\n`model_context_windows` provider-table keys above are the fix: a configured\nwindow is a hard override and renders as `configured` (or\n`configured (per-model)`) with no marker.\n\nOutput ceilings follow the same rule (#5440): an Anthropic-family model the\ncatalog does not describe keeps the 64K Messages floor as its clamp, labeled\n`unverified` (or an \"assumed floor\") instead of `documented`. The official\nChatGPT plan preview does not accept `max_output_tokens`; Codewhale sends no\noutput-token cap on that route. A local request budget is not an upstream\nlimit or a guarantee about the completed response length.\n\nThere is no environment variable for the context window; the provider-table\n`context_window` and per-model `model_context_windows` keys are the user\nknobs. They are the right ones to set when a gateway or self-hosted runtime\nserves a window Codewhale's catalog does not model — per-model when only some\nof a provider's routes differ, provider-wide when they all do. Codewhale will\nnot invent a window it cannot justify — it falls back to a conservative value,\nlabels it `fallback`, and marks it `(unverified)` at every surface that shows\nit.\n\n### Adjacent knobs\n\n- `auto_compact_threshold_percent` (settings.toml; also accepted as\n `auto_compact_threshold`; `10`–`100`, default `80`): the share of the full\n route context window at which auto-compaction fires, clamped so it can never\n cross the spendable input ceiling after output reservation and headroom.\n Editable from `/config`. Raising the window without touching this raises the\n absolute compaction point along with it.\n- `auto_compact` (settings.toml, on/off): turns automatic compaction off\n entirely; `/compact` and Ctrl+L stay available.\n- `[compaction] summary_instructions` and\n `[compaction] retained_user_message_tokens` (config.toml): standing\n summarizer instructions and the verbatim user-message retention budget. See\n the `compaction.*` entry in the config-key reference below.\n- `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias\n `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap. Without an\n override, Codewhale starts at the safe `65536` request cap and intersects it\n with any smaller documented model or route ceiling; a catalog `max_output`\n such as DeepSeek V4's 384K remains a capability ceiling, not the amount every\n response requests. Explicit overrides are preserved within the resolved\n route context window and any route output ceiling, and preflight/emergency\n budgeting reserves the same effective value that can reach the wire. A\n separately documented route input ceiling also clamps preflight and\n compaction even when the total context window is larger. A blank canonical\n variable falls through to a nonblank legacy value; a nonblank invalid or\n zero canonical value is authoritative and falls back to the safe automatic\n default instead of activating a stale legacy setting. There is no\n `max_output_tokens` key in `config.toml`.\n\nBefore compaction replaces conversation history, Codewhale durably saves the\noriginal messages to the session's `artifacts/context-transfer-.json` and,\nwhen a model summary is produced, its handoff to the matching `.md` file.\nThese use the existing session artifact store and persistence redaction.\nA failed write aborts compaction without replacing context. Pruning-only passes\nsave the original messages without making an extra model call. The model can\nread them back through `context_archive_search` (ASCII case-insensitive\nsubstring search returning short snippets and an archive id) and\n`context_archive_get` (paged read of one message by id); the handoff note names\nboth so the model knows the archive exists. Pressure metadata\nshows estimated input tokens and the configured trigger; it is an estimate, not\nan exact promise about a provider's remaining context.\n\nCompaction history is available through `codewhale metrics` (or `--json`) and\n`audit.log` in the Codewhale home. Completed passes record their trigger,\nsummary/pruning path, message and estimated-token counts, effective threshold,\nand summarizer token usage. Automatic refusals are recorded once per turn with\ntheir reason. These are local diagnostics, not provider invoice totals; earlier\nartifacts are not retroactively counted. A text-mode `exec` that attempts\ncompaction saves its owning session at turn completion so its recovery artifacts\nremain discoverable. A killed process may leave artifacts without that final\nsession snapshot; the audit writer reports I/O failures instead of inventing data.\n\nSee [Settings File](#settings-file-persistent-ui-preferences) for the\ncompaction settings and [Token Quantities and\nDrivers](#token-quantities-and-drivers) for what each displayed token number\nactually measures.\n\n## Profiles\n\nYou can define multiple profiles in the same file:\n\n```toml\ndefault_text_model = \"deepseek-flash\"\n\n[providers.deepseek]\napi_key = \"PERSONAL_KEY\"\n\n[profiles.work.providers.deepseek]\napi_key = \"WORK_KEY\"\nbase_url = \"https://api.deepseek.com/beta\"\n\n[profiles.nvidia-nim]\nprovider = \"nvidia-nim\"\ndefault_text_model = \"deepseek-ai/deepseek-v4-pro\"\n\n[profiles.nvidia-nim.providers.nvidia_nim]\napi_key = \"NVIDIA_KEY\"\nbase_url = \"https://integrate.api.nvidia.com/v1\"\n\n[profiles.fireworks]\nprovider = \"fireworks\"\ndefault_text_model = \"accounts/fireworks/models/deepseek-v4-pro\"\n\n[profiles.siliconflow]\nprovider = \"siliconflow\"\ndefault_text_model = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.siliconflow.providers.siliconflow]\nbase_url = \"https://api.siliconflow.com/v1\"\n\n[profiles.openai-compatible]\nprovider = \"openai\"\n\n[profiles.openai-compatible.providers.openai]\nbase_url = \"https://openai-compatible.example/v4\"\nmodel = \"glm-5\"\n\n[profiles.atlascloud]\nprovider = \"atlascloud\"\n\n[profiles.atlascloud.providers.atlascloud]\nbase_url = \"https://api.atlascloud.ai/v1\"\nmodel = \"deepseek-ai/deepseek-v4-flash\"\n\n[profiles.sglang]\nprovider = \"sglang\"\n\n[profiles.sglang.providers.sglang]\nbase_url = \"http://localhost:30000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.vllm]\nprovider = \"vllm\"\n\n[profiles.vllm.providers.vllm]\nbase_url = \"http://localhost:8000/v1\"\nmodel = \"deepseek-ai/DeepSeek-V4-Pro\"\n\n[profiles.ollama]\nprovider = \"ollama\"\n\n[profiles.ollama.providers.ollama]\nbase_url = \"http://localhost:11434/v1\"\nmodel = \"qwen2.5-coder:7b\" # an installed chat-capable tag\n\n[profiles.ollama-cloud]\nprovider = \"ollama-cloud\"\n\n[profiles.ollama-cloud.providers.ollama_cloud]\nbase_url = \"https://ollama.com/v1\"\nmodel = \"gpt-oss:120b\"\n```\n\nSelect a profile with:\n\n- CLI: `codewhale --profile work`\n- Env: `DEEPSEEK_PROFILE=work`\n\nIf a profile is selected but missing, codewhale exits with an error listing available profiles.\n\nFor local Ollama, omit `providers.ollama.model` to select a tag whose native\ncatalog confirms completion support. `/provider ollama`, exec, and Runtime API\ndefault-model admission refresh that catalog; embedding-only catalogs have no\nautomatic chat default. An explicit model keeps its exact tag.\n\nA loaded model's `/api/ps` context observation is usable for 60 seconds.\n`/api/show` reports the trained maximum, which does not establish the served\nwindow. An unloaded model or an expired observation uses the unverified 8192-token\nfallback; explicit context-window configuration still takes precedence.\n\n## Legacy top-level `base_url` and `api_key`\n\nOlder releases kept DeepSeek's endpoint and key at the top of `config.toml`,\nand each reader decided for itself which other routes inherited them. They\nnow live in provider tables only. An older file keeps working unchanged:\nevery load reads the top-level keys as if they were already in their table,\nby one rule, per file and per `[profiles.]`:\n\n1. `provider = \"custom\"` with no `[providers.custom]` table: the endpoint, key\n and a copy of the model become `[providers.custom]`.\n2. An endpoint on another vendor's official host (for example\n `integrate.api.nvidia.com`, `xiaomimimo.com`, `openrouter.ai`, or the\n ChatGPT Codex endpoint) belongs to that vendor's table, so a DeepSeek route\n never sends requests there.\n3. Anything else belongs to `[providers.deepseek]`, which DeepSeek-CN also\n reads for its endpoint and key.\n\nThe key goes with DeepSeek (or the literal custom route). It follows the\nendpoint to another vendor only when the same table sets `provider` to that\nvendor, the host is that vendor's own, and the vendor's table has no key. When\nno `provider` is set, a NIM host still selects `nvidia-nim` and\n`api.deepseeki.com` still selects `deepseek-cn`. A `[vision_model]` without a\nkey of its own keeps using the top-level key. A profile's top-level\n`base_url` now overrides the base file's `[providers.deepseek] base_url`\n(before, the base table silently won).\n\nWhen a top-level value and its table disagree, the table's `base_url` and the\ntop-level `api_key` are used, which is what the runtime sent before.\n\nLoading never rewrites the file. The next save Codewhale makes (for example\n`codewhale config set`, `/config ... --save`, or `auth set`) moves the keys,\nkeeps comments, writes a one-time credential-free copy of the old file to\n`config.toml.pre-migrate.bak`, and prints one line saying what moved. A\ndisagreeing pair is never resolved in the file on its own; both values stay\nuntil you choose, and `codewhale config doctor` / `codewhale doctor` report\nit (sources only, never values). `codewhale config get|set|unset base_url`\n(and `api_key`) address the active provider's table.\n\nTo tidy the file yourself:\n\n```bash\ncodewhale config migrate --dry-run # show what would move\ncodewhale config migrate # move it (backup first)\ncodewhale config migrate --prefer top-level # resolve a conflict: keep the top-level value\ncodewhale config migrate --prefer table # resolve a conflict: keep the table value\n```\n\n## Environment Variables\n\nMost runtime environment variables override config values. API-key variables are\nfallbacks after saved config and keyring credentials.\n\nThe three user-facing slots — provider, model, base URL — expose `CODEWHALE_*`\naliases. When both forms are set the `CODEWHALE_*` value wins; the\n`DEEPSEEK_*` form is kept for older shells:\n\n- `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) —\n `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|ollama-cloud|huggingface|modelscope|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra|mistral`\n- `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider\n- `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider\n\n`CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to\nanother provider — a subagent or fleet child, a routed tool, the per-turn\nauto-router, a picker preview — resolves its endpoint from that provider's own\n`[providers.
]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`,\n`OPENAI_BASE_URL`, …), then that provider's default. It never inherits the\nactive session's host, and a custom route with no configured `base_url` fails\nclosed on a loopback placeholder rather than borrowing another provider's\nendpoint. The environment writes the value into the active identity's own\ntable: DeepSeek-CN falls back to `[providers.deepseek]` for its endpoint and\nkey, but never to a DeepSeek value the environment addressed to DeepSeek alone.\nA managed-config overlay that supplies or reselects the effective\nroute's endpoint takes the generic override away from every route.\n\nRemaining variables:\n\n- `CODEWHALE_LOCAL_PROXY_FALLBACK` (`0`) — keeps reqwest's build-time proxy snapshot; the loopback fallback is on by default. Read once when the first client is built.\n- `DEEPSEEK_API_KEY`\n- `DEEPSEEK_ANTHROPIC_BASE_URL`\n- `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs)\n- `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`)\n- `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`)\n- `DEEPSEEK_STREAM_OPEN_TIMEOUT_SECS` (connection setup + response-header wait in seconds; default `45`, clamped to `5..=300`; distinct from the per-chunk idle timeout; `stream.open_timeout_secs` (legacy `tui.stream_open_timeout_secs` fallback) wins when positive)\n- `CODEWHALE_CACHE_MAXIMAL` (`1`/`true`/`on`/`yes`) — cache-maximal context mode (#528). When on, the Repo Working Set block materializes the **full current contents** of the top active files into the system prompt each turn (deterministic order, byte-bounded), instead of only listing their paths. The block stays byte-stable while those files are unchanged so DeepSeek's KV prefix cache keeps hitting; editing a file cache-misses from its block onward. Off by default (path list only). Byte caps default to 24 KB per file / 96 KB total.\n- `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (when provider is `nvidia-nim`)\n- `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL`\n- `NVIDIA_NIM_MODEL`\n- `OPENAI_API_KEY`\n- `OPENAI_BASE_URL`\n- `OPENAI_MODEL`\n- `ATLASCLOUD_API_KEY`\n- `ATLASCLOUD_BASE_URL`\n- `ATLASCLOUD_MODEL`\n- `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY`\n- `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL`\n- `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL`\n- `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY`\n- `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL`\n- `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL`\n- `OPENROUTER_API_KEY`\n- `OPENROUTER_BASE_URL`\n- `OPENROUTER_MODEL`\n- `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY`\n- `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL`\n- `XIAOMI_MIMO_MODEL` or `MIMO_MODEL`\n- `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`,\n `token-plan-ams`, or `pay-as-you-go`)\n- `NOVITA_API_KEY`\n- `NOVITA_BASE_URL`\n- `NOVITA_MODEL`\n- `FIREWORKS_API_KEY`\n- `FIREWORKS_BASE_URL`\n- `FIREWORKS_MODEL`\n- `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`)\n- `MODELSCOPE_API_KEY`\n- `HUGGINGFACE_BASE_URL` or `HF_BASE_URL`\n- `HUGGINGFACE_MODEL` or `HF_MODEL`\n- `SILICONFLOW_API_KEY`\n- `SILICONFLOW_BASE_URL`\n- `SILICONFLOW_MODEL`\n- `ARCEE_API_KEY`\n- `ARCEE_BASE_URL`\n- `ARCEE_MODEL`\n- `TOGETHER_API_KEY`\n- `TOGETHER_BASE_URL`\n- `TOGETHER_MODEL`\n- `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY`\n- `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL`\n- `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL`\n- `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` (legacy process tokens; ignored by the official ChatGPT plan route)\n- `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL`\n- `OPENAI_CODEX_MODEL` or `CODEX_MODEL`\n- `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` (legacy identity hints; ignored by the official ChatGPT plan route)\n- `CODEWHALE_CHATGPT_NEW_ACCOUNT=1` explicitly replaces the single selected ChatGPT registration after a validated browser sign-in\n- `ANTHROPIC_API_KEY`\n- `ANTHROPIC_BASE_URL`\n- `ANTHROPIC_MODEL`\n- `ZAI_API_KEY` or `Z_AI_API_KEY`\n- `ZAI_BASE_URL` or `Z_AI_BASE_URL`\n- `ZAI_MODEL` or `Z_AI_MODEL`\n- `STEPFUN_API_KEY` or `STEP_API_KEY`\n- `STEPFUN_BASE_URL` or `STEP_BASE_URL`\n- `STEPFUN_MODEL` or `STEP_MODEL`\n- `MINIMAX_API_KEY`\n- `MINIMAX_BASE_URL`\n- `MINIMAX_MODEL`\n- `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN`\n- `DEEPINFRA_BASE_URL`\n- `DEEPINFRA_MODEL`\n- `MISTRAL_API_KEY`\n- `MISTRAL_BASE_URL`\n- `MISTRAL_MODEL`\n- `MOONSHOT_API_KEY` or `KIMI_API_KEY`\n- `MOONSHOT_BASE_URL` or `KIMI_BASE_URL`\n- `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL`\n- `SGLANG_BASE_URL`\n- `SGLANG_MODEL`\n- `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth)\n- `VLLM_BASE_URL`\n- `VLLM_MODEL`\n- `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth)\n- `OLLAMA_BASE_URL`\n- `OLLAMA_MODEL`\n- `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth)\n- `OLLAMA_CLOUD_BASE_URL`\n- `OLLAMA_CLOUD_MODEL`\n- `OLLAMA_CLOUD_API_KEY` (preferred Cloud key; `OLLAMA_API_KEY` is the official fallback)\nFor every product-level `CODEWHALE_*` variable below, the matching legacy\n`DEEPSEEK_*` name is still read as a compatibility fallback; when both are set,\nthe `CODEWHALE_*` value wins.\n\n- `CODEWHALE_TIMING=1` emits local JSON phase durations on stderr for startup, model preparation, tool planning, approval waits and postprocessing. Nested intervals overlap. The records contain only phase, process ID, end time and elapsed microseconds; stream-json stdout is unchanged.\n- `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs)\n- `CODEWHALE_SKILLS_DIR`\n- `CODEWHALE_MCP_CONFIG`\n- `CODEWHALE_NOTES_PATH`\n- `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on)\n- `CODEWHALE_MEMORY_PATH`\n- `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — anonymous usage\n counting is on by default in the current 0.9.12 source, with a disclosure\n naming Codewhale and PostHog and an easy durable opt-out. Prior explicit\n declines remain off. Accepts `0|1|true|false|yes|no|on|off|enabled|\n disabled`. An explicit \"off\" is a **floor**: it beats `--telemetry true` and\n `telemetry = true` in config, and a value this list cannot read also resolves\n to off, because a typo in a kill switch must never resolve to \"on\". See\n [`TELEMETRY.md`](TELEMETRY.md).\n- `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias)\n — `https://`, or plain `http://` only for loopback. Overrides the config file.\n Unset selects the shipped default,\n `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty\n string** routes batches to a local dry-run file and contacts nobody. Either\n way it only decides where a session sends — it cannot override an opt-out.\n- `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables)\n- `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`)\n- `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`)\n- `CODEWHALE_NO_NEW_PRIVS` (`0`/`false`/`no`/`off`/`disabled` opts out) — Linux only. The\n TUI process sets the kernel's irreversible no-new-privileges flag at startup\n as defense-in-depth, which blocks `sudo`/`su`/setuid helpers for Codewhale's\n whole process tree. The flag is already skipped when the startup sandbox\n mode resolves to `danger-full-access` (#5723), so this variable is the\n explicit override in the remaining cases: set it to a falsey value before\n launching if you administer through Codewhale as a wheel-group user under a\n narrower posture and need escalation to work (#5413), or set a truthy value\n to force the flag on even under `danger-full-access`. Unset, the posture\n decides; the other startup hardening (no ptrace, no core dumps) always\n stays on.\n- `CODEWHALE_MANAGED_CONFIG_PATH`\n- `CODEWHALE_REQUIREMENTS_PATH`\n- `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`)\n- `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default\n `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the\n legacy directory exists)\n- `CODEWHALE_RUNTIME_DIR` (override the runtime thread store root). Interactive\n sessions default to `$CODEWHALE_HOME/sessions//runtime` so each\n Codewhale process owns its own store (#5630). The store is single-owner: a\n second process on the **same** root fails at startup. Set this variable to\n share one store across processes, or when the runtime API server should use a\n stable non-session path. Unset, the API/server path remains\n `$CODEWHALE_HOME/tasks/runtime`. Legacy alias: `DEEPSEEK_RUNTIME_DIR`.\n- `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject)\n- `CODEWHALE_FORCE_HTTP1` (`1|true|yes|on` pins the HTTP client to HTTP/1.1, disabling HTTP/2; useful on Windows or behind proxies that mishandle long-lived H2 streams)\n- `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`).\n If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`;\n the old env var is not used for new Codewhale state paths.\n- `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update`\n and by TUI startup update checks when `[update].update_uri` is not set, or as\n a fallback when that configured URI cannot be fetched)\n- `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses\n `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations`\n fallback when only the legacy directory exists)\n- `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and\n `fancy_animations = false` at startup, regardless of the saved\n settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)).\n- `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users\n point this at a PEM bundle (or single DER cert) and the cert(s)\n get added alongside the platform's system trust store. Failures\n log a warning and continue — the existing system roots still\n apply.\n\n### Instruction sources (`instructions = [...]`, #454)\n\nAdd a list of additional system-prompt sources that get\nconcatenated, in declared order, alongside the auto-loaded\n`AGENTS.md`:\n\n```toml\ninstructions = [\n \"./AGENTS.md\",\n \"~/.codewhale/global.md\",\n \"~/team/agents-shared.md\",\n]\n```\n\nRules:\n\n- Paths run through `expand_path` so `~` and env vars work.\n- These entries are **global-scope** instruction authority: they are charged\n to the global budget together with the global `AGENTS.md`/`instructions.md`\n fallbacks and `~/.claude/CLAUDE.md`.\n- A file keeps at most its first 100 KiB. A file larger than\n `[instruction_budget] file_max_bytes` (8 MiB by default) is skipped with a\n notice. Missing and empty files are skipped with a notice so a stale entry\n doesn't fail the launch.\n- Only user-owned config, profiles, and managed config may set this array.\n Project config (`/.codewhale/config.toml`, or legacy\n `/.deepseek/config.toml`) ignores `instructions` so a cloned repo\n cannot choose arbitrary local files to place into the prompt.\n\n### Instruction budget (`[instruction_budget]`, #6526)\n\nStanding instruction files are assembled into two independent scopes and\nbudgeted in **estimated tokens** — the same conservative estimator `/context`\nuses, never a `bytes/4` shortcut, which underestimates Cyrillic by 30–50%.\n\n- **global** — `~/.codewhale/AGENTS.md` (or the `.agents`/`.deepseek`\n fallbacks), the global `instructions.md` fallbacks, `~/.claude/CLAUDE.md`\n as a last resort when `claude_fallback` is on, and the\n `instructions = [...]` array.\n- **project** — the repository-root → workspace `AGENTS.md` chain, co-read\n foreign files, and the `.codewhale/rules`/`.claude/rules` block.\n\nEach scope is capped at `min(absolute_tokens, percent × usable_window)`. The\nusable window is the route window minus the output reservation the request\nwill actually carry and a safety reserve of 10% of the window (minimum 2,048\ntokens). A route whose context window is unknown has no usable window: the\npercentage ceilings and the guard are reported as `unknown` and only the\nabsolute caps apply.\n\n```toml\n[instruction_budget]\nglobal_budget_tokens = 100000\nproject_budget_tokens = 100000\nglobal_budget_percent = 25\nproject_budget_percent = 25\nstatic_prefix_guard_percent = 80\nfile_max_bytes = 8388608\npolicy = \"trim\"\nclaude_fallback = false\n```\n\nWith a 64K (65,536-token) output reservation:\n\n| window | usable | per scope | both scopes | guard 80% |\n| --- | --- | --- | --- | --- |\n| 128,000 | 49,664 | 12,416 | 24,832 | 39,731 |\n| 1,000,000 | 834,464 | 100,000 (absolute cap) | 200,000 | 667,571 |\n\nBehaviour:\n\n- **Nothing is trimmed while a source is within its scope budget.**\n- A scope over budget drops whole files from the broadest end first — the\n root-most ancestor goes first, the workspace file last — and a single file\n that alone exceeds the remaining budget keeps its **head** and drops its\n tail with a marker carrying bytes and estimated tokens.\n- Every instruction, rules, and configured file retains at most its first\n 100 KiB of text, ending in a marker that names the bytes kept and dropped.\n- Rules are charged to the project scope after the instruction chain and keep\n a head-first trim.\n- `file_max_bytes` is a hard ceiling that can only be lowered from its 8 MiB\n default: a larger file is **skipped**, never trimmed, and the read is\n bounded on the open file, so a FIFO or a growing file cannot exhaust memory.\n The same applies to a whitespace-only file and to files beyond the\n 50-per-directory rules cap.\n- `policy = \"trim\"` (default) trims and reports. `policy = \"refuse\"` stops the\n request before dispatch when a scope budget, the rules budget, or the\n 100 KiB retention would trim, or when the request prefix exceeds the guard;\n skips stay skips, because one bad file must not make the project unopenable.\n- `static_prefix_guard_percent` caps the request prefix: every system block\n plus the tool schemas the request carries. When it trips, the two scopes are\n narrowed largest first until the prefix fits, only the installed instruction\n block is replaced, and the change is recorded as a declared prefix change.\n If the prefix still exceeds the guard afterwards, the residual excess is\n reported rather than looped on. The guard must leave at least 10 points\n above the two scope percents. Prompts owned by a host (child agents,\n isolated chat, RLM) are measured and can be refused, but are never trimmed.\n- `claude_fallback = true` (opt in; default `false`): a directory with no\n `AGENTS.md` (or codewhale `instructions.md`) uses its `CLAUDE.md`. A skipped\n `AGENTS.md` counts as absent; a file already loaded from a wider scope (a\n symlink) is a reported duplicate, not a fallback trigger. Other tools'\n instruction files are never imported unless you opt in here or through\n `project_instruction_imports`.\n- Every loss — trimmed, skipped, refused — reaches the model as one\n runtime-provenance message whenever the list changes, and appears per source\n in `/context report`. `codewhale doctor`'s report is a preview built from the\n current files and config; it does not claim a request passed the tool-schema\n guard.\n- The table is **user config only**, like `instructions`: project config is\n ignored and a cloned repository cannot shrink the global budget or force\n `refuse`. An invalid table (unknown key, zero budget, percent outside\n 1–100, guard without room left, `file_max_bytes` outside 1–8388608) fails\n the launch.\n\n### Hooks\n\nHooks are a **TUI runtime feature**. They fire from the interactive TUI and\nthe engine turn loop it drives; `codewhale exec`, the CLI subcommands, the\napp-server / ACP surfaces, and the `workflow` tool do not fire them.\n\n[`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook\nevents — their firing points, environment variables, stdin payloads, timeout\nand background semantics, and which three of them can steer Codewhale. The\nsections below cover the configuration surface and the steering contracts in\nmore depth.\n\nTwo contract points worth reading there before writing a hook:\n\n- `background = true` means **submitted and never awaited**. The hook still\n gets the documented stdin payload and the same timeout, but it has no exit\n code and cannot steer.\n- A condition that references context its event never carries (an `exit_code`\n condition outside `tool_call_after` / `on_error`, a `mode` condition on\n `shell_env`, a tool condition on a non-tool event) is **rejected at load**,\n logged, and shown in `/hooks list`. It does not silently never match.\n Rejection is per entry, so a broken hook never drops another one that merely\n shares its `name` or is likewise unnamed.\n\n### `/hooks` listing\n\nRun `/hooks` (or `/hooks list`) inside the TUI to see every\nconfigured lifecycle hook grouped by event, including each\nhook's name, command preview, effective timeout, and condition. When\n`[hooks].default_timeout_secs` is set it replaces every per-hook\n`timeout_secs`, and the listing shows that effective value and names the\noverride rather than echoing the per-hook number. A\n`default_timeout_secs = 0` is rejected at load — it would expire every hook\nin the config immediately — so the override is ignored, per-hook\n`timeout_secs` applies, the listing shows that per-hook value with no\noverride provenance, and the rejection appears under `configuration\nproblems`. The\n`[hooks].enabled` flag's state is shown at the top so it's\nobvious when hooks are globally suppressed, and any entry rejected\nat load is listed under `configuration problems` with the reason.\nHooks are configured under `[[hooks.hooks]]` entries — see\n[`docs/HOOKS.md`](HOOKS.md) for the full schema.\n\n### Mutable `message_submit` hooks\n\n`message_submit` hooks run before a submitted message is added to\nhistory or sent to the model. Unlike observer-only lifecycle hooks,\nnon-background `message_submit` hooks can replace or block the\nsubmitted text.\n\n```toml\n[[hooks.hooks]]\nevent = \"message_submit\"\ncommand = \"~/.codewhale/hooks/inject-context.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe hook receives JSON on stdin:\n\n```json\n{\n \"event\": \"message_submit\",\n \"text\": \"original user text\",\n \"text_bytes\": 18,\n \"text_original_bytes\": 18,\n \"text_truncated\": false,\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234\n}\n```\n\nThe entire serialized document is capped at 32 KiB. Codewhale retains the\nlargest UTF-8-safe `text` prefix that fits after JSON escaping and bounded\nmetadata, and the three `text_*` fields make truncation explicit. Immediate\nmessages, restored queue entries, merged steers, and prior-hook replacements\nall cross this same serialization boundary.\n\nIf the hook exits `0` and prints JSON with a non-empty string `text` field,\nthat value replaces the submitted text:\n\n```json\n{ \"text\": \"replacement user text\" }\n```\n\nExit `0` with empty stdout, or stdout JSON without `text`, leaves\nthe current text unchanged. A JSON `text` field must not be empty;\n`{\"text\":\"\"}` is treated as invalid stdout and ignored. Exit `2`\nblocks the submission before the turn starts; a structured `reason` field can\nprovide the bounded, redacted status message shown in the TUI. Raw stdout,\nstderr, and process-error text are not copied into denial receipts.\nOther non-zero exits follow the hook's `continue_on_error` setting.\nTimeouts and spawn failures are also surfaced as transient TUI status\nmessages when `continue_on_error = true` lets submission continue.\n\nMultiple `message_submit` hooks run in config order, and each hook\nreceives the text produced by the previous hook. Hooks marked\n`background = true` are observer-only and cannot transform or block\nthe message — they still receive the same stdin payload and the same\nenvironment, they are simply never awaited. Existing environment\nvariables remain available.\n`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;\nJSON stdout contracts exist for `message_submit` (above) and\n`tool_call_before` (below).\n\n### `tool_call_before` decision hooks\n\n`tool_call_before` hooks run before each tool call executes. In\naddition to the legacy hard deny (exit code `2`, which always wins\nregardless of stdout), a foreground hook may print a JSON decision on\nstdout with exit code `0`:\n\n```json\n{\n \"decision\": \"allow\" | \"deny\" | \"ask\",\n \"reason\": \"human-readable explanation (used for deny)\",\n \"updatedInput\": { \"command\": \"ls -la\" },\n \"additionalContext\": \"text appended to the tool result for the model\"\n}\n```\n\nAll fields are optional. Empty stdout, non-JSON stdout, and JSON\nwithout a `decision` field behave exactly as before (allow). An\nunrecognized `decision` string logs a fixed warning without echoing the\nuntrusted value and is treated as allow.\n\n- `deny` blocks the tool; the model receives a permission-denied tool\n result containing `reason`.\n- `ask` forces the interactive approval prompt in Ask and Auto-Review even for\n tools that would otherwise auto-run. Full Access does not open tool-approval\n prompts, so hook `ask` does not downgrade that posture.\n- `updatedInput` must be a JSON object; it replaces the tool input\n before execution. When several hooks supply it, the last hook wins.\n- `additionalContext` is appended to the tool result sent back to the\n model as `[hook context] ...`. Multiple hooks' contexts are\n concatenated.\n\nWhen multiple hooks match, precedence is deny > ask > allow. Hooks\nmarked `background = true` cannot steer tool calls — they are\nsubmitted and never awaited, so they have no verdict to contribute.\n\nA foreground hook that produced no verdict at all — it hit its timeout, the\nprocess could not be started, or a strict process exited non-zero without an\nexplicit JSON decision — is not treated as permission. If\n*that* hook is configured with `continue_on_error = false`, the outcome\ndenies the tool call and the denial names the hook and a bounded reason.\nStrictness is read off the hooks that actually matched this call, so a\nstrict gate scoped to another tool cannot deny it, and a lenient hook's\ntimeout does not deny merely because a strict hook exists elsewhere in\nconfig. Under the default `continue_on_error = true` the outcome is\nlogged and the call proceeds.\n\n`reason` and `additionalContext` are capped (2 000 characters per field,\n8 000 for the concatenated context of one call) and stripped of control\ncharacters before they reach the TUI or the model.\n\nExample deny hook:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"blocked by project policy\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nExample ask hook (force approval for every MCP tool):\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"ask\"}' '''\ncondition = { type = \"tool_name\", name = \"mcp__*\" }\n```\n\nExample input rewrite:\n\n```toml\n[[hooks.hooks]]\nevent = \"tool_call_before\"\ncommand = \"~/.codewhale/hooks/clamp-shell-timeout.sh\"\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\nwhere the script reads the hook context, then prints\n`{\"updatedInput\": {...}}` with the adjusted arguments.\n\n`tool_name` conditions support `*` globs: `mcp__*` matches every MCP\ntool (e.g. `mcp__github__create_issue`) but not built-ins like\n`read_file`; exact names keep matching exactly. Other regex\nmetacharacters in the pattern are matched literally.\n\n### Project-local hooks\n\nRepositories can ship policy in `/.codewhale/hooks.toml`,\nusing the same shape as the `[hooks]` table (top-level fields plus\n`[[hooks]]` entries). Project hooks are executable shell\nconfiguration, so Codewhale only loads them after the workspace has\nbeen trusted in user-owned config through the trust prompt or a\n`[projects.\"\"] trust_level = \"trusted\"` entry. Session\n`/trust on` mode does not enable repo-supplied hooks by itself, and\nrepo-local legacy markers such as `.deepseek/trusted` do not enable\nproject hooks. Once trusted, project hooks are appended after global\nhooks from `config.toml`, so they run last and, for `updatedInput`,\nwin ties. A malformed trusted project file logs a warning and startup\nfalls back to global hooks only.\n\n```toml\n# .codewhale/hooks.toml\n[[hooks]]\nevent = \"tool_call_before\"\ncommand = '''echo '{\"decision\":\"deny\",\"reason\":\"no shell in this repo\"}' '''\ncondition = { type = \"tool_name\", name = \"exec_shell\" }\n```\n\n### Turn-end observer hooks\n\n`turn_end` hooks observe the end of each model turn after post-turn\nstate, usage totals, cost accounting, notifications, receipts, and\nqueue recovery have been updated. They receive JSON on stdin and are\nobserver-only: stdout is ignored, failures are logged as warnings, and\nthe hook cannot block user input, mutate the transcript, or change the\nnext queued follow-up.\n\nObserver-only UI events share one 32-entry queue and two persistent workers;\nthe terminal loop uses non-blocking submission and does not create a thread per\nevent. A full queue or unavailable dispatcher drops that observer event and is\nkept as an event-specific error toast, independent of ordinary agent/turn\nstatus text.\n\n```toml\n[[hooks.hooks]]\nevent = \"turn_end\"\ncommand = \"~/.codewhale/hooks/turn-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\nThe payload includes common hook metadata plus post-turn accounting:\n\n```json\n{\n \"event\": \"turn_end\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"created_at\": \"2026-07-12T10:30:00+00:00\",\n \"model_backed\": true,\n \"provider\": \"deepseek\",\n \"model\": \"deepseek-chat\",\n \"billing_surface\": null,\n \"turn_id\": \"turn_12345678\",\n \"status\": \"completed\",\n \"error\": null,\n \"duration_ms\": 1834,\n \"usage\": {\n \"input_tokens\": 1200,\n \"output_tokens\": 180,\n \"prompt_cache_hit_tokens\": 900,\n \"prompt_cache_miss_tokens\": 300,\n \"prompt_cache_write_tokens\": 0,\n \"reasoning_tokens\": null,\n \"reasoning_replay_tokens\": null\n },\n \"totals\": {\n \"session_tokens\": 1380,\n \"conversation_tokens\": 1380,\n \"input_tokens\": 1200,\n \"output_tokens\": 180\n },\n \"tool_count\": 2,\n \"queued_message_count\": 1,\n \"stop_hook_active\": false\n}\n```\n\n`created_at` anchors time-window pricing; `provider` and `model` identify the\neffective route used for model-backed turns. `billing_surface` is an optional,\nnon-secret classification derived from the endpoint that actually served the\nturn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw\nbase URL is never written to hook or runtime records. Runtime `TurnRecord`\nexports call the same field `effective_billing_surface`, which `scorecard`\naccepts as an alias. This keeps subscription quota separate from token-priced\nusage. Unrecognized and custom endpoints remain `null` and unpriced.\n\nShell-only lifecycle completions set `model_backed` to `false` and may report a\n`null` provider; offline scorecards exclude those records from model token and\ncost totals. Completion-only shell, manual-compaction, and purge events that do\nnot have a matching `TurnStarted` retain the observer notification with a\nsynthetic `lifecycle_` turn id and the time the completion was observed.\n\nFor `interrupted` or `failed` turns, `status` reflects that terminal\nstate and `error` carries the engine error string when one is available.\n`stop_hook_active` is reserved for future re-entry protection and is\ncurrently always `false`.\n\n### Sub-agent lifecycle hooks\n\n`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle\nevents. They receive bounded JSON metadata on stdin and are observer-only:\nhook failures are logged as warnings and do not block sub-agent scheduling,\nchange prompts, or change results. For these observer events,\n`continue_on_error` has no effect: later matching hooks still run even when an\nearlier hook exits non-zero.\n\n```toml\n[[hooks.hooks]]\nevent = \"subagent_complete\"\ncommand = \"~/.codewhale/hooks/subagent-audit.sh\"\ntimeout_secs = 2\ncontinue_on_error = true\n```\n\n`subagent_spawn` receives:\n\n```json\n{\n \"event\": \"subagent_spawn\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"prompt_preview\": \"bounded prompt preview\",\n \"prompt_truncated\": false\n}\n```\n\n`subagent_complete` receives the same common fields plus terminal metadata:\n\n```json\n{\n \"event\": \"subagent_complete\",\n \"agent_id\": \"agent_12345678\",\n \"session_id\": \"sess_12345678\",\n \"workspace\": \"/path/to/workspace\",\n \"mode\": \"agent\",\n \"model\": \"deepseek-chat\",\n \"total_tokens\": 1234,\n \"status\": \"completed\",\n \"result_preview\": \"bounded result preview\",\n \"result_truncated\": false\n}\n```\n\nPreviews are capped before delivery so lifecycle hooks do not receive full\nsub-agent prompts, transcripts, or unbounded results. Use the transcript handle\nreturned by `agent` when full sub-agent details are needed.\n\n### Running-turn input\n\nComposer shortcuts keep the same role throughout a session:\n\n- **Enter** sends when idle and queues a next-turn follow-up while busy. The\n behavior does not change before versus after the provider's first token.\n- With an empty composer and queued follow-ups visible, **Enter** sends the\n oldest queued follow-up into the active turn now.\n- **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly\n steers the active turn. It sends normally when idle.\n- By default, **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** insert a newline.\n- Set `composer_multiline_mode = true` to make **Enter** insert a newline and\n **Alt+Enter** send instead. **Shift+Enter** also sends when the terminal\n reports it distinctly; legacy terminals may report it as plain **Enter**.\n **Ctrl+J** still inserts a newline, and supported **Ctrl+Enter** /\n **Cmd+Enter** behavior is unchanged.\n- **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer.\n\n### Composer stash (`/stash`, Ctrl+G / Ctrl+S)\n\nPress **Ctrl+G** in the composer to park the current draft to\n`~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked\ndrafts with one-line previews and timestamps; `/stash pop`\nrestores the most recently parked draft (LIFO); `/stash clear`\nwipes the file. Capped at 200 entries; multiline drafts round-trip intact.\n**Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code\nreserve Ctrl+S for Save, so Ctrl+G is the portable default.\n\n## Settings File (Persistent UI Preferences)\n\ncodewhale also stores user preferences in:\n\n- `~/.codewhale/settings.toml` on new installs\n- `~/.deepseek/settings.toml` or the legacy platform config-dir\n `deepseek/settings.toml` when an existing settings file is present\n\nNotable settings include `auto_compact`, which uses a model-aware default-on\npolicy for known context windows up to the 1M-token V4 class. Automatic\ncompaction runs before the active model limit and carries the compacted summary\nforward into the next request. The trigger defaults to\n`auto_compact_threshold_percent = 80`. Users who prefer manual continuity can\npersist `auto_compact = false`; manual `/compact` / Ctrl+L remains available.\nYou can inspect or update these from the TUI with `/settings` and `/config`\n(interactive editor).\n\nCommon settings keys:\n\n- `theme` (`system`, `terminal`, `underwater`, `underwater-retro`,\n `shoreline`, `shoreline-light`, `dark`, `light`, `grayscale`,\n `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`,\n `matrix`, `solarized-light`, `uwu`; default `underwater`): `underwater` is\n the dark navy fresh-install default, `shoreline` is its warm charcoal\n alternative and `shoreline-light` the light variant, `system` follows terminal\n background detection, `dark`/`light` use the Codewhale Whale pair,\n `terminal` inherits the host terminal, `grayscale` is the low-opinion\n black/white theme, and the named community presets apply across the TUI.\n Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox`\n are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live\n work, Signal Gold owns human decisions and the whale, coral owns warnings,\n rose owns danger, violet owns Operate, and green remains completed/verified.\n Text labels, markers, and motion policy carry the same states when color is\n unavailable; color is never the only cue.\n User-authored overlays live only at `~/.codewhale/themes/.json` (or\n `$CODEWHALE_HOME/themes/.json`) and are selected with\n `/theme custom:`. The filename is a bounded slug, symlinks and files\n over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail\n validation. `/theme schema` prints the embedded JSON Schema and `/theme path`\n shows the exact directory. An overlay names one compiled `base` theme and\n changes only listed semantic colors; it cannot include or read another file.\n Open `/theme` to browse valid overlays, preview them live, and keep the\n active `custom:` selector when the picker is opened without moving.\n- `auto_compact` (on/off, model-aware default on for known context windows\n unless explicitly configured)\n- `auto_compact_threshold_percent` (10-100, default `80`): pre-send\n auto-compaction threshold used only when `auto_compact` is enabled.\n- `paste_burst_detection` (on/off, default on): fallback rapid-key paste\n detection for terminals that do not emit bracketed-paste events. This is\n independent of terminal bracketed-paste mode.\n- `work_surface_placement` (`bottom`, `top`, `left`, `right`, or `off`;\n default `bottom`): places the workbar — Tasks / To-do / Workers — under the\n composer (the default bottom workbar), above the transcript, in a side\n workbar, or hides it entirely (`off`). Side choices fall back to the top\n layout on narrow terminals without changing the saved preference. Set it\n live with `/config work_surface_placement right --save` (or `left` / `top` /\n `bottom` / `off`).\n- `rail_panel` (`tasks`, `agents`, `background`, `files`, `notepad`,\n `context`, `git`, `price`; default `tasks`, alias key `rail`): which panel\n the workbar shows. Panel selection is orthogonal to placement. `tasks` is\n the full live work list (to-dos, then sub-agents); `agents` narrows to the\n sub-agent rows; `background` lists background shells and automations;\n `files` lists touched files; `notepad` shows the workspace notes; `context`\n is a read-only session-facts list; `git` shows branch status; `price`\n shows cost. In every panel except `context`, rows are selectable and\n clickable and open their detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$`\n switch panels live.\n- `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80):\n ceilings for the top strip's height and the side workbar's width. Both are\n normally persisted by dragging the divider rather than edited by hand; the\n strip still auto-fits its content below the ceiling.\n- `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context\n texture for modal views. `scrim` dims the already-rendered background\n outside the focused modal toward the theme surface; `grain` sprinkles\n sparse dots over blank cells there. The texture is static (no time\n component, so it is unaffected by `low_motion`), never writes over a cell\n that carries text, and preserves the 4.5:1 body-text contrast floor\n wherever both colors are resolvable. It is skipped entirely on frames\n below the ambient-life minimum size and when the focused modal already\n covers 90% or more of the frame. Set it live with\n `/config focus_texture scrim --save`.\n- `mention_menu_limit` (integer, default `128`): maximum number of\n `@`-mention popup candidates retained before the composer renders the\n visible window. The visible rows still depend on terminal height.\n- `mention_walk_depth` (integer, default `10`): maximum workspace depth for\n `@`-mention completion walks. Set to `0` for unlimited depth in deeply\n nested workspaces; keep the default in very large repos unless needed.\n- `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how\n `@`-mention completions are populated. `fuzzy` searches the workspace and\n applies mention frecency. `browser` lists only the immediate children of the\n currently typed directory segment in deterministic alphabetical order.\n- `show_thinking` (on/off)\n- `thinking_default_expanded` (on/off, default off): renders thinking blocks\n expanded initially when `show_thinking` is enabled. Space still toggles the\n selected block, and it decides only the blocks you have not touched: a block\n you expanded or collapsed yourself keeps that state if you change this\n setting later. This is useful in SSH/tmux environments where the Space\n binding may be intercepted.\n- `thinking_preview_lines` (integer, default `2`): how many body rows a\n **collapsed** completed thought still shows. `0` is header-only; `10` is\n the older dump. Live streaming preview is unchanged. Expand a block with\n Space, or set `thinking_default_expanded` to open every block.\n- `help_expand_groups` (on/off, default off): start Help/shortcuts with every\n group expanded. Default folds the long tail (Grok-style); type-to-filter\n still unfolds matches.\n- `pin_last_prompt` (on/off, default on): pin the last user prompt at the top\n of the transcript viewport after it scrolls off.\n- `show_tool_details` (on/off)\n- `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the\n inline presentation of successful structured File mutations. `full` shows a\n bounded red/green diff and semantic statistics, `summary` keeps only the\n statistics, and `off` keeps the calm changed-file outcome. All three retain\n the exact applied change in the selected File receipt's Alt/Option+V detail.\n Failure and cancellation never render a successful diff. Save the choice\n with `/config inline_diffs --save`.\n- `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`,\n `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`,\n `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English.\n Every shipped pack holds full `en.json` parity, so no string falls back\n to English. The runtime also exposes the resolved locale in the system\n prompt as the fallback natural language for V4 reasoning and replies when the\n latest user message is ambiguous. Clear user language still takes priority;\n Chinese turns should produce Chinese `reasoning_content` and Chinese final\n replies even when the resolved locale is English.\n- `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI\n background color applied to the root, header, transcript, and footer\n surfaces while preserving panel contrast.\n- `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer,\n context panel, `/cost`, `/tokens`, and long-turn notification summaries. The\n aliases `rmb` and `yuan` normalize to `cny`.\n- `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary)\n- `launch_screen` (legacy, migration-only): this historical `on`/`off` value\n is still accepted when reading an existing settings file, but it no longer\n changes behavior and is omitted from new saves. A fresh interactive launch\n always opens Tideline Startup; only an explicit resume or an explicit\n initial prompt enters the live session directly.\n- `sidebar_focus` (legacy, migration-only): the classic right sidebar this key\n configured was removed in the 0.9.4 rail unification. The key is still read\n once so old settings carry forward, then folds into the live keys:\n `pinned`/`work`/`plan`/`todos` become `rail_panel = \"pinned\"`,\n `agents`/`subagents` become `rail_panel = \"agents\"`, `context`/`session`\n become `rail_panel = \"context\"`, `tasks`/`auto` (the old default) become the\n `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the\n workbar off via `work_surface_placement = \"off\"`. An explicit `rail_panel`\n in the file always wins over the migrated value. Configure the workbar with\n `rail_panel` and `work_surface_placement`, not this key.\n- `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions\n list in the workbar. Rows list this workspace's recent\n non-archived sessions, newest first, with the active one marked; activating a\n row opens the session picker preselected on it (`/sessions open `), so\n resume keeps its single implementation. Rows are projected from cached\n session metadata — the list never reads a transcript per frame, and never\n contacts a provider.\n- `session_auto_resume` (`on`/`off`; default `off`): reattach to this\n workspace's most recent session when Codewhale starts. Off by default so\n plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and\n `--fresh` always take precedence. When it is on, startup still refuses to\n resume a session that is archived, fails to load, or is recorded against a\n different workspace; each of those falls back to a fresh transcript and says\n which session was skipped and why. It applies to the interactive launch only\n — `codewhale \"\"` and `codewhale exec` are never silently prefixed\n with a prior conversation.\n- `max_input_history` (number of submitted input history entries; cleared\n drafts are also kept locally for composer history search). Note the spelling:\n the serde field on disk is `max_input_history`\n (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key\n name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not\n a settings.toml key — writing `max_history` into the file is silently\n ignored.\n- `default_model` (model name override)\n\n`/task digest` (alias `/tasks digest`) renders the canonical Work Graph\noperations and four-state To-do list as plain text, running work first. It\nreads the same snapshots as the styled Work surface and owns no parallel\nprogress state.\n\nPlan and Work are the everyday visible modes in the UI; Operate is an explicit\npreview entry while its Workflow control surface is still being built. Switch\nbetween them with `/mode`. For compatibility, older settings files with\n`default_mode = \"normal\"` still load as `agent`.\n\nLocalization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6\ncore pack covers high-visibility TUI chrome only; provider/tool schemas,\npersonality prompts, and full documentation remain English unless explicitly\ntranslated later.\n\nReadability semantics:\n\n- Selection uses a unified style across transcript, composer menus, and modals.\n- Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes.\n\n### Token Quantities and Drivers\n\nDeepSeek V4 prefix caching makes token labels matter. These quantities are kept\nseparate:\n\n| Quantity | Meaning | Allowed to drive |\n|---|---|---|\n| Active request input estimate | Conservative estimate of the next request's live system prompt and transcript payload. | Header/footer context percent, auto-compaction trigger, opt-in Flash seam trigger, and emergency overflow preflight. |\n| Reserved response headroom | The effective request cap plus `1024` safety tokens. Normal no-override requests start at `65536`; a smaller route/provider ceiling narrows that value, and an explicit output override raises it only within the resolved route window and output ceiling. The identical cap reaches the wire and drives preflight, except on a route that states an output limit above half its known context window with no explicit output override: there compaction aims to keep a quarter of the window free (at most `65536`, and never more than the request cap), preflight refuses a request only when fewer than `128` output tokens would fit, and every request's `max_tokens` is what the window has left after that request's estimated input and the `1024` safety tokens, never above the request cap. Reasoning effort does not add a second hidden reservation. A separately published route input ceiling independently clamps the spendable input budget. | Emergency overflow budget checks only. |\n| Cumulative API usage | Provider-reported input plus output tokens summed across completed API calls; multi-tool turns may count the same stable prefix more than once. | Session usage and approximate cost telemetry only. |\n| Prompt cache hit/miss | Provider cache telemetry for the most recent call when available. | Cache-hit display and cost estimation only; never compaction or seam triggers. |\n| Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. |\n| Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. |\n\nFor known context-window models, including 1M-class V4 models, replacement\ncompaction is enabled by default unless the user explicitly configures\n`auto_compact = false`. It fires at the active model's compaction threshold and\nreplaces old history with recent user context followed by one ordinary\ncheckpoint message. The standing system prompt remains unchanged. Unknown model\nids remain opt-in.\n\n### Command Migration Notes\n\nIf you are upgrading from older releases:\n\n- Old: `/codewhale`\n New: `/links` (aliases: `/dashboard`, `/api`)\n- Old: `/set model deepseek-reasoner`\n New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-flash`\n- Old: visible `Normal` mode or `default_mode = \"normal\"`\n New: use `Agent` / `default_mode = \"agent\"`; legacy `normal` still maps to `agent`\n- Old: discover `/set` in slash UX/help\n New: use `/config` for editing and `/settings` for read-only inspection\n\n## Key Reference\n\n### Kimi Code membership model IDs\n\nThe exact `https://api.kimi.com/coding/v1` endpoint accepts `k3`, `k3-256k`,\n`kimi-for-coding`, and `kimi-for-coding-highspeed`. Use `k3-256k` for a fixed\n262,144-token K3 window; use bare `k3` with `context_window = 1048576` only\nwhen the membership plan includes the 1M entitlement. Both K3 ids use the same\nreasoning contract, and all four membership ids omit generic sampling fields.\n\n### Core keys (used by the TUI/engine)\n\n- `provider` (string, optional): `deepseek` (default), `deepseek-anthropic`, `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `ollama-cloud`, `huggingface`, `modelscope`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `mistral`, `telecomjs`, `xai`, `orcarouter`, `modelstudio-token-plan`, `google`, `edenai`, or `custom`. Legacy `deepseek-cn` configs are still accepted as an alias for `deepseek`; DeepSeek uses the same official host [`https://api.deepseek.com`](https://api-docs.deepseek.com/) worldwide. `deepseek-anthropic` targets DeepSeek's Anthropic Messages-compatible endpoint at `https://api.deepseek.com/anthropic` using `DEEPSEEK_API_KEY`; `nvidia-nim` targets NVIDIA's NIM-hosted DeepSeek endpoints through `https://integrate.api.nvidia.com/v1`; `openai` targets a generic OpenAI-compatible endpoint, defaulting to `https://api.openai.com/v1`; `atlascloud` targets AtlasCloud's OpenAI-compatible endpoint at `https://api.atlascloud.ai/v1`; `wanjie-ark` targets Wanjie Ark's OpenAI-compatible endpoint at `https://maas-openapi.wanjiedata.com/api/v1`; `volcengine` targets Volcengine Ark's OpenAI-compatible coding endpoint at `https://ark.cn-beijing.volces.com/api/coding/v3`; `openrouter` targets `https://openrouter.ai/api/v1`; `xiaomi-mimo` targets Xiaomi MiMo's OpenAI-compatible endpoint, using `https://token-plan-sgp.xiaomimimo.com/v1` by default for Token Plan keys (`tp-...`) and `https://api.xiaomimimo.com/v1` for pay-as-you-go keys. For Token Plan accounts outside the Singapore default, set `base_url` explicitly or use `mode = \"token-plan-cn\"` for China and `mode = \"token-plan-ams\"` for Europe/Amsterdam; `novita` targets `https://api.novita.ai/openai/v1`; `fireworks` targets `https://api.fireworks.ai/inference/v1`; `siliconflow` targets SiliconFlow, defaulting to `https://api.siliconflow.com/v1`; `arcee` targets Arcee AI's OpenAI-compatible endpoint at `https://api.arcee.ai/api/v1`; `siliconflow-CN` targets the SiliconFlow China regional endpoint through `[providers.siliconflow_cn]`; `moonshot` targets Moonshot/Kimi, defaulting to `https://api.moonshot.ai/v1`; `sglang` targets a self-hosted OpenAI-compatible endpoint, defaulting to `http://localhost:30000/v1`; `vllm` targets a self-hosted vLLM OpenAI-compatible endpoint, defaulting to `http://localhost:8000/v1`; `ollama` targets Ollama's OpenAI-compatible endpoint, defaulting to `http://localhost:11434/v1`; `huggingface` targets Hugging Face Inference Providers at `https://router.huggingface.co/v1`; `modelscope` targets ModelScope's OpenAI-compatible inference API at `https://api-inference.modelscope.cn/v1`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets official ChatGPT plan inference with Codewhale-owned OAuth at `https://api.openai.com/v1`; `anthropic` targets Claude's native Messages API; `openmodel` targets OpenModel's Anthropic-compatible Messages API at `https://api.openmodel.ai`; `zai` targets Z.ai at `https://api.z.ai/api/coding/paas/v4`; `stepfun` targets StepFun at `https://api.stepfun.ai/v1`; `minimax` targets MiniMax at `https://api.minimax.io/v1`; `deepinfra` targets DeepInfra at `https://api.deepinfra.com/v1/openai`; `sakana` targets Sakana AI Fugu at `https://api.sakana.ai/v1`; `longcat` targets Meituan LongCat at `https://api.longcat.chat/openai/v1`; `opencode-go` targets the subscription-backed OpenCode Go model-aware route (Chat Completions, Responses, or Messages according to the documented model) at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `mistral` targets Mistral AI's OpenAI-compatible endpoint at `https://api.mistral.ai/v1`; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route.\n- `opencode-zen` (string provider value): selects the model-aware OpenCode Zen gateway through `[providers.opencode_zen]`. The default base URL is `https://opencode.ai/zen/v1`, the default model is `gpt-5.6`, and credentials come from `api_key`, `OPENCODE_ZEN_API_KEY`, or fallback `OPENCODE_API_KEY`—never ChatGPT/Codex OAuth. `OPENCODE_ZEN_BASE_URL` and `OPENCODE_ZEN_MODEL` are accepted. The selected model's wire comes from the curated Zen snapshot, then from the AI SDK package its Models.dev `opencode` row names (refresh with `codewhale models --update`): GPT, Grok, and Muse Spark use Responses; Claude and most Qwen rows use Anthropic Messages; DeepSeek, MiniMax, GLM, Kimi, `qwen3.8-max`, and the free rows use Chat Completions. Gemini, Models.dev rows marked `deprecated`, and models no loaded catalog lists fail closed because Codewhale has no proven supported wire contract for them. See the exact current model groups in [`PROVIDERS.md`](PROVIDERS.md#opencode-zen-protocol-catalog).\n- `minimax-anthropic` (string provider value): selects MiniMax's Anthropic-compatible Messages route through `[providers.minimax_anthropic]`. The default Base URL is `https://api.minimax.io/anthropic`; set `https://api.minimaxi.com/anthropic` for China. Keep the `/anthropic` suffix because Codewhale appends `/v1/messages`. The route uses `MINIMAX_API_KEY` and defaults to `MiniMax-M3`; `MiniMax-M2.7` is also registered. Official M3 input modalities are text, image, and video, with adaptive or disabled thinking. M2.7 is text-only and always keeps thinking enabled.\n- `api_key` (string, required for hosted providers): must be non-empty for DeepSeek/hosted providers (or set the provider API key env var). Self-hosted SGLang, vLLM, and local `ollama` can omit it. `ollama-cloud` requires a key saved for that provider or supplied by `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`.\n- `auth_mode` (string, optional provider-table key): selects a provider-specific authentication contract. Kimi Code membership uses `auth_mode = \"api_key\"` (or omit the field), a key created in the [Kimi Code console](https://www.kimi.com/code/console), `base_url = \"https://api.kimi.com/coding/v1\"`, and bare `model = \"k3\"` for K3. Codewhale gives that route a safe 262,144-token baseline; set `context_window = 1048576` only when the Kimi Code plan includes 1M access (Allegretto and above). `k3[1m]` is a Claude Code-only convention, not an API model ID, and Codewhale rejects it instead of silently changing the wire model or assuming an entitlement. `model = \"kimi-for-coding\"` remains the valid K2.7 compatibility route available to all Kimi Code members. Legacy `auth_mode = \"kimi_oauth\"` fails closed with API-key guidance and never probes, reads, refreshes, or rewrites `kimi_cli`/`kimi_code_cli` credential files. First-class OAuth requires Codewhale's own vendor-registered client identity and remains tracked in #4417.\n- `base_url` (string, optional, `[providers.]` key; see [Legacy top-level `base_url` and `api_key`](#legacy-top-level-base_url-and-api_key) for the older top-level spelling): defaults to `https://api.deepseek.com/beta` for DeepSeek's OpenAI-compatible Chat Completions API, including legacy `provider = \"deepseek-cn\"` configs. Other defaults are `https://api.deepseek.com/anthropic` for `deepseek-anthropic`, `https://integrate.api.nvidia.com/v1` for `nvidia-nim`, `https://api.openai.com/v1` for `openai`, `https://api.atlascloud.ai/v1` for `atlascloud`, `https://maas-openapi.wanjiedata.com/api/v1` for `wanjie-ark`, `https://ark.cn-beijing.volces.com/api/coding/v3` for `volcengine`, `https://openrouter.ai/api/v1` for `openrouter`, `https://token-plan-sgp.xiaomimimo.com/v1` for `xiaomi-mimo` when the API key starts with `tp-...` and `https://api.xiaomimimo.com/v1` otherwise, `https://api.novita.ai/openai/v1` for `novita`, `https://api.fireworks.ai/inference/v1` for `fireworks`, `https://api.siliconflow.com/v1` for `siliconflow`, `https://api.siliconflow.cn/v1` for `siliconflow-CN`, `https://api.arcee.ai/api/v1` for `arcee`, `https://api.moonshot.ai/v1` for `moonshot`, `https://api.minimax.io/v1` for `minimax`, `https://api.openmodel.ai` for `openmodel`, `https://api.z.ai/api/coding/paas/v4` for `zai`, `https://api.stepfun.ai/v1` for `stepfun`, `https://api.deepinfra.com/v1/openai` for `deepinfra`, `https://api.sakana.ai/v1` for `sakana`, `https://router.huggingface.co/v1` for `huggingface`, `https://api-inference.modelscope.cn/v1` for `modelscope`, `https://api.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://api.openai.com/v1` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `https://api.mistral.ai/v1` for `mistral`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, `http://localhost:11434/v1` for `ollama`, and `https://ollama.com/v1` for `ollama-cloud`. Set `base_url = \"https://token-plan-cn.xiaomimimo.com/v1\"` for China-region Xiaomi MiMo Token Plan accounts or `base_url = \"https://token-plan-ams.xiaomimimo.com/v1\"` for Europe/Amsterdam accounts. Mistral-specific reasoning fields and polymorphic replay are enabled only on the documented first-party HTTPS `/v1` hosts; a custom Mistral base URL keeps generic Chat semantics. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features.\n- `ollama-cloud` route: select `provider = \"ollama-cloud\"`, configure `[providers.ollama_cloud]` when overriding the default `https://ollama.com/v1` / `gpt-oss:120b` tuple, and save a key from [Ollama account settings](https://ollama.com/settings/keys) with `codewhale auth set --provider ollama-cloud`. Ambient precedence is `OLLAMA_CLOUD_API_KEY`, then `OLLAMA_API_KEY`; arbitrary Ollama model IDs pass through unchanged.\n- Legacy Ollama Cloud migration: a released `provider = \"ollama\"` config whose normalized `[providers.ollama].base_url` is exactly `https://ollama.com/v1` is upgraded to the `ollama-cloud` runtime identity in memory. Only that exact tuple may read its old `ollama` provider table and secret slot. The config and secrets are never rewritten, and neighboring paths, HTTP downgrades, lookalike hosts, or an explicit `ollama-cloud` selection never consume the fallback.\n- `telecomjs` base URL and catalog: `[providers.telecomjs]` defaults to `https://aigw.telecomjs.com/v1`; `TELECOMJS_BASE_URL` overrides it. With `TELECOMJS_API_KEY`, `/models` refreshes a key-scoped catalog without mixing rows into another provider.\n- `edenai` gateway: select `provider = \"edenai\"`; `[providers.edenai]` defaults to `https://api.edenai.run/v3` and `deepseek/deepseek-v4-pro`. `EDENAI_API_KEY`, `EDENAI_BASE_URL`, and `EDENAI_MODEL` are accepted. Use `EDENAI_BASE_URL = \"https://api.eu.edenai.run/v3\"` for Eden AI's documented EU endpoint; the default `deepseek/deepseek-v4-pro` is only listed on the global catalog, so pair the EU endpoint with an EU-listed model such as `qwen/deepseek-v4-pro` via `EDENAI_MODEL` or `model`. The provider refreshes Eden AI's `/models` catalog, but leaves model-specific reasoning controls untouched because the gateway spans multiple model families.\n- `codewhale` (Codewhale API): select `provider = \"codewhale\"`; `[providers.codewhale]` defaults to `https://api.codewhale.net/v1` and `deepseek/deepseek-v4-pro`. The credential is a Codewhale account API key (`cwc_key_…`) with the `models:infer` scope, read from `CODEWHALE_API_KEY` or the `codewhale` secret-store slot; `codewhale account api-keys create --name --use` mints one and saves it locally. `CODEWHALE_API_BASE` overrides the origin and must be HTTPS except on loopback. The model catalog is the account's own authenticated `GET /v1/models`: ids are `provider/model` and each row states its wire (`chat-completions` → `/v1/chat/completions`, `anthropic-messages` → `/v1/messages`, `responses` → `/v1/responses`). Connect the underlying provider keys with `codewhale account keys set `.\n- `concentrate` gateway: select `provider = \"concentrate\"`; `[providers.concentrate]` defaults to `https://api.concentrate.ai/v1` and `deepseek-v4-pro` over the OpenAI Responses wire. `CONCENTRATE_API_KEY`, `CONCENTRATE_BASE_URL`, and `CONCENTRATE_MODEL` are accepted. Model ids pass through verbatim (`gpt-5.6-sol`, `openai/gpt-5.6-sol`, or `concentrate/auto` for the gateway router). BYOK only; see [PROVIDERS.md](PROVIDERS.md#concentrate-notes).\n- `mistral` model and reasoning contract: `[providers.mistral]` defaults to `mistral-code-latest`; `MISTRAL_MODEL` overrides it and the generic `CODEWHALE_MODEL` override wins when both are set. The current picker also lists `mistral-medium-latest`, `mistral-small-latest`, and `mistral-large-latest`. On exact first-party HTTPS `/v1` routes, Medium and Small accept only `reasoning_effort = \"none\" | \"high\"` and replay polymorphic thinking blocks. Deprecated native Magistral IDs may still be configured explicitly, remain always-reasoning, and never receive the adjustable effort field.\n- `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.]` route when an OpenAI-compatible gateway, hosted model alias, or self-hosted runtime has a different limit than Codewhale's static model table. For example, `[providers.openai] context_window = 1000000` lets an OpenAI-compatible DashScope/Qwen route budget against a 1M-token window instead of the conservative fallback. For Kimi Code K3, keep `model = \"k3\"` and set `[providers.moonshot] context_window = 1048576` only when the membership plan includes 1M access; otherwise omit it to retain the 262,144-token safe baseline. The value must be greater than 0 and affects prompt context notes, compaction thresholds, context-pressure checks, and request output caps. Full resolution order, and how to see which rung produced the current window: [Context length (context window)](#context-length-context-window).\n- `path_suffix` (string, optional provider-table key): override the chat-completions path for OpenAI-compatible gateways that do not serve `/v1/chat/completions`. For example, `[providers.openai] path_suffix = \"/chat/completions\"` sends chat requests to the unversioned base URL plus `/chat/completions`; `models` and `beta/*` requests keep their normal routing.\n- `reasoning_stream_style` (string, optional provider-table key): override how streaming reasoning is separated from answer text for the active provider route. Use `separate_field` for `reasoning_content` / `reasoning` deltas, `inline_tags` for gateways that stream `...` inside `delta.content`, or `none` to render incoming content exactly as answer text. When unset, every Chat Completions route uses `separate_field` (Mistral's first-party route uses its typed thinking blocks); set `none` only for a gateway that streams its answer inside `reasoning_content`.\n- `[providers..auth]` (table, optional): provider-scoped auth source metadata. `source = \"command\"` stores a command argv plus optional `timeout_ms`; `source = \"secret\"` stores a `secret_id`. This slice lets provider readiness, `/provider`, and doctor JSON report the auth source class without exposing command argv output or secret values; executing commands and resolving external secret material is handled by the follow-up resolver work.\n- `insecure_skip_tls_verify` (bool, optional provider-table key): legacy compatibility key, disabled by default. When true on the active provider table, provider clients reject the configuration instead of skipping TLS certificate verification. Use `SSL_CERT_FILE` for corporate or private CA bundles; `codewhale doctor` reports stale uses of this setting.\n- `default_text_model` (string, optional): defaults to `deepseek-flash` for DeepSeek and `deepseek-anthropic`, `gpt-5.6` for OpenAI, `grok-4.6` for xAI, `deepseek-ai/deepseek-v4-pro` for NVIDIA NIM, `deepseek-ai/deepseek-v4-flash` for AtlasCloud, `deepseek-reasoner` for Wanjie Ark, `DeepSeek-V4-Pro` for Volcengine Ark, `deepseek/deepseek-v4-pro` for OpenRouter and Novita, `mimo-v2.6-pro` for Xiaomi MiMo, `accounts/fireworks/models/deepseek-v4-pro` for Fireworks, `deepseek-ai/DeepSeek-V4-Pro` for SiliconFlow and DeepInfra, `trinity-large-thinking` for Arcee AI, `kimi-k2.7-code` for Moonshot, `MiniMax-M3` for MiniMax, `GLM-5.3` for Z.ai, `step-3.7-flash` for StepFun, `ernie-4.0-turbo-8k` for Qianfan, `fugu` for Sakana AI, `deepseek-ai/DeepSeek-V4-Pro` for SGLang/vLLM, no fixed id for local Ollama (Codewhale adopts a chat-capable tag from the live local catalog — coder or tool-capable tags first, then the largest context — and never an embedding or reranker model), and `gpt-oss:120b` for Ollama Cloud. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-6.1-sol`; `anthropic` defaults to `claude-sonnet-5-5`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs include `deepseek-v4-pro` and `deepseek-flash` (V4.1 Flash, shipped as the unversioned id), both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek's live pricing/model page now labels the Pro backend `DeepSeek-V4-Pro-0813`; the callable API ID remains `deepseek-v4-pro`, so Codewhale does not send the backend label or the Claude Code-specific `deepseek-v4-pro[1m]` selector. DeepSeek retired `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. DeepSeek has also retired the `deepseek-v4-flash` and `deepseek-v4-flash-vision-exp` models: it still accepts both names, serves them with V4.1 Flash, and bills them at the `deepseek-flash` price, which Codewhale applies to turns from 2026-09-10 04:00 UTC. The `flash`, `deepseek-v4flash`, `flash-vision`, and `deepseek-v4flashvisionexp` shorthands select `deepseek-flash`. Explicit `reasoning_effort` wins, and provider-owned ids on Wanjie Ark, aggregators, self-hosted runtimes, and custom endpoints are not globally rewritten. SiliconFlow retains its own mapping: `deepseek-reasoner` and `deepseek-r1` select its Pro model while `deepseek-chat` and `deepseek-v3` select Flash. Provider-specific mappings translate `deepseek-v4-pro` / `deepseek-v4-flash` to each provider's model ID where supported. OpenRouter also recognizes recent large IDs such as `arcee-ai/trinity-large-thinking`, `minimax/minimax-m3`, `minimax/minimax-m2.7`, `xiaomi/mimo-v2.5-pro`, `qwen/qwen3.6-flash`, `qwen/qwen3.6-35b-a3b`, `qwen/qwen3.6-max-preview`, `qwen/qwen3.6-27b`, `qwen/qwen3.6-plus`, `qwen/qwen3.7-max`, `google/gemma-4-31b-it`, `moonshotai/kimi-k2.7-code`, `moonshotai/kimi-k2.6`, `nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free`, and `nvidia/nemotron-3-ultra-550b-a55b`; direct Arcee uses bare IDs such as `trinity-large-thinking` and `trinity-large-preview`; direct Moonshot recognizes `kimi-k3`, `kimi-k2.7-code`, and `kimi-k2.6`. The exact Kimi Code endpoint recognizes bare `k3` for K3 and `kimi-for-coding` for K2.7; those membership IDs are distinct from the direct Moonshot IDs and are never rewritten across routes. Direct MiniMax recognizes `MiniMax-M3` and the documented M2.x chat model IDs; direct Z.ai recognizes `GLM-5.3` (the default), `GLM-5.2`, `GLM-5.1`, and `GLM-5-Turbo`, and OpenRouter recognizes the matching `z-ai/glm-5.1`, `z-ai/glm-5.2`, `z-ai/glm-5.3`, and `z-ai/glm-5-turbo` IDs — `GLM-5.3` has been live on the Z.ai Coding Plan since 2026-08-13; it inherits its catalog metadata from `GLM-5.2` until Z.ai publishes distinct 5.3 numbers and carries no price, and an explicit `GLM-5.2` selection keeps its own id; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.6-pro`, `mimo-v2.6-flash`, and `mimo-v2.5-pro-ultraspeed` (the retired `mimo-v2.5-pro` and `mimo-v2.5` are sent as `mimo-v2.6-pro` and `mimo-v2.6-flash`), while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, local Ollama, and Ollama Cloud model IDs are passed through unchanged after known aliases are normalized. OpenRouter and SiliconFlow provider configs with a custom `base_url` also preserve explicit model values, which lets OpenAI-compatible gateways accept bare model IDs. Use `/models` or `codewhale models` to discover live IDs from your configured endpoint. `CODEWHALE_MODEL` overrides this for a single process; `DEEPSEEK_MODEL` is the legacy alias.\n- TelecomJS uses `deepseek-v4-pro` only as a conservative pre-refresh fallback. Once its key-scoped `/models` catalog is available, the picker uses those live rows; Codewhale omits unsupported reasoning request fields on this route.\n- `reasoning_effort` (string, optional): `auto`, `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `ultra` (`ultracode` is an alias of `ultra`), or `max`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. Ollama Cloud's OpenAI-compatible Chat Completions route preserves its documented `none` / `low` / `medium` / `high` / `max` ladder (`off` is sent as `none`; `xhigh` and `ultracode` normalize to `max`). Direct xAI `grok-4.7` and `grok-4.6` on exact `https://api.x.ai/v1` receive top-level `reasoning_effort = \"low\" | \"medium\" | \"high\" | \"xhigh\"` (the ladder comes from the bundled catalog row; `grok-4.5` maps `xhigh` to `high`, and rows without a documented effort such as `grok-4.3` get no field); Grok reasoning cannot be disabled, so `off` normalizes to `high`, `max`/`ultracode` to `xhigh`, and `auto` leaves the field omitted so xAI's documented default `high` applies. A custom xAI-compatible `base_url` does not inherit that dialect. Direct Moonshot `kimi-k3` on exact `https://api.moonshot.ai/v1` is always-thinking and receives only top-level `reasoning_effort = \"low\" | \"high\" | \"max\"`; `off` normalizes to `low`, and `medium` to `high`. Kimi Code membership `k3` on exact `https://api.kimi.com/coding/v1` instead receives nested `thinking.effort`, and its `off` setting also normalizes to enabled `low`. Normal dispatched `auto` uses Codewhale's auto-reasoning selector and sends a concrete route-normalized tier; only an omitted reasoning setting leaves the provider default in control. Neighboring gateways and model/endpoint combinations retain the generic Moonshot contract. OpenAI Codex normalizes stale `off` to `low` and sends `max` / `ultracode` as Responses `xhigh`. Z.ai receives documented `thinking` controls and treats enabled thinking as the GLM coding high/max lane. NVIDIA NIM receives equivalent settings through `chat_template_kwargs`.\n- `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the\n default conversational prompt. `concise` appends a prompt discipline block\n for direct, low-chatter output; CLI noninteractive commands (`exec` and\n `eval`) default to `concise` unless config/env/CLI overrides it.\n Override per process with `CODEWHALE_VERBOSITY` or the legacy\n `DEEPSEEK_VERBOSITY` alias.\n- `telemetry` (bool, optional): anonymous usage counting, **`true` by default\n in the current 0.9.12 source**. Notice version `5` names Codewhale and PostHog\n and describes the opt-out policy; no acceptance is invented for a default\n user. Existing explicit declines remain off. An explicit `false` here is the\n durable *opt-out*: it deletes the random install id, truncates buffered and\n dry-run events, and leaves a tombstone reasserted while the setting is false.\n It is a floor: `--telemetry true` and `CODEWHALE_TELEMETRY=1` lose to it.\n Use `/settings` or `codewhale config set telemetry true` to turn counting\n back on explicitly for new sessions through the existing privacy transition. `CODEWHALE_TELEMETRY`\n (legacy alias `DEEPSEEK_TELEMETRY`) and `--telemetry false` provide a run-scoped\n kill switch that stops collection and delivery without erasing the owner's\n state. A repo-local `.codewhale/config.toml` cannot set this preference.\n `codewhale config telemetry` shows the disclosure; `codewhale config get\n telemetry` reports preference and privacy status. Full schema and opt-out\n behavior: [`TELEMETRY.md`](TELEMETRY.md).\n- `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it\n unset selects the shipped default,\n **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest\n service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in\n `telemetry-ingest/`. This key decides only *where* a permitted session sends;\n it cannot override an opt-out. Setting it\n to the **empty string** is how you stay enabled and contact nobody: each batch\n is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client\n is constructed at all, so you can read exactly what would have been sent. Any\n other value replaces the default outright. `https://` is required; plain\n `http://` is accepted only for loopback hosts, and there is no environment\n variable that overrides that refusal. A rejected endpoint turns telemetry off\n for the run rather than falling back to plaintext or to the default. Override\n per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias\n `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same \"contact\n nobody\". A repo-local `.codewhale/config.toml` cannot set it.\n- `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting\n this keeps shell tools available with approval prompts; setting it to `false`\n hides shell tools. Headless, durable-task, and other noninteractive profiles\n keep the conservative omitted-field default and require `allow_shell = true`\n to expose shell. Plan mode always hides shell; Full Access enables shell and\n auto-approval.\n- `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases.\n- `[approval] default_selection` (string, optional): which option an approval\n card highlights when it first appears — `deny` (default) or `allow_once`.\n `deny` means a reflexive Enter on a card you have not read refuses the call.\n The highlighted row is tagged `(Enter)`; press `y` to allow once.\n Set `allow_once` to restore the pre-v0.9.6 Enter-to-approve muscle memory\n (#5293). It moves the highlight only: which calls are prompted for is still\n `approval_policy` plus the rules in `permissions.toml`.\n\n ```toml\n [approval]\n default_selection = \"allow_once\"\n ```\n- `[approval] timeout_seconds` (integer, optional): bound how long an\n approval may wait — the TUI card and the Runtime API approvals the desktop\n app and web use alike. When the window elapses the call is refused\n (fail-closed) and recorded as a timeout, not as the operator's denial.\n Omitted or `0` waits indefinitely, which is the default everywhere: no\n approval is ever denied on your behalf unless you set this. Values above\n 24h clamp with a warning (#6101).\n\n ```toml\n [approval]\n timeout_seconds = 300\n ```\n- `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`.\n The value can only narrow the sandbox the permission mode picks (Full\n Access: no wrapper; Ask and Auto-Review: `workspace-write`; Plan:\n `read-only`), so `danger-full-access` and `external-sandbox` take effect\n only in Full Access (`yolo`).\n Platform support is not identical. macOS uses Seatbelt when its runtime\n probe succeeds. Linux uses bubblewrap by default whenever `/usr/bin/bwrap`\n is installed and a probe proves it can confine a child; `prefer_bwrap =\n false` opts out and reports no OS command sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts\n with process-tree containment only and must not be described as read-only\n filesystem isolation, workspace-write enforcement, network blocking,\n registry isolation, or AppContainer isolation until those are implemented.\n- The cross-layer relationship between mode admission, hooks, registered tool\n requirements, typed rules, auto-review, repo law, human approval, and the\n execution sandbox is defined in\n [Authorization order](AUTHORIZATION_ORDER.md).\n- **Read deny-list.** Every sandbox posture — `read-only` included — grants\n read access to the whole filesystem; the postures differ in what they may\n *write* and whether they may reach the network. The read deny-list narrows\n that, in two scopes. Codewhale's own file tools (`read`, `grep`,\n `file_search`, `read_media`) refuse every listed path. A shell command\n refuses only the *key material and other-principal* entries — SSH and GPG\n keys, `/etc/shadow`, `/etc/sudoers`, system and login keychains, browser\n profiles, other agents' credential stores, Codewhale's own secret stores,\n runtime daemon sockets, and anything in `sandbox_denied_read_paths`. A\n tool's own auth config is refused to the model but stays readable to the\n tool: `gh` cannot start without `~/.config/gh/hosts.yml`, so denying it to\n the process would disable the CLI without keeping its token out of reach.\n - `sandbox_read_denylist_defaults` (bool, default `true`): apply the built-in\n credential-store set — `~/.ssh`, `~/.gnupg`, cloud credential directories\n (`~/.aws`, `~/.config/gcloud`, `~/.azure`, `~/.kube`, …), `~/.netrc`,\n `~/.npmrc`, `~/.git-credentials`, macOS keychains, browser profiles,\n Codewhale's own secret stores, and `.env` files (but not `.env.example`\n and friends). Ordinary source, `Cargo.toml`, `~/.gitconfig`, `~/.cargo`,\n and `~/.npm` stay readable so builds and tests still work. Set `false` to\n restore the pre-0.9.12 full-disk-read behavior.\n - `sandbox_denied_read_paths` (list of paths): additional denied subpaths.\n `~` expands. These can never be exempted.\n - `sandbox_read_denylist_exempt` (list of paths): subtract a path from the\n *built-in defaults* when a project genuinely needs it. Deny wins over\n allow: this never reopens anything in `sandbox_denied_read_paths`.\n\n Exemption granularity is **whole-rule**, not per-file. An exempt path\n removes a built-in rule only when the rule's own path is at or below it,\n so exempting `~/.ssh/config` does nothing: the `~/.ssh` rule still denies\n it, because `~/.ssh` does not lie within `~/.ssh/config`. To reopen that\n one file you must exempt `~/.ssh` itself — which also reopens the private\n keys next to it. That is the documented tradeoff: there is no shipped way\n to narrow a built-in rule to \"everything except one file\"; copy what you\n need out of the denied tree instead. The one name-shaped rule, `.env`\n files, is exempted by *name* rather than by path: any exempt entry whose\n file name is exactly `.env` — bare `.env`, `~/.env`,\n `some/project/.env` — disables the entire `.env` filename rule, i.e.\n every `.env` and `.env.` on disk rather than one project's.\n (`.env.example` and friends are never denied, so they need no exemption.)\n\n Enforced at two points, with different reach. Codewhale's own in-process\n tools, which the OS sandbox never wraps, refuse every rule — `read_file` /\n `read` / `read_media` for contents, `list_dir` / `file_search` for\n *enumeration* (listing a denied directory, or searching one, is refused just\n as Seatbelt blocks its readdir; a name search rooted above a denied tree\n skips entries inside it). Sandboxed shell commands (Seatbelt last-match-wins\n `deny file-read*` rules; bubblewrap masks each path) refuse only the\n key-material and other-principal rules, so an authenticated CLI can still\n read its own config. A refused read is always an explicit error, never an\n empty result, and the error names the path as the caller spelled it rather\n than a symlink target's real location.\n\n **This is defense-in-depth, not a security boundary.** It does not stop a\n hardlink to a denied file, a secret already copied into the workspace, an\n indirect read (`ssh-agent`, `security find-generic-password`, `aws sts …`),\n reads under `danger-full-access` shell commands, reads by MCP servers or\n other unwrapped child processes, or exfiltration of anything that *was*\n read. Keep least-privilege credentials and short-lived tokens doing the real\n work.\n- `permissions.toml` (sibling file, optional): typed permission rule records\n loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`.\n This active user file is the only permission-rule source today; project\n config overlays do not load a project-local `permissions.toml`. A rule's\n optional `workspace` field is its repository scope, not a second source.\n Manually authored `[[rules]]` entries accept `tool`, optional `command` or\n `path`, optional absolute `workspace`, optional `command_exact = true`, and\n optional `action = \"deny\" | \"ask\" | \"allow\"`; omitted `action` defaults to\n `\"ask\"`. `workspace` limits a rule to that repository, while\n `command_exact = true` changes a command rule from the historical\n arity-aware prefix match to a complete-command match. `deny` blocks matching\n invocations before mode-based\n approval handling, `allow` skips approval for matching invocations, and\n `ask` forces approval only in modes that can prompt. Outside the TUI\n auto-approve path, a matching `ask` rule under `approval_policy = \"never\"`\n is rejected because no prompt can be shown. In Full Access / auto-approval sessions,\n `ask` rules do not downgrade the session into prompting or blocking; explicit\n `deny` rules still block according to the current execution-policy logic.\n\n In a supported approval card, press `S` to allow the request once and append\n exact `action = \"ask\"` rules to this file. For eligible safe requests, choose\n **Always allow this exact rule in this repo** (shortcut `P`) to append an\n `action = \"allow\"` rule with the current absolute `workspace` scope.\n Remembered shell grants set `command_exact = true`, so later commands with\n extra arguments do not inherit the grant. File and patch grants retain the\n exact workspace-relative paths produced by the existing validation path.\n Supported saves are intentionally narrow:\n `exec_shell` stores the exact approved command string; `write_file` and\n `edit_file` store the exact workspace-relative file path; `apply_patch`\n stores one exact workspace-relative `path` rule per validated touched file\n from apply-patch preflight. Existing exec command matching remains\n arity-aware for manually authored prefix rules; approval-card allow grants\n use complete-command matching. File paths are normalized to the same\n workspace-relative form used by runtime matching.\n\n `read_file` rules can still be authored manually when you want future reads\n of a specific path to ask, allow, or deny, but the approval UI does not save\n `read_file` rules. Commands classified as requiring approval or dangerous,\n critical approval cards, and repo-law prompts cannot save allow grants and\n continue to require review.\n\n `/permissions` (or `/permissions list`) is the narrow rule-management\n surface. It lists each numbered rule with the active user-file source, its\n exact effective matcher (tool-wide, command prefix, exact command, or exact\n normalized path), global or repository scope, and whether that scope\n applies in the current workspace. `/config ask-rules` remains a compatibility\n entry to the same list.\n\n Deletion is review-gated: `/permissions remove ` only previews the\n selected rule and prints a confirmation command. That command carries an\n opaque token bound to the exact file bytes and rule index; if another writer\n changes `permissions.toml`, confirmation fails instead of deleting a rule\n that moved into the old position. Confirmed removal and approval-card appends\n share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments\n and formatting, and atomically replace the file. The running TUI reloads the\n user ruleset without clearing session-only approvals.\n\n This editor intentionally does not create or rewrite rules, persist deny\n choices from approval cards, expand globs, or create broad\n directory/recursive rules. Author those supported exact/prefix records\n manually when needed.\n- `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for\n the TUI hotbar. Each entry has `slot`, `action`, and optional `label`.\n The Hotbar is hidden by default (#3807): omitting `hotbar` and setting\n `hotbar = []` both mean no hotbar. `/hotbar on` writes the eight default\n slots (`slash.workflow`, `slash.goal`, `slash.auto`, `mode.plan`,\n `mode.agent`, `mode.operate`, `palette.open`, `sidebar.toggle`) as explicit\n `[[hotbar]]` tables. When one or more `[[hotbar]]` tables are present, that\n list is the whole bar; missing slots stay empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate\n slots use the later entry, and unknown action IDs are kept so the UI can show\n a disabled/unknown cell instead of silently deleting user config. Trusted\n user config, profiles, and managed config replace the whole list; project\n overlays cannot change hotbar bindings. Setup or wizard flows that persist\n hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml`\n path, preserving legacy `~/.deepseek/config.toml` only when that fallback file\n is already the active config.\n\n ```toml\n [[hotbar]]\n slot = 1\n action = \"mode.plan\"\n label = \"Plan\"\n\n [[hotbar]]\n slot = 2\n action = \"session.compact\"\n ```\n- `[auto_review]` (table, optional): tool-call review policy — a deterministic floor plus a model guardian tier.\n This layer sits on top of the existing permission posture; it can hold or block a\n tool call, but it is not an auto-push, auto-merge, or hosted review service.\n Block rules are checked first, then the built-in safety floor, then allow\n rules. In Ask, a safety hold opens approval that no session grant or allow\n rule can answer; in Auto-Review or a non-interactive `never` posture it\n fails closed as a hard block. Full Access skips the built-in floor. The\n safety floor covers publish-like actions, destructive background/headless\n actions, and `curl`/`wget` uploads whose request body carries a credential\n file or a secret-named variable, or reads stdin fed from one of those or\n from `env`/`printenv`, even if an allow rule matches; a shell command,\n shell input or verifier over 64 KiB or 4,096 words whose text contains\n `curl` or `wget` anywhere, or one nested too deeply to read, is held too.\n Outside Full\n Access it also refuses, in every posture, a\n shell command, shell input or verifier containing a hidden Unicode character\n (bidirectional controls, zero-width and other invisible characters, tag\n characters, stray variation selectors, and terminal control characters\n except ASCII control keys in shell input), naming the code point and its\n position.\n\n ```toml\n [[auto_review.allow]]\n id = \"read-only-inspection\"\n action_kind = \"read\"\n reason = \"Read-only inspection is safe to run automatically.\"\n\n [[auto_review.block]]\n id = \"no-release-publish\"\n action_kind = \"publish\"\n reason = \"Release and publish actions require maintainer review.\"\n ```\n\n Rule matchers are exact `tool` and/or `action_kind`. At least one matcher is\n required. `action_kind` accepts the six decision-relevant kinds `read`,\n `write`, `shell`, `external`, `publish`, and `destructive`. Invalid names\n fail config validation instead of silently broadening into another policy\n class. In block rules, the old names remain conservative compatibility\n aliases: `network`, `git`, `mcp_action`, `browser`, and `unknown` map to\n `external`; `secret` maps to `destructive`; and `mcp_read` maps to `read`.\n Retired narrow kinds in allow rules fail validation rather than widening to\n a broader class. The retired `text_contains` matcher likewise fails\n validation instead of silently broadening an old intent-dependent rule.\n Fallback holds in interactive Auto-Review escalate to one stateless guardian\n request. The request contains the exact held call and deterministic\n observations as separate JSON fields. Conversation history, skill\n instructions, attached file contents, and other expanded model context are\n excluded. The guardian does not infer user intent or compute an authorization\n score. It exposes no tools and returns a risk level, allow/deny, and a\n rationale. High or critical risk cannot auto-run even if the model says\n allow. An oversized exact call is denied rather than truncated. Exactly one\n reviewer request is made; incomplete or malformed output, timeout,\n cancellation, provider failure, or an empty rationale all fail closed. The\n deterministic floor is never model-reviewed, and headless adapters use the\n deterministic-only tier. The pinned Codex, Kimi, and DeepSeek source\n boundaries are linked from [Permission Posture](MODES.md#permission-posture).\n Reviewer outcomes emit `tool.auto_review` audit events with\n `gate = \"guardian\"`.\n\n Auto-review decisions emit `tool.auto_review` audit events with\n `gate = \"deterministic\"` when tool\n audit logging is enabled. Future PreToolUse/PostToolUse hooks can add\n observer input around this layer, but the configured auto-review policy is\n evaluated before a tool call is allowed to proceed.\n- `managed_config_path` (string, optional): managed config file loaded after user/env config.\n- `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values.\n- `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`.\n- `subagents.*` (optional compatibility table): manual per-role model pins\n for direct and Workflow `agent` starts. An explicit saved profile wins,\n then a manual role pin, then a unique saved role pin. Conflicting tool\n `model` or `model_strength` choices are refused before admission. Unpinned\n roles allow task model/strength choices before inherited defaults.\n `[subagents.roles.] model = \"provider/model\"` folds into the existing override\n map and wins over `[subagents.models]`, then the convenience keys. Structured\n canonical role keys win over legacy aliases. Only this structured syntax\n separates the explicit provider from the model suffix; unknown providers\n fail before admission. Bare structured model ids inherit the active provider.\n Legacy scalar/map values preserve namespaced provider-owned ids unchanged.\n Supported convenience keys are\n `default_model`, `worker_model`, `scout_model`, `planner_model`,\n `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`,\n `launch_concurrency`, `token_budget`, `api_timeout_secs`, and\n `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`,\n and `review_model` remain accepted as aliases. The `[subagents]\n max_concurrent` value overrides\n top-level `max_subagents` and is also clamped to `1..=128`. `[subagents]\n max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total\n of queued plus running sub-agents; it defaults to `1024`\n (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`,\n applied at `config.rs:6400`) so high-fanout turns can queue and drain while\n runtime launch pressure remains bounded, and is clamped to\n `max_concurrent..=1024`. `[subagents]\n launch_concurrency` sets how many direct children start at once before the\n rest queue for a launch slot; it defaults to the resolved `max_subagents` cap\n and is clamped to `1..=max_subagents` (the deprecated\n `interactive_max_launch` key is accepted as an alias, with the new key\n winning when both are set). `[subagents] token_budget` is an optional\n aggregate token ceiling for each root `agent` run and its descendants; unset\n or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs`\n controls the per-step API timeout for sub-agent model calls and is clamped to\n `1..=3600`, with `0` or unset preserving the 600 second default. A timeout\n after possible dispatch interrupts with a preserved checkpoint and unknown\n outcome. Only a transient connection timeout proven to precede dispatch uses\n exponential backoff, up to 5 retries.\n `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup,\n defaults to `300`, and is clamped to `30..=3600` while staying above the\n resolved API timeout. `[subagents.providers.]` accepts the same\n fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`,\n `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`,\n `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global\n `[subagents]` value for any key you omit. Provider keys accept canonical\n names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience\n aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek:\n\n ```toml\n [subagents]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n max_depth = 6\n\n [subagents.providers.deepseek]\n max_concurrent = 20\n launch_concurrency = 20\n max_admitted = 200\n\n [subagents.providers.glm]\n max_concurrent = 4\n launch_concurrency = 3\n max_admitted = 12\n max_depth = 2\n\n [subagents.providers.openrouter]\n max_concurrent = 5\n launch_concurrency = 3\n max_admitted = 20\n ```\n\n `/config subagents status` prints both global values and the active\n provider's resolved profile so rate-limit tuning is visible in the TUI.\n `[subagents.models]` accepts lower-case Fleet role keys such as `worker`,\n `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys\n remain accepted during v0.9.x. Values are validated\n against the active provider at spawn time; direct DeepSeek requires DeepSeek\n IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs\n through to that provider. To route a child to a different provider than the\n parent session, save a Fleet/AgentProfile with explicit `provider` and\n `model` fields (including user-named custom providers such as `lm-studio`)\n and call `agent(profile: \"...\")`; see [SUBAGENTS.md](SUBAGENTS.md).\n- `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is\n a directory containing `SKILL.md`). After workspace trust, project skills\n take precedence, with `.codewhale/skills` ahead of compatible roots such as\n `.agents/skills`. Flat `./skills` requires explicit opt-in below. The runtime\n also discovers global agentskills.io-compatible `~/.agents/skills` and the\n broader Claude-ecosystem\n `~/.claude/skills`. With the default bundle policy and no `skills_dir` override, first launch installs versioned bundled skills for common\n workflows including skill creation, delegation, MCP/plugin scaffolding,\n documents, presentations, spreadsheets and PDFs. Feishu/Lark is outside the\n default pack. Only\n Codewhale-owned roots (`/.codewhale/skills` and\n `~/.codewhale/skills`) are writable install/import targets; compatible harness\n roots stay read-only. Bare `/skills` opens Extensions → Skills (owned-only,\n zero network); `/skills manage` opens the manager. See [SKILLS.md](SKILLS.md)\n for the manager, audit statuses, provenance markers, and mutation rules, and\n [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary\n between portable `SKILL.md` bundles and Claude Code plugin runtimes.\n- `[skills.bundled].install` (`\"auto\"` default, `\"all\"`, `\"none\"`): controls bundle writes independently of discovery. `auto` keeps deletion-as-opt-out and disables installation when `skills_dir` is set. `all` repairs enabled missing skills into `$CODEWHALE_HOME/skills` (normally `~/.codewhale/skills`) even with a discovery override. `none` writes nothing and leaves the marker untouched. Modified and managed bodies are preserved in every mode.\n- `[skills.bundled].selected` / `disabled` / `explicit_only` (arrays of names, default `[]`): selection limits installation and discovery; empty selects the whole bundle. Disabled wins and reveals lower-root copies. Explicit-only retains user invocation while removing the shipped copy from the model index and model loading. Aliases share canonical policy. These settings affect recognized global bundle copies only and never delete files. Profile fields merge separately; `[]` clears an inherited list. `doctor` warns about unknown/ignored entries and `/skills inspect` reports per-path reasons. See [bundled skills](SKILLS.md#bundled-skills).\n- `[skills].listing_max_description_chars` (integer, default `120`, range `1..=40000`): ambient-index description cap, shared by parent and child prompts and `/skills inspect`.\n- `[skills].listing_budget_fraction` (finite number, default `0.002`, greater than `0` and at most `1`): share of the route window for that same index, estimated at four characters per token and clamped to `2400..=40000` characters. Explicit profile fields override inherited values independently; `/context` uses the same resolution. Full skill loading is unaffected.\n- `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session\n skill discovery ignores cross-tool roots such as `.claude/skills`,\n `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still\n scans `/.codewhale/skills`, `~/.codewhale/skills`, and any explicit\n `skills_dir` override. The Skills Manager can still toggle a local compatible\n audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md).\n- `[skills].flat_workspace_root` (bool, default `false`): opt in to the flat\n `/skills` compatibility root after workspace trust. Without this\n opt-in it is an audit candidate only; an explicit `skills_dir` remains an\n alternative. `scan_codewhale_only = true` excludes the flat compatibility\n root regardless of this flag, unless it is the explicit `skills_dir`.\n- `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by\n `/skills --remote`, `/skills suggest `, `/skills sync`, and `/skill\n install|update`. The default manager open path does not contact the registry.\n- `[verifier].enabled` (bool, default `false`): enables automatic\n claim-of-done verifier preview once that runtime trigger is active. The\n manual `run_verifiers` tool is still available when this is false.\n- `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with\n legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent.\n Custom paths must be absolute; a relative value falls back to the user-global\n path so changing the launch directory cannot silently change the MCP pool.\n It is visible in `/config` and can be changed from the TUI. The new path is\n used immediately by `/mcp`; run `/mcp reload` to rebuild the model-visible\n MCP tool pool without restarting the TUI. A headless runtime still needs a\n restart because its `ConfigReload` request does not refresh MCP connections.\n- `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with\n legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and\n is used by the model-visible `note` tool.\n- `[memory].enabled` (bool, optional): defaults to `false`. When `true`,\n the TUI loads the user memory file into a `` prompt block,\n enables `# foo` quick-capture in the composer, surfaces the `/memory`\n slash command, and registers the `remember` tool. The same toggle is\n available via `DEEPSEEK_MEMORY=on`.\n- `memory_path` (string, optional): anchors the native memory store. The\n configured filename is **not** the file that is written. Under the Native\n backend (the only backend) the store is re-rooted to\n `/memory/global/MEMORY.md` — so the default\n `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md`\n (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See\n [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer\n prefix, `/memory` slash command, `remember` tool, opt-in toggle).\n- `snapshots.*` (optional): side-git workspace snapshots for file rollback:\n - `[snapshots].enabled` (bool, default `true`)\n - `[snapshots].max_age_days` (int, default `7`)\n - snapshots live under\n `~/.codewhale/snapshots///.git`, with legacy\n `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and\n never use the workspace's own `.git` directory\n- `context.*` (optional):\n - `[context].project_pack` (bool, default `false`): include a deterministic\n project context pack (a large pretty-printed directory listing) in the\n stable prompt prefix (#4781). Useful for weak tool-calling models; the\n model can rebuild the same information with one `File` call.\n - The removed seam-manager keys (`enabled`, `verbatim_window_turns`,\n `l1_threshold`, `l2_threshold`, `l3_threshold`, `seam_model`) are\n ignored if an older config still carries them; they no longer load.\n- `compaction.*` (optional, config.toml): how a compaction pass behaves once\n it fires. `auto_compact` / `auto_compact_threshold_percent` (settings.toml)\n still decide *when* it fires. Both keys are absent by default, and absent\n means the built-in behaviour, unchanged:\n - `[compaction].summary_instructions` (string, default empty): standing\n operator instructions appended to the summarizer prompt as a clearly\n delimited \"Additional instructions from the operator\" section on **every**\n pass, manual and automatic — the effort-free counterpart to a one-off\n `/compact `, which still composes after this text. Useful for\n \"always list exact file paths and line numbers\", \"always restate open\n decisions and their trade-offs\", \"always write a TL;DR first\". Truncated\n at 4 000 characters with a warning naming the key; whitespace-only reads\n as unset. The summarizer still runs with no system prompt and no tools —\n this suffix is the only operator-authored input it sees.\n - `[compaction].retained_user_message_tokens` (int, default `20000`, clamped\n to `2000`–`200000`; also accepted as `retained_user_message_max_tokens`):\n token budget for the recent plain user messages kept **verbatim** in the\n replacement history. Raising it keeps more of the user's own earlier\n messages instead of only whatever the lossy summary captured; the\n last-round survival contract still applies on top. The `/compact` receipt\n names the effective budget and whether operator instructions were applied,\n so you can tell the knob took effect.\n- `retry.*` (optional): HTTP retry settings. The following backoff fields govern\n ordinary transport retries; HTTP 429 uses the independent policy below:\n - `[retry].enabled` (bool, default `true`)\n - `[retry].max_retries` (int, default `3`): ordinary transport retries\n - `[retry].initial_delay` (float seconds, default `1.0`)\n - `[retry].max_delay` (float seconds, default `60.0`)\n - `[retry].exponential_base` (float, default `2.0`)\n - `[retry].jitter` (bool, default `true`): randomize ordinary transport backoff\n - `[retry].jitter_factor` (float, default `0.1` = ±10%; clamps to `0.0..=1.0`)\n - `[retry].respect_retry_after` (bool, default `true`): wait for a server\n `Retry-After` preference retained for legacy retry configuration; it does not\n disable HTTP 429 server minima\n\n HTTP 429 recovery has its own validated `[retry.rate_limit]` fields:\n\n - `max_attempts` (integer, default `20`, range `1..=1000`): total attempts,\n including the first\n - `recovery_budget_secs` (integer seconds, default `600`, range `1..=86400`)\n - `initial_delay_secs` (integer seconds, default `5`)\n - `max_delay_secs` (integer seconds, default `60`), with\n `1 <= initial_delay_secs <= max_delay_secs <= 86400`\n\n This policy always honors server minima and uses equal jitter, independently\n of the legacy `initial_delay`, `max_delay`, `exponential_base`, `jitter`,\n `jitter_factor` and `respect_retry_after` fields. `max_retries = 0` does not\n disable 429 recovery; set `retry.rate_limit.max_attempts = 1` to disable only\n automatic 429 retries, or `retry.enabled = false` to disable this HTTP retry\n loop. A safe pre-dispatch HTTP/1.1 fallback inside a stream-opening operation\n remains possible. The one-hour limit on actual provider pauses and automatic sleeps\n still applies. See [HTTP rate-limit retries](rate-limit-retries.md) for the\n budget, server-minimum and active-time boundaries.\n\n `[retry]` schedules HTTP-request retries inside the client. Each physical\n streaming request gets its own `stream.open_timeout_secs` response-header\n wait. Provider backoff and honored `Retry-After` waits happen between\n attempts, outside that timer, so the configured retry count remains\n reachable. Inference retries require an ordinary rate limit or a proven\n transient connection failure before dispatch. Only that connection failure\n can switch once to HTTP/1.1; a header stall cannot. Both inference clients\n disable redirects and reqwest protocol retries. Explicit plan-window\n exhaustion is a terminal quota error. Server failures, uncertain timeouts\n and interrupted streams preserve unknown coverage and stop without\n automatically reissuing inference. Read-only catalog GETs retain bounded\n retries at their actual method owner. The `[stream]` keys below bound safe\n pre-dispatch recovery and stream consumption; legacy `tui.stream_max_*`\n keys remain configuration fallbacks.\n- `[notifications]`: notification delivery, attention, categories and audio share one\n policy. `quiet = true`, `method = \"off\"`, `condition = \"never\"` and disabled\n categories suppress both the banner and Codewhale's selected sound.\n- `notifications.method`: `auto` (default), `osc9`, `kitty`, `ghostty`, `bel`, `off`.\n- `notifications.condition`: `unfocused` (default), `always`, `never`. When absent,\n the legacy `tui.notification_condition` remains the fallback. `always` also\n bypasses the duration threshold; `unfocused` requires two seconds away.\n- `notifications.threshold_secs`: nonnegative integer, default `30`.\n- `notifications.include_summary`: boolean, default `false`.\n- `notifications.sound`: optional `off`, `whale`, `bell`, `beep`, `file`.\n A selected value controls audio across enabled categories. Absent keeps legacy\n `completion_sound` and `event_sound` choices; `off` overrides both.\n- `notifications.sound_file`: custom local WAV path for `sound = \"file\"` or legacy\n `completion_sound = \"file\"`.\n- `notifications.subagent_completion`: `always`, `final-only` (default), `off`.\n Covers all background work that finishes: sub-agents, background shells and\n durable tasks. `final-only` sends one notice naming everything that finished\n once no agent, workflow or durable task is still running; a running shell\n (a dev server, a watcher) never holds it back. `always` sends one per item.\n- `notifications.quiet`: boolean, default `false`.\n- `notifications.events`: six boolean categories, all enabled by default; see below.\n- `notifications.completion_sound`: legacy completion cue, default `off`, with the\n same values as `sound`. Used only when `sound` is absent.\n- `notifications.event_sound`: legacy `enabled` (default `false`), `events`\n (default `[\"turn-complete\", \"approval-needed\"]`), and `quiet` (default `false`).\n `min_interval_ms` (default `2000`) applies to each category's audio in both modes.\n- `tui.alternate_screen` (string, optional, default `auto`): which screen an interactive session starts on. `auto` and `always` start on the TUI-owned alternate screen; `never` starts in inline mode — a ratatui viewport the full height of the terminal with no alternate screen, so the shell's scrollback survives the session and stays scrollable after exit. `/fullscreen` and `/inline` switch it in-process; a switch that the terminal refuses rolls back and says why. Inline mode paints the whole transcript inside its viewport — nothing is written into the host scrollback while the session runs.\n- `tui.mouse_capture` (bool, optional, default `true` on non-Windows terminals and on Windows Terminal/ConEmu/Cmder when the alternate screen is active; `false` on legacy Windows console and inside JetBrains JediTerm — PyCharm/IDEA/CLion/etc. — where mouse-event escapes leak into the input stream as garbled text, see #878 / #898): enable internal mouse scrolling, transcript selection, right-click context actions, and transcript scrollbar dragging. TUI-owned drag selection copies the intersected cells, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane; the payload is Markdown source by default, see `tui.selection_copy_markdown` below. Set this to `false` or run with `--no-mouse-capture` for raw terminal selection; set it to `true` or run with `--mouse-capture` to opt in anywhere it's defaulted off. On raw terminal selection, especially on legacy Windows console or when mouse capture is disabled, selection may cross the right workbar and include visual wraps because the terminal, not the TUI, owns the selection.\n On Linux, finishing a transcript or composer selection quietly copies text to\n PRIMARY, leaving the regular clipboard unchanged. Middle-click inside the\n composer pastes PRIMARY at the pointer without submitting it. This uses native\n X11 or Wayland data control; compositors must support PRIMARY selection. Over\n SSH without a forwarded graphical display, use your terminal's selection/paste\n gestures or `--no-mouse-capture`. Explicit Copy still uses the regular clipboard.\n\n- `tui.selection_copy_markdown` (bool, optional, default `true`): copy TUI-owned\n transcript selections (drag release, context-menu Copy, and `Cmd+C`/`Ctrl+C`\n on an active selection) as Markdown source instead of rendered text. Every\n intersected cell serializes through the same canonical projection `Ctrl-Y`\n and `/copy` use — user and assistant cells keep their authored Markdown,\n other cells keep their full transcript form — partial intersections round out\n to whole cells, cells join with blank lines, and a toast names the copied\n cell count. Set `false` to copy the rendered text as displayed. Composer\n selections and the Linux PRIMARY auto-copy are unchanged; PRIMARY always\n carries rendered text.\n\n- `tui.stream_chunk_timeout_secs` (int, optional, default `900`): per-SSE-chunk idle timeout for streamed model responses. Slow local or compatible servers can raise this with `/config stream_chunk_timeout_secs ` (add `--save` to write canonical `stream.chunk_timeout_secs`); `0` maps to the default and explicit values must be `1..=3600`. The legacy `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` env var is still honored when this key is omitted.\n- `tui.osc8_links` (bool, optional, default on for macOS/Linux, off for Windows): emit OSC 8 escape sequences around URLs in transcript output so supporting terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm, Alacritty, recent gnome-terminal/konsole) can open them with the terminal's link gesture—usually Cmd-click on macOS and Ctrl-click on Linux/Windows. Terminals without OSC 8 support render the plain label and ignore the escape. The escapes are emitted out-of-band (not inside buffer cells), so column corruption is not a concern; set `false` only for terminals that misrender the OSC 8 terminator itself. Windows legacy consoles default off; opt in with `true`.\n- `tui.max_model_steps` (int, optional, default uncapped): optional model-step ceiling for one ordinary turn. Omission or `0` leaves model steps uncapped; explicit positive values are preserved (`4294967295` is the compatibility value for no limit). Headless `exec` and Fleet workers also have no implicit model-step ceiling; `exec --max-turns N` and positive worker budgets still apply. At exhaustion the turn ends `Failed` with `Maximum model steps reached before completion (limit: N)` after one bounded final-report response when needed. Cumulative wall-clock and per-stream limits remain independent. Active interactive goal turns use `goal.max_steps` instead (also uncapped by default); see the Goal loop section below.\n- `tui.turn_wall_clock_secs` (int, optional, default: no limit): cumulative per-turn wall-clock budget in seconds, measured across every model step of one turn (not per request). Time blocked on a human approval is excluded. Omitted or `0` means no limit; positive values are preserved without an added runtime ceiling. When exhausted the turn stops before authorizing another billable request with a message naming the limit and the key to raise.\n- `tui.stream_max_resumes` (int, optional, default `3`): turn-level recovery after a proven transient connection failure before request dispatch. `0` disables this additional recovery; values clamp to `0..=10`. Missing response headers, empty or partial streams, sleep and network drops never authorize inference replay.\n- `tui.stream_max_transparent_retries` (int, optional, default `2`): retained for configuration compatibility, clamped to `0..=10`. Interrupted streams no longer trigger transparent inference reissues, including before visible content.\n- `tui.stream_max_errors` (int, optional, default `5`): retained for configuration compatibility; `0` selects the default and positive values clamp to `1..=50`. Core ends an uncertain stream on its first read error or error frame regardless of this value.\n- `tui.stream_open_timeout_secs` (int, optional, default `45`): wait for a streaming request's response headers (connection setup included). A header stall ends with an unknown outcome and does not replay over HTTP/1.1. Omitted or `0` falls back to `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS`, then the default; values clamp to `5..=300`.\n- `tui.connect_timeout_secs` (int, optional, default `30`): TCP/TLS connect timeout for the model HTTP client. Omitted or `0` uses the default; values clamp to `1..=300`.\n- `tui.force_http1` (bool, optional, default `false`): pin the model HTTP client to HTTP/1.1, for provider edges or proxies that mishandle long-lived HTTP/2 streams. `CODEWHALE_FORCE_HTTP1=1` does the same; either one pins.\n- `tui.stream_max_content_mb` (int, optional, default `10`) and `tui.stream_max_duration_secs` (int, optional, default `1800`): per-step caps on one stream's accumulated content and wall-clock duration. `0` selects the default; values clamp to `1..=512` MB and `10..=86400` seconds.\n- `transcript.prose_measure` (positive integer, optional, default absent = full width): wrap cap, in columns, for prose cells — user messages, assistant answers, and reasoning/thinking blocks — in the live transcript (#5436). Absent (or `0`) spends the full content width, consistent with tool/status cells and the #5322 wide-frame decision; the former 105-column prose rail is gone. Set a positive whole number (e.g. `prose_measure = 120` under `[transcript]`) to restore a bounded reading measure on ultrawide terminals. Narrow terminals always keep their content width — the cap clamps from above only. Tool, diff, and status cells never inherit this cap. Invalid values (negative or non-integer) are rejected at startup with a `transcript.prose_measure` config error. Resolved once per render pass, so the main transcript cache and the full-screen overlay always agree on the effective width.\n- `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`).\n- `features.*` (optional): feature flag overrides (see below).\n\n### Stream and transport settings\n\n`[stream]` is the canonical table for model-stream policy and its HTTP clients.\n`codewhale config dump` and `codewhale config get stream` show effective values\nfrom the runtime resolver, including environment fallbacks and clamps; this does\nnot persist defaults. Set or unset individual fields with, for example,\n`codewhale config set stream.open_timeout_secs 120` and\n`codewhale config unset stream.open_timeout_secs`. Per-run\n`--set stream.open_timeout_secs=120` uses the same validation.\n\n| `[stream]` key | Default | Accepted behavior | Legacy `[tui]` fallback |\n| --- | --- | --- | --- |\n| `open_timeout_secs` | 45 | Positive values clamp to 5–300; 0 or omission falls through to the environment/default | `stream_open_timeout_secs` |\n| `chunk_timeout_secs` | 900 | 0 selects default; positive values clamp to 1–3600 | `stream_chunk_timeout_secs` |\n| `max_resumes` | 3 | Proven transient pre-dispatch recovery only; 0 disables, maximum 10 | `stream_max_resumes` |\n| `max_transparent_retries` | 2 | Compatibility value; interrupted streams never reissue inference | `stream_max_transparent_retries` |\n| `max_stream_errors` | 5 | Compatibility value; Core ends on the first uncertain stream error | `stream_max_errors` |\n| `max_duration_secs` | 1800 | Per-stream wall clock; 0 selects default, positive values clamp to 10–86400 | `stream_max_duration_secs` |\n| `max_content_mb` | 10 | Per-stream content; 0 selects default, positive values clamp to 1–512 MiB | `stream_max_content_mb` |\n| `connect_timeout_secs` | 30 | TCP/TLS setup; 0 selects default, positive values clamp to 1–300 | `connect_timeout_secs` |\n| `force_http1` | false | Boolean; a truthy environment pin always enables HTTP/1.1 | `force_http1` |\n| `tcp_keepalive_secs` | 30 | Idle time before TCP keepalive probes; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_interval_secs` | 15 | PING interval on active HTTP/2 connections; 0 disables, positive values clamp to 1–3600 | none |\n| `http2_keep_alive_timeout_secs` | 20 | PING acknowledgement deadline; 0 selects default, positive values clamp to 1–3600 | none |\n\nFor each field, an explicit canonical value wins over the legacy `[tui]` field,\nincluding zero or false; an absent canonical field preserves the legacy value.\nThe existing environment precedence is unchanged: a positive configured header\nwait wins over `CODEWHALE_STREAM_OPEN_TIMEOUT_SECS` (then its `DEEPSEEK_` alias);\nzero falls through to those variables. An omitted chunk timeout uses\n`CODEWHALE_STREAM_IDLE_TIMEOUT_SECS` (then its `DEEPSEEK_` alias), whereas an\nexplicit zero uses 900. `CODEWHALE_FORCE_HTTP1` (legacy `DEEPSEEK_FORCE_HTTP1`)\nis ORed with the selected config flag, so a false config flag cannot defeat a\ntruthy environment pin. Profile overlays merge canonical fields independently.\n\nHTTP transport values apply to all newly constructed model, catalog and HTTP/1\nfallback clients. They do not rebuild an active client, govern MCP/other network\nservices, or send HTTP/2 PINGs on idle pooled connections. HTTP/2 knobs have no\neffect when HTTP/1.1 is pinned. The operating system controls TCP probe details;\nthese settings do not disable certificate validation. `[retry]` still owns the\nHTTP-request backoff schedule independently of stream-level budgets.\n\n### Workspace notes\n\n`/note` manages a simple notes file in the current workspace at\n`.codewhale/notes.md` (legacy `.deepseek/notes.md` is the fallback path when\nno `.codewhale/notes.md` exists yet).\nExisting `/note ` usage still appends a note.\nThe management forms are:\n\n| Command | Action |\n|---|---|\n| `/note ` | Append a note (legacy shorthand) |\n| `/note add ` | Append a note explicitly |\n| `/note list` | List notes with temporary 1-based numbers |\n| `/note show ` | Show the full note at number `n` |\n| `/note edit ` | Replace note `n` with new text |\n| `/note remove ` | Delete note `n`; `rm` and `delete` are aliases |\n| `/note clear` | Empty the workspace notes file |\n| `/note path` | Show the resolved workspace notes path |\n\nThe numbers shown by `/note list` are not stored in the file; they are derived\nfrom the current order each time notes are read. This keeps the file format\ncompatible with the existing `---`-separated notes.\n\n### User memory\n\nUser memory is split across one top-level path setting and one opt-in\ntoggle table:\n\n```toml\n# Anchors the store only — actual writes go to\n# ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md).\nmemory_path = \"~/.codewhale/memory.md\"\n\n[memory]\nenabled = true\n```\n\nNotes:\n\n- `memory_path` stays at the top level beside `notes_path` and\n `skills_dir`; it is not nested under `[memory]`.\n- The configured path is an **anchor**: its parent directory gains\n `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`.\n Pointing `memory_path` at the native layout path itself would double-nest\n (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style\n anchor filename.\n- `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment.\n- `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`)\n flips the feature on without editing `config.toml`.\n- The feature is inert when disabled: no file is injected, `# foo`\n falls through to normal message submission, and the model does not\n see the `remember` tool.\n- See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory`\n command surface.\n\n### Goal loop (`[goal]`)\n\nOperate-mode goals run to their completion gate with no default token, time, or\ncontinuation ceiling (#5052). Token/time budgets, when explicitly supplied,\nare telemetry only and do not stop a goal. Users who want a circuit breaker can\nopt into one:\n\n```toml\n[goal]\n# Optional safety backstop on automatic goal continuation passes.\n# Default: 0 (unlimited). Set a positive value to opt into a ceiling.\nmax_continuations = 100\n\n# Optional cancellable quiet period between successful turns. This is useful\n# for coordinator goals that should poll on a cadence instead of keeping one\n# provider turn open. Default: 0 (continue immediately).\ncontinuation_delay_seconds = 300\n\n# Optional per-turn model-step ceiling while a goal is active (#6512).\n# Default: uncapped (0 or absent). Positive values are preserved.\n# This bounds each provider turn, never the number of continuation passes.\nmax_steps = 0\n\n# Optional independent judges (#6013). Default: false (the model's own\n# update_goal call is final). When true, one adversarial critique of the claim\n# runs before `update_goal` completes (complete_judge) or blocks (blocked_judge)\n# the goal. Each judged call is billed as auxiliary usage.\ncomplete_judge = false\nblocked_judge = false\n```\n\nThe effective delay is capped at 86,400 seconds (24 hours); use an automation\nfor schedules that are less frequent than once per day.\n\nWhen an explicit backstop fires, the goal pauses with a status message naming\n`[goal] max_continuations` and a warning is logged; resume the goal after\ninspecting progress, or raise/disable the backstop.\n\n`[goal] max_steps` governs one engine turn at a time. Like ordinary interactive\nturns, omitted or `0` leaves model steps uncapped. Explicit per-invocation\nceilings — `exec --max-turns N`, child-worker caps — always win over it. At exhaustion\nit gets one bounded final report and the turn classifies as budget-exhausted. An\nunfinished goal then pauses with the BudgetLimit reason instead of re-arming\nanother goal turn — resume it explicitly after reviewing the report. Wall-clock\nand stream protections are separate and still apply.\n\nWith `[goal] complete_judge = true`, a refuted completion claim keeps the goal\nactive and records the judge's findings as critical gaps. A missing,\nunparseable, uncertain or failed verdict never completes the goal; it records\none fixed gap, so a persistent judge outage ends in the existing no-progress\npause rather than a silent pass. With `[goal] blocked_judge = true`, a refuted\nblocker keeps the goal active, and an unverifiable one hands the goal back as a\nyield that your next message resumes. The judge reads only the objective, the\nevidence and the verification receipt, not the transcript or the diff. The\noperator `POST /v1/threads/{id}/goal/complete|block` routes and `/goal`\ncommands stay ungated.\n\nThe delay starts only after a successful turn while an explicitly created goal\nis still active. `/goal pause`, `/goal done`, `/goal blocked`, `/goal clear`,\nEsc, or Ctrl+C cancels a pending continuation before another provider request\nstarts. Failed turns and policy/route failures never schedule another turn.\nOnly the numeric cadence is stored in config; no prompt, credential, or secret\nis persisted for the loop.\n\n### Reasoning-only recovery (`[reasoning_only]`)\n\nWhen a model finishes with only reasoning and no answer or tool call, Codewhale\ncan request an answer again. A clean stop permits two retries by default:\nthe first keeps the request unchanged, and later retries carry a request-scoped\nnudge. The default nudge is `Continue: give your answer, or make the next tool call.`\n\n```toml\n[reasoning_only]\nmax_reprompts = 2\non_output_limit = false\n```\n\nSet `on_output_limit = true` to also retry `length`/`max_tokens` stops that\nproduced no answer or tool call, including providers that did not stream their\nhidden reasoning. This option is off by default. Output-limit retries carry the\nnudge immediately and share the same `max_reprompts` allowance. Set\n`max_reprompts = 0` to disable both reasoning-only and output-limit recovery;\ntransport and empty-stop recovery have their own existing limits.\n\nThis applies to ordinary interactive, exec and Runtime API engines and Fleet\nworkers. Workers use the captured parent configuration. RLM keeps its separate\ncompletion/FINAL/REPL stop protocol and does not use this option.\n\nEvery retry is a fresh request and may repeat the model's reasoning. It does not\ncarry hidden reasoning forward, raise output limits or bypass route, context,\nturn, cancellation or tool-admission controls. Each request can incur provider\ncost. Existing output-limit overrides still apply, and request preparation may\nreduce or refuse an allowance when input grows. Restart existing engines after\nchanging these settings.\n\nThe nudge appears in one outbound request without joining conversation history\nor later resumed model context. Retry status retains its stable protocol prefix\nand localizes its human text. Its receipt is visible in the transcript; Fleet\nalso stores an owner-bound prepared-request receipt in its existing private\nartifact before dispatch. A prepared receipt does not claim a completed model\ncall. Existing version-1 artifacts remain readable.\n\nTo customize or disable the nudge:\n\n```toml\n[reasoning_only]\nreprompt_message = \"Give your answer, or make the next tool call.\"\n```\n\nAn empty `reprompt_message = \"\"` disables the nudge, including on output-limit\nretries; the retry still counts against `max_reprompts` and uses the unchanged\nrequest. Configuration with recovery enabled does not guarantee the model will\ncomplete a long proof within its allowance.\n\n### Notifications\n\nNotification controls are available in the existing `/config` settings view,\nfrom terminal commands, and through the CLI. All write the same `config.toml`\nkeys. Terminal changes apply immediately; add `--save` to keep them. CLI writes\napply when the next process loads its configuration.\n\n```sh\ncodewhale config set notifications.sound whale\ncodewhale config set notifications.events.approval-needed false\ncodewhale config set notifications.quiet true\ncodewhale config get notifications\ncodewhale config unset notifications.quiet\n```\n\n```text\n/config notifications sound whale --save\n/config notifications condition unfocused --save\n/config notifications quiet true\n/config notifications status\n```\n\nNested CLI edits preserve TOML types, unrelated keys and comments. Unset removes\nonly the selected leaf. `notifications.sound legacy` in the TUI, or CLI unset of\n`notifications.sound`, restores previous sound choices. Invalid values are\nrejected before file or session changes. In an active TUI profile, saved edits\nupdate its notification table when it owns one; otherwise they update the\ninherited root table. The settings detail keeps saved and current values distinct.\n\n```toml\n[notifications]\nmethod = \"auto\" # auto | osc9 | kitty | ghostty | bel | off\ncondition = \"unfocused\" # unfocused | always | never\nthreshold_secs = 30\ninclude_summary = false\nsound = \"whale\" # optional; sound is opt-in, not enabled by default\nquiet = false\n\n[notifications.events]\nturn-complete = true\nsubagent-terminal = true\napproval-needed = true\ninput-needed = true\nelevation-needed = true\nmodel-notify = true\n```\n\n`quiet = true` mutes every category without changing saved choices. `method =\n\"off\"` also stops both banner and selected audio. Disabling a category stops its\nsound. Attention and duration gates apply before any sink runs. Title animation\ncompletion is silent; only the authorized notification event can request audio.\nSuccessful turn completion is a notification category; failed/cancelled turns do\nnot create a success notification.\n\n`auto` chooses a recognized terminal protocol or the existing macOS native\nfallback; unknown terminals remain unsupported and never invent a bell. `bel`\nis an audio-only transport: one selected cue is dispatched, without a second\ntransport bell. With `sound = \"off\"`, that transport is silent. `osc9`, `kitty`\nand `ghostty` use their terminal notification protocols; tmux passthrough is\npreserved. Terminal/OS notification preferences still govern display, attribution\nand any sound the host itself adds.\n\nBy default the terminal must stay unfocused for two seconds. `condition =\n\"always\"` allows foreground notifications and bypasses the duration threshold;\n`\"never\"` suppresses all delivery. The canonical condition takes precedence over\nlegacy `[tui].notification_condition`.\n\nThe bundled `whale` is a 1.55-second original whale-inspired cue, with no\nthird-party recording. It remains an opt-in candidate pending listening approval.\nWAV playback uses a background worker: macOS `/usr/bin/afplay`, Linux `aplay`\nfrom [ALSA utilities](https://github.com/alsa-project/alsa-utils), or Windows\n`PlaySoundW`. A missing player/file or unsupported platform has no fallback bell.\nOnly one WAV plays at a time. A worker-start receipt is a dispatch attempt, not\nproof of audible playback or OS acceptance. The existing macOS `osascript`\nbanner retains Script Editor attribution; this Core change does not provide a\nbranded native Apps banner.\n\n#### Previous sound settings\n\nWhen `notifications.sound` is absent, completion uses a non-off\n`completion_sound` selection, and other events use the existing event allow-list.\nThese are compatibility inputs to the same audio decision, not separate playback\npaths. When the global sound is selected, it takes precedence over the previous\ncompletion/event choices. The default remains silent unless a legacy sound or\nexplicit `bel` transport was already selected.\n\n```toml\n[notifications.event_sound]\nenabled = false\nevents = [\"turn-complete\", \"approval-needed\"]\nmin_interval_ms = 2000\nquiet = false\n```\n\nLegacy event cues use one bell for completion, subagent completion, input and\nmodel notices; approval/elevation cues use two. The per-category repeat interval\nsurvives settings refreshes. Old unknown event names are ignored on load; new\nCLI/TUI edits require names from the six categories above.\n\nLocal approval/input/elevation prompts and error receipts remain available when\nexternal notifications are muted. Action prompts use the selected UI language\nand retire when that request settles. Repeated live notices keep their first\nexpiry; a later routine update does not hide an unresolved warning at completion.\nOptional plugin suggestion toasts and contextual tips share one session guidance\nbudget. `/config contextual_tips off` hides those toasts while preserving\nrequired notices and explicit plugin review requests.\n\n#### What a notification can contain\n\nA desktop notification is a glance surface: on macOS it can appear on the\nlock screen, and on every platform it is visible to anyone near the machine.\nCodewhale therefore builds notifications from a typed payload with a fixed\nper-event disclosure policy rather than from whatever text was on hand:\n\n| Event | Shown | Never shown |\n|---|---|---|\n| Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — |\n| Sub-agent finished | status line, agent id, preview of the child's summary line | — |\n| Approval needed | the tool name | the tool description, the command, the arguments |\n| Input needed | \"Answer the question in the terminal to continue\" | the question |\n| Sandbox elevation needed | the tool name and the denial reason | the command |\n| `notify` tool | model-supplied title and body | — |\n\nEvery field is capped (80 characters for the status line, 120 for the\nidentifier, 200 for the preview), stripped of control bytes and escape\nsequences, and passed through a redactor that replaces credential-shaped\nstrings with `[redacted]`, reduces absolute local paths to `…/basename`,\nand replaces raw tool JSON with `[details hidden]`. The redactor is\ndeliberately over-eager: an unbroken 40-character run has no word\nstructure, so it is redacted even when it is not a secret.\n\n#### macOS: why the banner says \"Script Editor\"\n\nOn macOS terminals that provide no notification escape of their own —\nApple Terminal, the VS Code and JetBrains embedded terminals, plain tmux\nwithout `LC_TERMINAL` — `method = \"auto\"` falls back to `osascript`'s\n`display notification`. That command posts on behalf of the *bundled*\nhost process, and `/usr/bin/osascript` is unbundled, so macOS attributes\nthe banner to `com.apple.ScriptEditor2`. That attribution supplies the\nScript Editor icon and owns the System Settings → Notifications entry\n(alert style, previews, Do Not Disturb). `display notification` has no\nicon parameter, so this cannot be fixed from the notification code; it\nneeds Codewhale to ship a real `.app` bundle. Tracked in\n[#4834](https://github.com/codewhale-hq/CodeWhale/issues/4834). In the meantime,\niTerm2, WezTerm, Ghostty, and kitty are matched first and use their own\nnotification protocols, and `method = \"osc9\"` / `\"bel\"` / `\"off\"` opt out\nof the `osascript` path explicitly.\n\n## Automations in the terminal\n\nOpen `/automation`, then choose **New automation** (`n`) or **Edit automation**\n(`e`). The form edits the name, multiline prompt, schedule, model, workspace,\nand enabled status. `Tab` moves between fields; `Enter` inserts a newline in\nthe prompt. Schedule presets include daily, weekly, hourly, once, and a custom\nRRULE. Time fields accept `HH:MM`; their arrow controls change the time by\n15 minutes. Weekly day buttons and model search support mouse and keyboard.\n\nThe next-run preview uses the scheduler's local time zone, shown with its UTC\noffset. An enabled automation can run after **Save** (`Ctrl+S`); a paused one\nhas no scheduled next run. **Cancel** (`Esc`) discards the draft. Editing keeps\nexisting permission settings, custom schedules, and additional workspace\nentries unless the corresponding supported field is explicitly changed.\n\nSchedules are evaluated in the machine's local time zone against the wall\nclock. A wall time that does not exist on a spring-forward day is skipped and\nan ambiguous fall-back time fires once. Occurrences missed while Codewhale was\nclosed, asleep, or still running the previous occurrence are coalesced: the\nnext start runs one catch-up occurrence and then continues from the next\nfuture slot, never replaying every missed slot. An occurrence never starts\nwhile an earlier run of the same automation is still queued or running. Each\nrun is recorded durably with its status, timing and error; a run that needs a\ntool approval has no operator to ask and fails once the approval wait expires.\n\nChoosing a concrete model pins both the model and its exact configured\nprovider, including named custom routes. Later changes to the active provider\ndo not move that automation's pin. The default-model choice and legacy\ndefinitions without a provider pin keep the runtime's existing default\nbehavior. New automation records use schema v2 and tasks use v3 so older\nruntimes reject records whose provider pins they cannot preserve.\n\n## Lifecycle Outbox (`[lifecycle_outbox]`)\n\nThe lifecycle outbox is an opt-in, machine-readable stream of session,\nturn, and sub-agent lifecycle events. With a path configured, Codewhale\nappends one JSON line per event to that file — for interactive TUI\nsessions *and* headless `codewhale exec` runs — so a supervisor\n(terminal multiplexer wrapper, automation harness, alerting setup) can\nreact to what happened without scraping the screen or installing per-hook\nshell commands. Unset or empty `path` = the feature is **off** and\nbehavior is unchanged.\n\n```toml\n[lifecycle_outbox]\npath = \"~/.codewhale/notifications/outbox.jsonl\" # unset/empty = OFF\nwebhook_url = \"\" # optional; POSTs events as JSON when set\nwebhook_token = \"\" # optional bearer token for webhook_url\n```\n\n### Events emitted\n\n| Event | Kind | Fired at |\n|---|---|---|\n| `turn_start` | `turn.started` | a new turn begins (TUI TurnStarted; `exec` at message dispatch) |\n| `turn_end` | `turn.completed` / `turn.failed` / `turn.interrupted` | turn completion, kind projected from the turn status |\n| `turn_stalled` | `turn.stalled` | the stall watchdog recovers a wedged turn |\n| `subagent_spawn` | `subagent.spawned` | a sub-agent is spawned |\n| `subagent_complete` | `subagent.completed` | a sub-agent reaches a terminal state |\n| `session_start` | `session.started` | interactive session start |\n| `session_end` | `session.ended` | interactive session end |\n\n### File contract\n\nEach line is a `RuntimeEventEnvelope`:\n\n```json\n{\"schema_version\": 1, \"seq\": 3, \"event\": \"turn_start\", \"kind\": \"turn.started\",\n \"thread_id\": \"…\", \"turn_id\": \"…\", \"item_id\": null, \"timestamp\": \"…\",\n \"created_at\": \"…\", \"payload\": {…}}\n```\n\n- `seq` is monotonic per outbox file and recovers from the last written\n line when a new process opens the file.\n- Lines are written one complete JSON line per append, serialized by an\n internal writer task and flushed before the next event; concurrent\n sessions writing the same path do not interleave bytes mid-line, but\n separate processes each continue from their own recovered `seq`, so seqs\n can repeat across processes sharing one file — prefer one file per\n process for strict uniqueness.\n- Parent directories are created lazily on the first event.\n- Payloads are constructed from bounded, pre-redacted fields only — never\n raw tool arguments, environment, or full transcript text. Free-form\n fields (error messages, previews) are capped at the notification limits\n (80 headline / 120 detail / 200 preview characters) and stripped of\n control bytes.\n\n### Webhook delivery\n\nWith `webhook_url` set, every event is additionally POSTed as\n`{\"at\": \"\", \"event\": {…}}` with\n`Authorization: Bearer ` when a token is configured.\nDelivery is best-effort: failures are logged and dropped, never retried\ninto the agent loop, and a failing webhook never blocks the local file\nappend.\n\n## Control Socket (`[control_socket]`)\n\nPer-session control surface for supervised operation: with the feature\nenabled, the interactive TUI binds one unix domain socket per *running*\nsession at `//control.sock` (mode `0600`;\n`` is the same directory the session store uses, typically\n`~/.codewhale/sessions`). The socket is removed with the session's\nartifact directory, and a stale socket left by a crashed process is taken\nover by the next launch. Unset or `enabled = false` = the feature is\n**off** (the default) and behavior is unchanged. Unix-only; on other\nplatforms the key parses but no socket is bound.\n\n```toml\n[control_socket]\nenabled = false # default: OFF\n```\n\nThe socket speaks newline-framed JSON-RPC, one request per connection:\nwrite one request line, read one response line, close.\n\n```json\n{\"id\":\"1\",\"method\":\"message\",\"params\":{\"text\":\"hello\"}}\n{\"id\":\"2\",\"method\":\"interrupt\",\"params\":{}}\n{\"id\":\"3\",\"method\":\"status\",\"params\":{}}\n```\n\n- `message` — delivers `text` as a structured user message through the\n ordinary composer dispatch path; dispatched immediately when idle,\n queued when a turn is in flight (the response's `delivery` field says\n which).\n- `interrupt` — the Esc-shaped cancel of the active turn; `cancelled`\n reports whether active work was in flight.\n- `status` — answers `turn_state` (`idle` / `in_progress` / `waiting`) and\n `goal` (`objective`, `status`, `paused`).\n\nSuccess responses echo the request id with a `type`-tagged result;\nfailures carry `error.code` (`invalid_request`, `command_error`,\n`timeout`, `server_unavailable`). Requests are bounded at 1 MiB per line\nand a handler that does not answer within 5 s is reported as `timeout`.\n\n## Tool Catalog\n\nCodewhale loads a small core native tool catalog by default and leaves less\ncommon native tools discoverable through ToolSearch. To keep specific native\ntools loaded on every request, add them to `[tools].always_load`:\n\n```toml\n[tools]\nalways_load = [\"Git\", \"notify\"]\n```\n\n### Script tools and overrides\n\nScripts in `~/.codewhale/tools/` (or `[tools].plugin_dir`) that start with a\n`# name:` header become model-visible tools, and `/plugin tools` lists them.\nThe script reads the tool's JSON input on stdin and writes a JSON\n`ToolResult` (`{\"content\": \"...\", \"success\": true}`) on stdout.\n\n```sh\n#!/usr/bin/env sh\n# name: word_count\n# description: Count words in the given text\n# schema: {\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}}}\n# approval: required\n```\n\n`# approval:` takes `suggest` (the default) or `required`; either way the\ntool follows the session's approval setting. A script cannot approve itself:\n`approval: auto` is no longer supported, so such a script gets the default,\nand the runtime log (`~/.codewhale/logs/`) and `/plugin tools` name it.\n\nA script cannot replace a built-in tool either. A script whose `# name:` is\nalready registered is not loaded. `[tools.overrides]` may disable a built-in,\nor add a script or command tool under a name of its own:\n\n```toml\n[tools.overrides]\n\"Web\" = { type = \"disabled\" } # turn a built-in off\n\"audited_shell\" = { type = \"script\", path = \"audit-shell.sh\" } # a new tool\n\"Bash\" = { type = \"script\", path = \"audit-shell.sh\" } # refused: Bash is built in\n```\n\nA `script` or `command` override keyed by a built-in is refused, and the\nbuilt-in stays active. A status line names the key once per session, and the\nruntime log records it. To route a\nbuilt-in through your own wrapper, disable the built-in and register the\nwrapper under a new name. An override keyed by a drop-in script's name still\nreplaces that script. Relative `path` values resolve against the plugin\ndirectory.\n\n### `request_user_input` limits\n\n`request_user_input` asks the user a short batch of multiple-choice questions.\nBoth ceilings are configurable (#5949): raise `user_input_max_questions` when a\nresearch or planning workflow legitimately needs more clarifications, lower it\nwhen interactive triage should stay terse.\n\n```toml\n[tools]\nuser_input_max_questions = 6 # default 6, clamped to 1..=10\nuser_input_max_options = 4 # default 4, clamped to 2..=10\n```\n\nThe effective values are applied in three places at once: the tool's JSON\nschema (`minItems` / `maxItems`), its model-visible description, and the\npayload validator. A rejected payload names the key to raise, so the model can\neither resize the batch or tell the user which setting to change.\n\n### User-input wait timeout\n\nQuestions from `request_user_input` wait until answered or canceled by default\n(#6003). An omitted setting or `0` leaves the wait unbounded; a positive value\ncancels the question when that many seconds pass, capped at 86,400 (24 hours).\nHeadless `exec` runs have no\nresponder, so `request_user_input` is withheld there by default:\nthe model reports the tool absent and finishes instead of stalling.\n\n```toml\n[tools]\nuser_input_timeout_seconds = 300 # opt into 5 minutes; omitted or 0 waits indefinitely; maximum 86400\n```\n\nThis key governs question waits only. Approvals have their own clock,\n`[approval] timeout_seconds`, which waits indefinitely unless you set it.\nWall-clock and stream protections elsewhere are unaffected.\n\n## Feature Flags\n\nFeature flags live under the `[features]` table and are merged across profiles.\nDefaults are enabled for built-in tooling, so you only need to set entries you\nwant to force on or off.\n\n```toml\n[features]\nshell_tool = true\nsubagents = true\nweb_search = true # enables deferred Web; the flag name is retained for config compatibility\napply_patch = true\nmcp = true\nexec_policy = true\ncode_mode = true # execute_tools composes MCP/plugin/native calls; false defers it behind tool_search\nverify_tool = true # agent-callable `verify` self-critique; false removes it from the tool catalog\nvision_model = false # true routes image analysis to the [vision_model] model (see above)\nextension_host = false # experimental: run reviewed plugins' native code (see EXTENSIONS.md)\n```\n\n`code_mode` is on by default: `execute_tools` is advertised from the first\nrequest and nested calls go through the same permission gate as direct calls\n(see [Tool surface](TOOL_SURFACE.md#code-mode-execute_tools)). Set\n`code_mode = false` to defer it behind `tool_search` again.\n\n`extension_host` is experimental and off by default. Turning it on lets reviewed\nplugins run their `native` TypeScript/JavaScript tool and slash-command code in\na separate Node (or, opt-in, Bun) process; toggling it in either direction\nchanges the plugin activation policy, so every plugin is reviewed again after a\nrestart. Its `[extension_host]` table (`runtime`, `node`, `bun`) and what the\nhost's sandbox does on each platform are in\n[Writing an extension tool](EXTENSIONS.md).\n\n`[extension_host] mcp_backend` (user config only) chooses the MCP client\nindependently of the `extension_host` flag: `\"host\"` (default) uses the official\nMCP SDK behind the Rust broker; `\"rust\"` keeps the previous Rust client for one\nrelease. A host refusal never falls back to Rust. See\n[MCP connection lifecycle](MCP.md#connection-lifecycle).\n\nEvery flag has a row in [`docs/features.toml`](features.toml), the feature\nregistry; a test fails when a flag and its row disagree.\n\nYou can also override features for a single run:\n\n- `codewhale --enable web_search`\n- `codewhale --disable subagents`\n\nUse `codewhale features list` to inspect known flags and their effective state.\nThe native `/config` view also includes a read-only **Experimental** section\nfor experimental feature flags. It shows each flag's effective enabled/disabled\nstate and whether that state comes from the default or a configured override.\nChange feature flags in `[features]` or with `--enable` / `--disable`; the\n`/config` section is an audit surface, not a stability promise. Goal and\nWorkflow preview rows may appear there as reserved entries until those workflows\ngraduate behind real gated flags.\n\n## Web Search Provider\n\n`web_search` uses keyless Firecrawl by default. Runtime failure or an exhausted\nkeyless quota degrades visibly through DuckDuckGo and Bing. China deployments\ncan explicitly select Baidu, Metaso, Volcengine, or a trusted SearXNG endpoint;\nCodewhale does not guess geography from locale or model provider.\n\nConfigured API providers are attempted first. Runtime failure or an empty\nresult visibly degrades through DuckDuckGo and then Bing; the structured search\nreceipt records every hop. Missing configuration and network-policy denials\nfail closed without sending the query to another provider.\n\n**Provider-native search.** On routes whose provider offers its own web-search\ntool (OpenAI, xAI, Anthropic, DeepSeek, Kimi and others), that search can run\nahead of the configured provider. It is a separate model call on the active\nroute. `[search] native` decides the order:\n\n- unset (default): native search leads only when no search provider is\n configured; a provider chosen in `[search] provider`,\n `CODEWHALE_SEARCH_PROVIDER`, or a Tavily key wins;\n- `native = true`: native search leads even when a provider is pinned;\n- `native = false`: native search is never used.\n\nNative citations are link-checked before they are returned. Each cited URL is\nrequested through the same guarded fetch as `Web` fetch (SSRF, DNS and redirect\nchecks, `[network]` policy): a `HEAD`, then a one-byte ranged `GET` when `HEAD`\nis refused, at most four at a time and five seconds each. The request uses the\naddress as it will appear in `results`, without userinfo, fragment or\ncredential-like query parameters; an address whose query string repeats the\nsearch query is not requested and is removed. Only sources that\nanswer with a 2xx status stay in `results`; the receipt lists each removed one\nas `citation_unverified` with its reason. When no citation passes, or when the\nquery asks for `recency` (no native search tool can enforce a time window), the\nsearch falls through to the configured provider. A citation whose host the\n`[network]` policy denies is removed. A citation whose host the policy would\nask you about is not fetched: it stays in `results` and the receipt records it\nas `citation_unchecked` until you allow the host.\n\nThe provider's prose answer is returned whole in a separate\n`unverified_synthesis` field: it is the provider model's summary, and neither\nits claims nor its links are checked. Oversized tool output spills to a session\nartifact the model can page back.\n\n**Recency and locale.** `recency` and `locale` are forwarded where the\nbackend's API takes them, and the search receipt reports each as honored or\nignored:\n\n| Backend | Recency | Locale |\n| --- | --- | --- |\n| Firecrawl | `tbs=qdr:d/w/m/y` | `country` from the region (`de-DE` → `DE`); a bare language is ignored |\n| Tavily | `time_range` | not sent (Tavily takes country names) |\n| SearXNG | `time_range` | `language`, as given |\n| Serply | not sent (undocumented) | `hl` language, `gl` country |\n| DeepSeek | not sent | not sent (`localization` takes an IP address) |\n| Provider-native | declined; the search falls through to the configured provider | not sent |\n\nRecency is rounded up to the backend's nearest window (day, week, month,\nyear), so `recency = 10` searches the last month. Other backends ignore both\nknobs and say so in the receipt.\n\nFor a private/internal search service that serves DuckDuckGo-compatible HTML,\nkeep `provider = \"duckduckgo\"` and set `base_url`; Codewhale appends the `q`\nquery parameter to that endpoint and applies network policy to its host.\nCustom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL`\ncan override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as\nthe legacy alias.\n\n**SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the\nconfigured instance's JSON API. Set `provider = \"searxng\"` and\n`base_url = \"https://your-searxng.example\"`; Codewhale calls\n`/search?q=...&format=json`. Codewhale does not use a public SearXNG instance\nby default because public instances often disable JSON output or rate-limit API\ntraffic.\n\nSelf-host it as a separate process (Docker is fine); Codewhale never bundles or\nmanages the search engine itself:\n\n- Enable JSON on the instance (`settings.yml`, `search.formats` must include\n `json`) and restart it. An HTML-only instance answers the API with HTTP 403;\n Codewhale reports that as a JSON/API-access problem on the SearXNG hop rather\n than silently returning no results.\n- Bind it to loopback, or to a host and port your network policy allows. The\n instance keeps its own engine list, limiter, and limits.\n- Point Codewhale at it with `[search] provider = \"searxng\"` and `base_url`\n (required; either the root URL or the `/search` endpoint). No instance ships\n as a default, and none is discovered automatically.\n- `codewhale doctor --probe-search` sends a transport-only `HEAD` to that\n origin — no `q=`, no credentials, no redirects, no audit receipt — so a green\n probe proves reachability and network-policy admission, not that JSON is on.\n\nConfirm the JSON API itself before assuming a Codewhale bug:\n\n```sh\ncurl -sS \"$BASE/search?q=codewhale&format=json\" | jq '.results[0] | {title,url,score}'\n```\n\nCodewhale ranks the returned rows by `score`, highest first, and applies\n`max_results` to that ranking; rows an instance reports without a usable score\nkeep their original relative order.\n\n**Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set\n`METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key.\n\n**Firecrawl** ([docs](https://docs.firecrawl.dev/sdks/cli)) searches Firecrawl\nCloud without a key using its bounded per-IP daily quota. Set\n`FIRECRAWL_API_KEY` or `[search] api_key` for authenticated limits. Codewhale\nsends no `Authorization` header in keyless mode.\n\n**Baidu** uses Baidu AI Search at\n`https://qianfan.baidubce.com/v2/ai_search/web_search`. Set\n`BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend\nonly; it does not add a Baidu model provider.\n\n**Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content\nrather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the\n`SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a\nSofya model provider.\n\n**Serply** ([serply.io](https://serply.io)) returns Google organic results with\ntitle, URL, and snippet. Set `[search] api_key` to your Serply key, or the\n`SERPLY_API_KEY` env var. This is a search-tool backend only; it does not add a\nSerply model provider.\n\n**DeepSeek** ([docs](https://api-docs.deepseek.com/guides/search)) uses\n`POST https://api.deepseek.com/v1/tools/search`. Set `provider = \"deepseek\"`\n(`deep-seek` and `deepseek-search` are also accepted) and supply `[search] api_key`\nor the `DEEPSEEK_API_KEY` env var, in that order. The saved DeepSeek model-provider\nkey is not reused by this search backend. Queries are truncated to 128 Unicode\ncharacters. The backend clamps counts to 1–50; the shared search tool currently\ncaps requests at 10 results. Query operators such as `site:`, `lang:`,\n`after:`, and `before:` are passed through; the `recency` and `locale` knobs are\nignored. Results include title, URL, and snippet, falling back to summary when a\nsnippet is empty.\n\n**Tavily** ([tavily.com](https://tavily.com)) is selected automatically when a\nTavily key is present and no provider is pinned: `TAVILY_API_KEY` set, or\n`[search] api_key` / `CODEWHALE_SEARCH_API_KEY` in the `tvly-` family. Doctor\nreports that as `source: tavily key`. Autodetect is runtime-only — Codewhale\nnever writes `[search] provider` for it, and `TAVILY_API_KEY` is never merged\ninto `[search] api_key`. An explicit `[search] provider` or\n`CODEWHALE_SEARCH_PROVIDER` always wins, so `provider = \"firecrawl\"` keeps\nFirecrawl even with a Tavily key in the environment. Pinned `tavily` accepts\nany non-empty `[search] api_key` and is configured by that key or\n`TAVILY_API_KEY`; with both empty it fails closed.\n\n```toml\n[search]\nprovider = \"firecrawl\" # also duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya | serply | deepseek\n# base_url = \"https://search.example/\" # optional with provider = \"duckduckgo\"; required with \"searxng\"\n# api_key = \"YOUR_KEY\" # optional for firecrawl; required by the other API providers\n# native = false # provider-native search: unset = only when no provider is configured\n```\n\n## Local Media Attachments\n\nUse `@path/to/file` in the composer to add local text file or directory context\nto the next message. Use `/attach ` for local image/video media paths, or\n`Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded\nX11/Wayland clipboard. SSH terminal paste without a forwarded graphical display\nis text-only; use the local terminal's paste command (`Cmd+V` on macOS or\n`Ctrl+Shift+V` on Linux/Windows), and use `/attach ` for remote image\nfiles. OpenSSH loopback X11 displays are detected automatically. For an\nexplicitly forwarded Wayland or non-loopback X11 display, set\n`CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal\ntransfer instead of an ambient remote display. DeepSeek's public Chat\nCompletions API currently accepts text message\ncontent, so media attachments are sent as explicit local path references instead\nof native image/video payloads.\nAttachment rows appear above the composer before submit; move to the start of\nthe composer, press `↑` to select an attachment row, then press `Backspace` or\n`Delete` to remove it without editing the sample text by hand.\n\n## Managed Configuration and Requirements\n\ncodewhale supports a policy layering model:\n\n1. user config + profile + env overrides\n2. managed config (if present)\n3. requirements validation (if present)\n\nBy default on Unix:\n- managed config: `/etc/deepseek/managed_config.toml`\n- requirements: `/etc/deepseek/requirements.toml`\n\nRequirements file shape:\n\n```toml\nallowed_approval_policies = [\"on-request\", \"untrusted\", \"never\"]\nallowed_sandbox_modes = [\"read-only\", \"workspace-write\"]\n```\n\nIf configured values violate requirements, startup fails with a descriptive error.\n\n## Notes On `codewhale doctor`\n\n`codewhale doctor` follows the same config resolution rules as the rest of the\nTUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy\n`DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills\nchecks use the resolved `mcp_config_path` / `skills_dir` (including env overrides).\n\nTo bootstrap missing MCP/skills paths, run `codewhale setup --all`. You can\nalso run `codewhale setup --skills --local` to create the owned project\n`.codewhale/skills` directory. Project skills load after workspace trust.\n\nBoth plain `codewhale doctor` and `codewhale doctor --json` are structural and\noffline by default. They do not check the release service, hosted provider\nAPIs, local provider endpoints, or MCP processes, and they do not load a\nworkspace credential `.env`. Use `--check-updates`,\n`--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding\nlive boundary; `--probe-local` may start a desktop-managed service such as\nOllama. Only the explicit API/local probe paths may load workspace credential\n`.env` values. Live flags conflict with `--json`, so machine-readable doctor\noutput is always offline. Top-level keys include `version`, `paths`, `secret_backend`,\n`config_path`, `config_present`, `workspace`, `api_key.source`,\n`api_key.availability`, `base_url`,\n`default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`,\n`platform`, `api_connectivity`, and `capability`. CI consumers should rely on\n`api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/\n`secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/\n`external_consent`/`none`/`local_runtime`/`unknown`) and\n`api_key.availability`\n(`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the\nhuman-readable `doctor` text. Source is declaration metadata, not proof that a\ncredential exists or works. Only a non-empty, non-sentinel literal config value\nis structurally `present`; no-auth and local routes are `not_required`. Environment,\nexternal-auth, OAuth, consent, and secret-store declarations remain `not_probed`\nand cannot make structural Setup or Fleet readiness true. A secret-store sentinel\non a named/custom endpoint that is prohibited from using the shared store is\n`secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for\nthe absence of a supported structural conclusion. Exact and whitespace-wrapped\nlegacy sentinels are never treated as literal credentials. The structural\nloader still honors safe environment routing/model/policy fields, but it never\nmaterializes environment HTTP headers, sandbox API keys, or search API keys;\nonly an explicit API/local probe switches to the normal credential-loading\npath. An opted-in update check also emits only typed generic failures: untrusted\nrelease metadata and transport errors are not echoed.\n\nIf configuration loading or validation fails, `doctor --json` returns nonzero\nand prints a bounded JSON error envelope with\n`status = \"error\"` and `error.kind = \"config_validation\"`. It does not emit a\nnormal route or capability report—or the underlying possibly sensitive error—\nfor an invalid configuration.\n\nMCP entries are configuration diagnostics unless an explicit MCP command is\nrun. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false,\nand each server separates `checks.configuration` / `checks.command` from\n`checks.process_reachable`, `checks.protocol_initialized`, and\n`checks.backend_tool_health`. The latter three remain `not_checked` in doctor\noutput. Run `codewhale mcp validate` to explicitly start enabled servers and\nverify protocol initialization/discovery; backend health still requires an\nappropriate explicit tool call. Doctor reports only safe structural MCP fields:\nURL userinfo/path/query/fragment and raw command arguments, environment values,\nheader values, and token material are omitted. Provider URLs follow the same\nrule and expose only `scheme://host[:explicit-port]`.\n\nThe `capability` key contains per-provider capability info derived from\nstatic knowledge (release docs, API guides) rather than live API probes.\nTop-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`,\n`max_output`, `thinking_supported`, `cache_telemetry_supported`,\nand `request_payload_mode`.\n\nUse `capability.context_window` and `capability.max_output` for model-limit\nchecks in CI scripts; do not treat `capability.max_output` as the per-turn\nrequest budget. Use `capability.thinking_supported` to decide whether to\nconfigure reasoning effort.\n\n## Setup status, clean, and extension dirs\n\n`codewhale setup` accepts a few flags beyond the existing `--mcp`,\n`--skills`, `--local`, `--all`, and `--force`:\n\n- `--status` — print a compact one-screen status (api key, base URL, model,\n MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and\n network-free; safe to run in CI. If `.env` is missing and `.env.example` is\n present in the workspace, the status output points at `cp .env.example .env`.\n- `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the\n self-describing frontmatter convention (`# name:` / `# description:` /\n `# usage:`) and an `example.sh` that follows it. The directory is\n intentionally not auto-loaded; wire individual scripts into the agent via\n MCP, hooks, or skills.\n- `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an\n `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered\n read-only, untrusted, and disabled; review them through `/plugin` before\n enabling. v0.9.1 activates only declared Skills and MCP servers. See\n [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md).\n- `--all` now scaffolds MCP + skills + tools + plugins together.\n- `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and\n `offline_queue.json` if they exist. Legacy\n `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set\n `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to\n actually remove matched files. This never touches real session history or the\n task queue.\n\n`--status` and `--clean` are mutually exclusive with the scaffold flags.\n\n## Why the engine strips XML/`[TOOL_CALL]` text\n\ncodewhale sends and receives tool calls only over the API tool channel\n(structured `tool_use` / `tool_call` items). The streaming loop in\n`crates/engine/src/core/engine.rs` recognizes a fixed set of fake-wrapper start\nmarkers — `[TOOL_CALL]`, `` — and scrubs them from visible assistant text without ever\nturning them into structured tool calls. When a wrapper is stripped, the loop\nemits one compact `status` notice per turn so the user can see why their\nvisible text shrank. Treat any change that re-enables text-based tool\nexecution as a regression; the protocol-recovery tests in\n`crates/tui/tests/integration/protocol_recovery.rs` lock the contract.\n\n## Model-bound redaction (`[redaction] model_bound`)\n\nCodewhale masks credential-looking values in tool output **before it is sent\nto an upstream model** — the \"model boundary\". A file read by a tool can\ncontain a configured API key, a bare provider token, or a credential-shaped\nopaque string, and the model must not see those bytes. This backstop is\nseparate from the display/export scrubbers: it decides what the model itself\ncan quote back, and it is deliberately conservative (`CredentialShaped`\npolicy, see `crates/config/src/persistence.rs`), so ordinary code and config\nstay byte-exact while keys, JWTs, bearer tokens, PEM blocks, and long opaque\nruns are masked.\n\nTurning that masking **off** is a security decision, so it is not a plain\nboolean:\n\n```toml\n[redaction]\nmodel_bound = \"disabled\" # \"enabled\" (default) | \"disabled\"\n```\n\nSetting `\"disabled\"` only records a *request*. It takes effect only when all\nof these are true:\n\n1. You restart the interactive TUI.\n2. The startup gate appears and you press `1`/`Y` on its first stage\n (\"confirm and disable\"). This only advances to a second, final-confirmation\n stage - the gate repeats the red warning and asks \"are you really sure?\".\n3. On that second stage you press `1`/`Y` again. The gate is rendered with the\n same explicit-key discipline as workspace trust - `Enter` never confirms by\n reflex, and `2`/`U` on the second stage steps back.\n4. Only that second confirmation persists a receipt to\n `~/.codewhale/redaction-state.json` (next to `config.toml`) and rebuilds\n the engine with masking off for the rest of this launch and future ones.\n\nThe receipt is bound to the config it was made against and is valid only\nwhile that config still requests `\"disabled\"`. Setting `model_bound` back\nto `\"enabled\"` - or rewriting `config.toml` in any way after the\nconfirmation - invalidates it, so requesting `\"disabled\"` again later\nasks for a fresh confirmation on the next launch. The receipt checks both the\nconfig contents and modification time; missing, unreadable, or malformed config\nand older receipts without this binding keep masking enabled. Legacy-home\ninstalls store the receipt beside their resolved config file.\n\nUntil a confirmation exists, the effective mode is always `\"enabled\"`:\n\n- Choosing `2`/`U` (\"keep masking on\") leaves the config field untouched, so\n the next launch asks again. Edit the field back to `\"enabled\"` to stop being\n asked.\n- Non-interactive entry points (`codewhale exec`, hooks, automations, headless\n agents) never confirm anything and never apply an unconfirmed request.\n- Routing/classification summaries and durable goal-state text keep their own\n always-on redaction regardless of this switch; the opt-out exists so the\n model can quote file bytes for exact edits, not to relax stored state.\n\nThe config value itself is forgiving: `true`/`false`, `\"on\"`/`\"off\"`, and\n`\"enabled\"`/`\"disabled\"` (any casing) all parse, with `false`/`\"off\"` meaning\n`\"disabled\"`.\n\nA confirmed opt-out still sends your configured API keys to the provider you\nare already talking to. Only use it when the model must read and edit files\nthat contain real credentials.\n\n### Stored sessions\n\nThe masking runs once, when tool output enters the transcript, so saved\nsession files (`~/.codewhale/sessions/*.json` and their checkpoints) and\nRuntime API thread items (their text and tool metadata) do not store a\ncredential-shaped value from tool output. With the confirmed opt-out above,\ntool output is stored as the model saw it.\n\nNot yet masked: large outputs spilled to `~/.codewhale/tool_outputs`, shell\ncompletion evidence artifacts, and tool-call *inputs* (for example a\n`curl -H \"Authorization: Bearer …\"` command line). `doctor` and\n`scrub-secrets` do not check those either.\n\nSessions saved by builds before this change may still hold credentials in\ntheir tool output; nothing rewrites them automatically. `codewhale doctor`\nreports them under **Stored Sessions** (it checks the newest 50 files), and\nthis command finds and masks them across every saved session:\n\n```sh\ncodewhale sessions scrub-secrets # report only\ncodewhale sessions scrub-secrets --apply # rewrite the affected files\n```\n\n`--apply` rewrites each file under the same per-session lock a save takes,\nso it never loses a concurrent save. A session that is still open can write\nits in-memory copy back on its next save, so close open sessions first, and\nrotate any credential that was exposed — masking a stored copy cannot un-leak it.\n" } ] }, @@ -267,13 +267,18 @@ "sources": [ { "path": "docs/PLUGINS.md", - "sha256": "66a354c06964da0c786e3e29d5b79fee8b090ad2fbb53170a75d62e241759867", - "content": "# Installing plugins\n\n> 阅读简体中文版:[zh_hans/PLUGINS.md](zh_hans/PLUGINS.md)。\n\nTo create a bundle, start with [Write your first Codewhale plugin](PLUGIN_AUTHORING.md)\nand its runnable Skills example.\n\nThis is the walkthrough for the `/plugin install` on-ramp (v0.9.4, #5182).\n[PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md) remains the contract for the bundle\nformat (`plugin.json`, compatible `kimi.plugin.json` or\n`.claude-plugin/plugin.json`, or legacy `plugin.toml`), discovery, validation, and\nthe trust/enable lifecycle — this document covers how bits get onto disk in\nthe first place.\n\n`/plugin suggest ` is a local, read-only companion: it ranks already\ninstalled bundles (name, keywords, description, bundled skill names, declared\nhosts) and any marketplace catalogs you have added with `/plugin marketplace add`.\nIt explains the match and gives the next review, enable, or catalog-install\nstep, but never installs, trusts, or enables a bundle on its own.\n\n## How Codewhale offers plugins\n\nCodewhale is helpful about plugins, not pushy. The rules:\n\n- **One proactive surface.** Sending a task can show one quiet toast when the\n prompt matches an installed-but-idle plugin or a catalog candidate you do\n not have yet, for example `/plugin trust supabase` or\n `/plugin marketplace install supabase`. Nothing appears while you\n type, and nothing is appended to your message to advertise plugins.\n- **One switch.** With `contextual_tips` off, no plugin guidance appears\n anywhere. Required notices and your own `/plugin` commands still work.\n- **One budget.** Plugin offers share the per-session guidance budget with\n other tips. In the interactive TUI the model can call\n `request_plugin_install` once per session to ask you to review a plugin the\n task needs; a second call fails. Exec, ACP, and runtime-API sessions do not\n get the tool.\n- **Built-ins are never advertised.** Bundled plugins such as Computer Use\n appear only in `/plugin list` and Extensions.\n- **Specific terms only.** Generic words (accessibility, browser, chrome,\n docs, screenshot, web, wiki, …) and Codewhale's own vocabulary (agent, code,\n data, MCP, model, session, tool) never trigger an offer, so asking why an\n MCP server is stuck does not suggest a vendor's plugin. Distinctive names,\n phrases and domains still do. The matcher and the marketplace's\n `check-marketplace.mjs` share one stoplist.\n- **Only what runs here.** Plugins whose `when.os` excludes this OS are not\n offered.\n- **Review before acting.** A model-requested row offers Review. Its button\n opens the existing Extensions inventory: Plugins for an installed bundle,\n Marketplace for a suggestion that is not installed. Nothing is installed,\n trusted, or enabled by that click. Choose the inventory's explicit action;\n trust still reviews the exact installed content before activation.\n- **Reversible dismissal.** Esc clears a non-empty draft first, then hides the\n row for this session only. \"Don't suggest again\" is the explicit,\n persisted choice. `/plugin dismissals` lists both kinds, and\n `/plugin dismissals reset []` lets suggestions offer a plugin again.\n- **Discovery is passive.** Find new plugins in these docs,\n `/plugin marketplace list`, Extensions, and the browser guide below.\n\nCodewhale does not invent a remote plugin URL; missing plugins are suggested\nonly from catalogs you added. On-disk bundle changes still toast\n`/plugin reload` on send and between turns.\n\n## Browser: pick one\n\nSeveral options can drive a browser. They differ in whose browser it is and\nwhat it can see.\n\n| Option | Whose browser | Good for |\n| --- | --- | --- |\n| `chrome-devtools` MCP (`/mcp recommendations`) | A Chrome it drives, which can include signed-in pages | DevTools-level inspection and performance work |\n| Playwright MCP (`/mcp recommendations`) | A fresh, isolated profile with `--isolated` | Scripted flows and testing without your identity |\n| Computer Use `browser_*` tools (bundled, off until reviewed) | One it launches, in a profile of its own | Browser steps inside a wider desktop task |\n| Chromewhale (developer preview, `codewhale-hq/codewhale-plugin-marketplace`) | Yours, already open, in your own Chrome profile; load unpacked | Reading or acting on the tab you are looking at, one granted site at a time |\n\nNone of these is offered to you proactively. Add the one that fits the job.\n\n## Sources\n\n`codewhale install ` and `/plugin install ` use the same installer.\nThe shell entry does not start a model or require model credentials. Both keep\nbundles disabled and untrusted until you explicitly review and enable them.\n\n```sh\ncodewhale install git:github.com/example/plugin@v1.0.0\ncodewhale install npm:@example/plugin@1.0.0\ncodewhale install ./local-plugin\n```\n\nGit shorthand supports GitHub archives only; refs must be a single safe segment.\nUse the existing HTTPS archive URL plus `#path=...` for a nested bundle.\nNpm requires an exact version and a native plugin manifest in the tarball.\nNo git hooks, npm lifecycle scripts, dependency installation or Pi executable\nextensions run. A ref names the requested revision, not an integrity signature;\nreview still binds the exact downloaded bytes. Tags can move: pin a commit for\nimmutable GitHub selection. Updates of version-pinned sources retain that version.\nNetwork approval and archive safety checks are unchanged.\n\nThe original source forms remain available:\n\n```text\n/plugin install ./path/to/bundle # local directory (copied)\n/plugin install github:owner/repo # GitHub archive of the default branch\n/plugin install https://example.com/x.tar.gz # direct tarball URL\n```\n\nThere is no registry index and no `git clone` in v1 — tarball-only fetching\nkeeps the size cap and no-symlink guarantees of the installer. Downloads are\ngated by the per-domain network policy: an unknown host returns a\n\"needs approval\" error naming the host (`/network allow `, then retry),\na denied host aborts without touching disk.\n\nThe fetched tree must contain **exactly one** bundle root — a directory\nholding a `plugin.json`, compatible `kimi.plugin.json`,\n`.claude-plugin/plugin.json`, or legacy `plugin.toml` manifest. Kimi bundles are accepted when they use Codewhale-compatible Skills,\ncommands, agents, and MCP declarations; unsupported Kimi runtime fields fail\nclosed instead of being silently ignored. Bundles land in\nthe user plugins root at `~/.codewhale/plugins//`, where `` is the\nmanifest's plugin name.\n\nClaude bundles keep their metadata in `.claude-plugin/plugin.json` and their\ncomponents at the bundle root. The importer supports skills, commands, agents,\nand MCP servers declared inline or in root `.mcp.json` (flat server map or an\n`mcpServers` wrapper). Claude `http` transport maps to Streamable HTTP. Relative\nsources in a `.claude-plugin/marketplace.json` catalog resolve from the marketplace\nrepository root. The whole bundle remains subject to the same review hashes and\npath checks as native plugins.\n\nRemote MCP headers can name credentials without embedding them: exact\n`Bearer ${ENV_NAME}` authorization values become `bearer_token_env_var`, and\nexact `${ENV_NAME}` header values become `env_headers`. Import reads no credential\nvalues. Literal credentials and compound templates are rejected.\n\nThis is a compatible subset: hooks, LSP declarations, custom MCP file paths, and\n`${CLAUDE_PLUGIN_ROOT}` expansion are rejected with an explanation; no partial\nplugin is installed. Installing a remote MCP declaration does not complete its\nauthentication. Plugin-contributed remote servers retain the existing explicit\ncredential requirements; this importer does not enable plugin OAuth.\n\n## The guided flow\n\nInstalling never activates anything. The command places the bits, then drops\nyou straight into the standard capability review:\n\n```text\n/plugin install github:someone/neat-plugin\n→ Installed plugin 'neat-plugin' to ~/.codewhale/plugins/neat-plugin.\n It is disabled and untrusted. Review its requested authority below…\n \n /plugin trust neat-plugin .\n\n/plugin trust neat-plugin # records the hash-bound receipt\n/plugin enable neat-plugin # activates for this workspace\n```\n\nThis is the same review render and confirmation token as `/plugin trust\n` — trust is the strict hash-bound receipt flow, not an advisory marker.\nIf the bundle's content or declared capabilities change, the receipt stops\nmatching and the plugin goes inactive until you review again.\n\n## Update and uninstall\n\n```text\n/plugin update # re-download, byte-compare, atomic swap if changed\n/plugin disable # required before uninstall\n/plugin uninstall # deletes the bundle and prunes its state entry\n```\n\n- `update` re-downloads the recorded source. Identical bytes are a no-op; a\n changed bundle is swapped atomically and its trust receipt is automatically\n invalidated (the hash no longer matches), so re-review is forced before the\n plugin can activate again. Plugins installed from a local path cannot be\n re-downloaded. To replace their installed copy, disable and uninstall it,\n then run `/plugin install ` and review the new bundle; the original\n source directory is left intact. See the [local authoring loop](PLUGIN_AUTHORING.md#4-iterate-and-review-changes).\n- `uninstall` refuses enabled plugins (disable first), deletes the bundle\n directory, and removes its persisted trust/enablement entry.\n\n## Safety rules\n\n- Every install carries an `.installed-from` provenance marker. The installer\n **refuses to overwrite or delete** a bundle that lacks it — hand-placed\n bundles under `~/.codewhale/plugins/` are never clobbered.\n- Tarballs are size-capped and extracted into a private staging directory\n first; path traversal (`..`, absolute paths) and symlinks/hard links inside\n the bundle are rejected, and the destination only appears via an atomic\n rename after every check passes.\n- Install pre-checks the name against builtin and workspace bundles so a\n higher-precedence bundle cannot silently shadow (or be shadowed by) the\n install.\n- Newly installed bits are always **disabled and untrusted**; enablement only\n ever happens through the explicit trust review above.\n" + "sha256": "2b69ca39b3ecb8f02a2ba3454c24dd7e9d49f4c26b3a36407ef76b6ab1e4cb0c", + "content": "# Installing plugins\n\n> 阅读简体中文版:[zh_hans/PLUGINS.md](zh_hans/PLUGINS.md)。\n\nTo create a bundle, start with [Write your first Codewhale plugin](PLUGIN_AUTHORING.md)\nand its runnable Skills example.\n\nThis is the walkthrough for the `/plugin install` on-ramp (v0.9.4, #5182).\n[PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md) remains the contract for the bundle\nformat (`plugin.json`, compatible `kimi.plugin.json` or\n`.claude-plugin/plugin.json`, or legacy `plugin.toml`), discovery, validation, and\nthe trust/enable lifecycle — this document covers how bits get onto disk in\nthe first place.\n\n## Choose a plugin, skill, or MCP connection\n\n| What you need | Use | Starting point |\n| --- | --- | --- |\n| Reusable instructions that work through your existing tools | A skill | `/skills`, then `/skill `; [Skills](SKILLS.md) |\n| A reviewed bundle of workflows, tools, commands or agent profiles | A plugin | `/plugin marketplace list`; [first-party catalog](https://github.com/codewhale-hq/codewhale-plugin-marketplace) |\n| Tools or context from a particular local process or remote service | A direct MCP connection | `/mcp`; [MCP setup](MCP.md) |\n\nA plugin can combine skills, Markdown commands, Fleet agent profiles, hooks,\nMCP servers and AI-provider declarations. Tools come from an MCP server or an\nexperimental native extension; there is no generic application-UI component in\nthe bundle manifest. Computer Use's Mac helper and Chromewhale's browser\nextension have their own setup after plugin review.\n\nMCP has two connection kinds: a local process over stdio, or a remote URL over\nStreamable HTTP. Legacy SSE is available when the server still supports it.\nUser-configured remote servers can use environment-backed credentials or the\nhost's OAuth login. Plugin-contributed remote servers require explicit\nenvironment-backed credentials when authentication is needed; their interactive\nOAuth path is not supported. Installing a skill or plugin does not connect an\naccount or establish that a service works.\n\nA local stdio server runs with your operating-system user's access. Requested\nfilesystem roots in a plugin manifest do not sandbox that process.\n\nFor example, configure [Cloudflare's public documentation server](https://developers.cloudflare.com/changelog/post/2026-10-22-cloudflare-mcp-servers-retire-sse/)\ndirectly, or install the `cloudflare-docs` plugin when you also want its skill:\n\n```sh\ncodewhale mcp add cloudflare-docs --url https://docs.mcp.cloudflare.com/mcp\ncodewhale mcp connect cloudflare-docs\ncodewhale mcp tools cloudflare-docs\n```\n\nChoose one setup for that server. `connect` is a diagnostic connection; it does\nnot attach tools to an already running session. In an existing terminal session,\nrun `/mcp reload` after changing configuration.\n\n## Bundle compatibility\n\n| Format | Supported boundary |\n| --- | --- |\n| Native `plugin.json` or legacy `plugin.toml` | Reviewed declarative adapters above; [manifest contract](PLUGIN_BUNDLES.md) |\n| Claude `.claude-plugin/plugin.json` | Skills, commands, agent profiles and MCP subset; hooks, LSP, custom MCP paths and plugin-root expansion are rejected; [compatibility details](CLAUDE_PLUGIN_COMPAT.md) |\n| `kimi.plugin.json` | Compatible skills, commands, agents and MCP declarations; unsupported runtime fields fail closed |\n| Local DSH package | Explicit preview and exact-hash import through `/plugin import dsh`; experimental native runtime, not blanket DSH compatibility or release qualification; [current evidence and limits](DSH_PLUGIN_COMPAT.md) |\n\nNative TypeScript extensions require `extension_host = true` under `[features]`\nand fresh capability review. LSP, requested filesystem roots and lifecycle mutation\nremain inventory-only. Current DSH adapters include work beyond the published\nv0.10.2 CLI; third-party DSH plugins' complete workflows are not established by\nan import succeeding. DSH plugin UI is unsupported. Other applications' plugin\ndirectories and credentials are not borrowed.\n\n`/plugin suggest ` is a local, read-only companion: it ranks already\ninstalled bundles (name, keywords, description, bundled skill names, declared\nhosts) and available marketplace catalogs, including the bundled snapshot and\ncatalogs you have added with `/plugin marketplace add`.\nIt explains the match and gives the next review, enable, or catalog-install\nstep, but never installs, trusts, or enables a bundle on its own.\n\n## How Codewhale offers plugins\n\nCodewhale is helpful about plugins, not pushy. The rules:\n\n- **One proactive surface.** Sending a task can show one quiet toast when the\n prompt matches an installed-but-idle plugin or a catalog candidate you do\n not have yet, for example `/plugin trust supabase` or\n `/plugin marketplace install supabase`. Nothing appears while you\n type, and nothing is appended to your message to advertise plugins.\n- **One switch.** With `contextual_tips` off, no plugin guidance appears\n anywhere. Required notices and your own `/plugin` commands still work.\n- **One budget.** Plugin offers share the per-session guidance budget with\n other tips. In the interactive TUI the model can call\n `request_plugin_install` once per session to ask you to review a plugin the\n task needs; a second call fails. Exec, ACP, and runtime-API sessions do not\n get the tool.\n- **Built-ins are never advertised.** Inspect existing bundles in `/plugin list`\n and Extensions before installing another copy. The published v0.10.2 CLI\n includes Computer Use; its layout differs from the current source candidate.\n See [Computer Use across versions](PLUGIN_MARKETPLACE.md#computer-use-across-versions).\n- **Specific terms only.** Generic words (accessibility, browser, chrome,\n docs, screenshot, web, wiki, …) and Codewhale's own vocabulary (agent, code,\n data, MCP, model, session, tool) never trigger an offer, so asking why an\n MCP server is stuck does not suggest a vendor's plugin. Distinctive names,\n phrases and domains still do. The matcher and the marketplace's\n `check-marketplace.mjs` share one stoplist.\n- **Only what runs here.** Plugins whose `when.os` excludes this OS are not\n offered.\n- **Review before acting.** A model-requested row offers Review. Its button\n opens the existing Extensions inventory: Plugins for an installed bundle,\n Marketplace for a suggestion that is not installed. Nothing is installed,\n trusted, or enabled by that click. Choose the inventory's explicit action;\n trust still reviews the exact installed content before activation.\n- **Reversible dismissal.** Esc clears a non-empty draft first, then hides the\n row for this session only. \"Don't suggest again\" is the explicit,\n persisted choice. `/plugin dismissals` lists both kinds, and\n `/plugin dismissals reset []` lets suggestions offer a plugin again.\n- **Discovery is passive.** Find new plugins in these docs,\n `/plugin marketplace list`, Extensions, and the browser guide below.\n\nCodewhale does not invent a remote plugin URL; missing plugins are suggested\nonly from available catalogs. On-disk bundle changes still toast\n`/plugin reload` on send and between turns.\n\n## Browser: pick one\n\nSeveral options can drive a browser. They differ in whose browser it is and\nwhat it can see.\n\n| Option | Whose browser | Good for |\n| --- | --- | --- |\n| `chrome-devtools` MCP (`/mcp recommendations`) | A Chrome it drives, which can include signed-in pages | DevTools-level inspection and performance work |\n| Playwright MCP (`/mcp recommendations`) | A fresh, isolated profile with `--isolated` | Scripted flows and testing without your identity |\n| Computer Use browser tools (off until reviewed) | One it launches, in a profile of its own | Browser steps inside a wider desktop task |\n| Chromewhale (developer preview, `codewhale-hq/codewhale-plugin-marketplace`) | Yours, already open, in your own Chrome profile; load unpacked | Reading or acting on the tab you are looking at, one granted site at a time |\n\nNone of these is offered to you proactively. Add the one that fits the job.\n\n## Sources\n\n`codewhale install ` and `/plugin install ` use the same installer.\nThe shell entry does not start a model or require model credentials. Both keep\nbundles disabled and untrusted until you explicitly review and enable them.\n\n```sh\ncodewhale install git:github.com/example/plugin@v1.0.0\ncodewhale install npm:@example/plugin@1.0.0\ncodewhale install ./local-plugin\n```\n\nGit shorthand supports GitHub archives only; refs must be a single safe segment.\nUse the existing HTTPS archive URL plus `#path=...` for a nested bundle.\nNpm requires an exact version and a native plugin manifest in the tarball.\nNo git hooks, npm lifecycle scripts, dependency installation or Pi executable\nextensions run. A ref names the requested revision, not an integrity signature;\nreview still binds the exact downloaded bytes. Tags can move: pin a commit for\nimmutable GitHub selection. Updates of version-pinned sources retain that version.\nNetwork approval and archive safety checks are unchanged.\n\nThe original source forms remain available:\n\n```text\n/plugin install ./path/to/bundle # local directory (copied)\n/plugin install github:owner/repo # GitHub archive of the default branch\n/plugin install https://example.com/x.tar.gz # direct tarball URL\n```\n\nThere is no registry index and no `git clone` in v1 — tarball-only fetching\nkeeps the size cap and no-symlink guarantees of the installer. Downloads are\ngated by the per-domain network policy: an unknown host returns a\n\"needs approval\" error naming the host (`/network allow `, then retry),\na denied host aborts without touching disk.\n\nThe fetched tree must contain **exactly one** bundle root — a directory\nholding a `plugin.json`, compatible `kimi.plugin.json`,\n`.claude-plugin/plugin.json`, or legacy `plugin.toml` manifest. Kimi bundles are accepted when they use Codewhale-compatible Skills,\ncommands, agents, and MCP declarations; unsupported Kimi runtime fields fail\nclosed instead of being silently ignored. Bundles land in\nthe user plugins root at `~/.codewhale/plugins//`, where `` is the\nmanifest's plugin name.\n\nClaude bundles keep their metadata in `.claude-plugin/plugin.json` and their\ncomponents at the bundle root. The importer supports skills, commands, agents,\nand MCP servers declared inline or in root `.mcp.json` (flat server map or an\n`mcpServers` wrapper). Claude `http` transport maps to Streamable HTTP. Relative\nsources in a `.claude-plugin/marketplace.json` catalog resolve from the marketplace\nrepository root. The whole bundle remains subject to the same review hashes and\npath checks as native plugins.\n\nRemote MCP headers can name credentials without embedding them: exact\n`Bearer ${ENV_NAME}` authorization values become `bearer_token_env_var`, and\nexact `${ENV_NAME}` header values become `env_headers`. Import reads no credential\nvalues. Literal credentials and compound templates are rejected.\n\nThis is a compatible subset: hooks, LSP declarations, custom MCP file paths, and\n`${CLAUDE_PLUGIN_ROOT}` expansion are rejected with an explanation; no partial\nplugin is installed. Installing a remote MCP declaration does not complete its\nauthentication. Plugin-contributed remote servers retain the existing explicit\ncredential requirements; this importer does not enable plugin OAuth.\n\n## The guided flow\n\nInstalling never activates anything. The command places the bits, then drops\nyou straight into the standard capability review:\n\n```text\n/plugin install github:someone/neat-plugin\n→ Installed plugin 'neat-plugin' to ~/.codewhale/plugins/neat-plugin.\n It is disabled and untrusted. Review its requested authority below…\n \n /plugin trust neat-plugin .\n\n/plugin trust neat-plugin # records the hash-bound receipt\n/plugin enable neat-plugin # activates for this workspace\n```\n\nThis is the same review render and confirmation token as `/plugin trust\n` — trust is the strict hash-bound receipt flow, not an advisory marker.\nIf the bundle's content or declared capabilities change, the receipt stops\nmatching and the plugin goes inactive until you review again.\n\n## Update and uninstall\n\n```text\n/plugin update # re-download, byte-compare, atomic swap if changed\n/plugin disable # required before uninstall\n/plugin uninstall # deletes the bundle and prunes its state entry\n```\n\n- `update` re-downloads the recorded source. Identical bytes are a no-op; a\n changed bundle is swapped atomically and its trust receipt is automatically\n invalidated (the hash no longer matches), so re-review is forced before the\n plugin can activate again. For a local install, it stages a fresh copy of the\n recorded source directory; keep that authoring directory available. Editing\n the installed copy does not update its source. See the\n [local authoring loop](PLUGIN_AUTHORING.md#4-iterate-and-review-changes).\n- `update` retains the recorded repository revision, npm version, archive URL\n and bundle selector. Refreshing a catalog does not retarget that source. To\n select a newer catalog pin, follow\n [Changing an installed pin](PLUGIN_MARKETPLACE.md#changing-an-installed-pin).\n- `uninstall` refuses enabled plugins (disable first), deletes the bundle\n directory, and removes its persisted trust/enablement entry.\n\n## Safety rules\n\n- Every install carries an `.installed-from` provenance marker. The installer\n **refuses to overwrite or delete** a bundle that lacks it — hand-placed\n bundles under `~/.codewhale/plugins/` are never clobbered.\n- Tarballs are size-capped and extracted into a private staging directory\n first; path traversal (`..`, absolute paths) and symlinks/hard links inside\n the bundle are rejected, and the destination only appears via an atomic\n rename after every check passes.\n- Install pre-checks the name against builtin and workspace bundles so a\n higher-precedence bundle cannot silently shadow (or be shadowed by) the\n install.\n- Newly installed bits are always **disabled and untrusted**; enablement only\n ever happens through the explicit trust review above.\n" }, { "path": "docs/PLUGIN_BUNDLES.md", - "sha256": "47437bf9e128db39302605ea2c6e4737b12c22caccf35bf8b5e974d8b1c3fdbf", - "content": "# Plugin bundles\n\n> 阅读简体中文版:[zh_hans/PLUGIN_BUNDLES.md](zh_hans/PLUGIN_BUNDLES.md)。\n\nCodewhale supports a deliberately small plugin-bundle boundary. The boundary\nwas drawn in v0.9.1 and is extended deliberately in v0.9.10: a bundle may\ncontribute declarative Skills, MCP configuration, Commands, Agent profiles,\nand Hooks through Codewhale's existing engines. Unsupported declarations stay\ninventoried instead of disabling a mixed bundle. Discovery alone never\nexecutes, enables, trusts, downloads, updates, or installs anything.\n\nThis document owns the bundle formats, discovery,\nvalidation, and the trust/enable/runtime contract. [PLUGINS.md](PLUGINS.md)\nowns how bits get onto and off disk — the `/plugin install`, `update`,\n`uninstall`, and `suggest` on-ramp added in v0.9.4 (#5182). Compatible Claude Code bundles use the same native adapters; their supported subset is\n[CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md).\nFor a runnable native example and explicit OpenCode/DSH data conversion, see\n[Write your first plugin](PLUGIN_AUTHORING.md).\n\n## Discovery and precedence\n\nCodewhale scans only its own roots, looking in each `/` directory for a\nmanifest named `plugin.json` (the native Agent Plugins v1.0.0 format, since\nv0.9.4), `kimi.plugin.json` (the compatible Kimi Skills/MCP subset, since\nv0.9.8), `plugin.toml` (the legacy Codewhale format, still fully readable), or\n`.claude-plugin/plugin.json` (the compatible Claude subset, since v0.9.13):\n\n- User: `~/.codewhale/plugins//`\n- Workspace: `/.codewhale/plugins//`\n\nA bundle that publishes multiple formats is read through `plugin.json` first,\nthen `kimi.plugin.json`, then the legacy `plugin.toml`, then\n`.claude-plugin/plugin.json`.\nComputer Use installs from the first-party catalog as a normal user plugin.\nIt updates independently of the Engine and requires review and enablement.\nThe native Mac helper is installed separately for permission setup and the\ncomputer-control indicator. See the [Computer Use guide](https://github.com/codewhale-hq/codewhale-cu-plugin).\n\nUser plugins take precedence over workspace plugins. Symbolic-link roots,\nmanifests, component paths, and nested component files fail closed.\nHistorical built-in snapshots and receipts remain untouched for older binaries;\nthe current Engine neither creates nor discovers a built-in Computer Use copy.\nInstall and review the catalog plugin once when upgrading from that layout.\n\nNew user and workspace bundles are always untrusted and disabled. Discovery is\nread-only and does not inspect any other application's extension or credential\ndirectories: ambient roots such as `.claude/plugins` or `.cursor/plugins` are\nnever scanned.\n\nPre-v0.9.1 `overrides.json` enablement was intentionally not imported as\ntrust; every bundle activates only through the content-hash and\n`codewhale-plugin-capabilities-v3` activation-policy review below.\n\n## Manifest\n\nAll supported encodings parse into the same internal manifest, so validation, hashing,\nreview, and runtime behavior are identical downstream. On-disk auto-migration\nbetween them is deliberately not performed; `/plugin export \n` publishes a loaded bundle as a spec-valid Agent Plugins v1.0.0\ndirectory without modifying the installed one.\n\n### `plugin.json` (Agent Plugins v1.0.0)\n\nThe standard's manifest root is closed: `$schema`, `name`, and the optional\nwell-known fields (`version`, `description`, `author`, `homepage`,\n`repository`, `license`, `keywords`, `extensions`). An unknown root key is a\nparse error. Client-specific data lives under `extensions`, keyed by\nreverse-domain namespace: unknown vendor namespaces are ignored, never\nrejected — that is what lets a bundle authored for another client load here —\nwhile unknown keys inside Codewhale's own `extensions[\"net.codewhale\"]`\nnamespace are rejected rather than silently dropped.\n\nNames follow the standard's rule: 1–64 lowercase ASCII letters, digits, or\ninternal single `-`/`.`, starting and ending alphanumeric, never `--` or `..`.\nA `skills/` directory in the bundle root is picked up automatically; other\ncomponent locations, `capabilities`, `when`, and `display_name` ride in\n`extensions[\"net.codewhale\"]`.\n\nMCP servers cannot live in `plugin.json` (the root is closed); they live in a\nsibling `mcp.json` under `mcpServers`, with `stdio`, `streamable-http`, or\n`sse` transports (`type` may be omitted and is inferred from `command` vs\n`url`). Codewhale-only server options — timeouts, tool filters, env-backed\ncredentials, enablement — ride per-server under `extensions[\"net.codewhale\"]`.\nThe `env` names `PLUGIN_ROOT` and `PLUGIN_DATA` are reserved by the standard\nfor the host runtime and are rejected in plugin definitions.\n\n### `plugin.toml` (legacy Codewhale format)\n\n```toml\nschema_version = 1\n\n[plugin]\nname = \"example\"\nversion = \"0.1.0\"\ndescription = \"Example instruction and MCP bundle\"\nauthor = \"Example Author\"\n\n[skills]\npath = \"skills\"\n\n[commands]\npath = \"commands\"\n\n[agents]\npath = \"agents\"\n\n[hooks]\npath = \"hooks\"\n\n[mcp_servers.local]\ncommand = \"node\"\nargs = [\"server.js\"]\ncwd = \"mcp\"\n\n[mcp_servers.remote]\nurl = \"https://example.invalid/mcp\"\n\n[capabilities]\nnetwork_hosts = [\"example.invalid\"]\n\n[when]\nos = [\"macos\", \"linux\", \"windows\"]\nbinaries = [\"node\"]\n```\n\nLegacy TOML names are 1–64 lowercase ASCII letters, digits, or internal\nhyphens. A pre-versioned manifest without `schema_version` still parses, with\na migration warning from `/plugin validate` (and `0.0.0` displayed when\n`[plugin].version` is missing). An unknown top-level table or field is a\nparse error (`deny_unknown_fields`), reported by byte offset without echoing\nmanifest values.\n\n### Validation (both formats)\n\nComponent paths must be relative, contained, present, and free of symbolic\nlinks or Windows reparse points (including junctions and mount points). The v1\nschema rejects unknown MCP fields, ambiguous local/remote\ntransport combinations, unbounded lists/timeouts, and overlapping tool\nfilters.\n\nRemote MCP URLs must use HTTPS, except for explicit loopback HTTP endpoints.\nThey cannot contain user information, a query, or a fragment. Literal headers\nare rejected: authentication must name a source environment variable through\n`env_headers` or `bearer_token_env_var`. A remote bundle must declare exactly\nthe normalized host set used by its endpoints in\n`capabilities.network_hosts`; endpoint scheme, normalized host, port, and path\nremain bound to the review. Redirects are limited and must retain that exact\nnormalized origin. Reviewed remote transports use an explicit no-proxy HTTP\nclient: plugin bundles never read or use ambient `HTTP_PROXY`, `HTTPS_PROXY`,\nor `NO_PROXY` values, because proxy credentials and proxy observation are\noutside the reviewed authority. User-authored MCP configuration keeps its\nexisting explicit proxy support.\n\nLocal stdio environment entries must use exact `${SOURCE_ENV}` references.\nThe review shows destination and source names, but never reads or prints their\nvalues. Plugin children inherit only Codewhale's base secret-scrubbed child\nenvironment plus those reviewed mappings; credential-capable proxy variables\nand the broader compatibility environment used by user-authored MCP\nconfiguration are not inherited ambiently. Absolute arguments and parent\ntraversal are rejected; contained bundle entrypoints are frozen to their\nstaged paths before spawn.\n\nEvery stdio argument is shown losslessly as a JSON string during review.\nCommon credential-bearing flags and known literal token shapes are rejected\nfrom argv; credentials must instead use a reviewed environment mapping.\nPlugin-contributed MCP OAuth has been disabled since v0.9.1 and remains\ndisabled as of v0.9.6, including discovery, login, refresh, and token storage;\na manifest declaring OAuth fields on a plugin MCP server fails validation.\n\n### Active and inactive component surfaces\n\nCodewhale 0.9.10 activates declarative `[skills]`, `[mcp_servers.*]`,\n`[commands]`, `[agents]`, and `[hooks]` components from its content-addressed\nruntime snapshot. Commands use markdown command files, Agents use Fleet TOML\nprofiles, and Hooks use `HooksConfig` TOML files. A component may name one file\nor a directory of the corresponding files. Ordinary user/workspace commands\nand Agent profiles keep precedence over plugin contributions; trusted project\nhooks run after plugin hooks.\n\nThe manifest can additionally inventory the following inactive surfaces.\nThose declarations stay hashed, reviewed, and displayed, but do not activate\nand no longer disable the whole bundle:\n\n```toml\n[lsp] # TOML alias: [lsp_servers]\npath = \"lsp\"\n\n[native] # TOML alias: [native_extension]\npath = \"native/index.mjs\"\n\n[capabilities]\nfilesystem_roots = [\"workspace\"]\nnetwork_hosts = [\"api.example.invalid\"]\nlifecycle_mutation = true\n```\n\n(In a `plugin.json` bundle the same tables ride under\n`extensions[\"net.codewhale\"]`.)\n\nThe accept/reject behavior is deliberately loud, never silent:\n\n- Compatibility is per-component: `full` when every declared surface has an\n adapter (or the bundle is empty), `partial` when supported components can\n activate beside named inactive surfaces, and `unsupported` when the bundle\n only declares surfaces Codewhale cannot activate yet. The same versioned\n activation policy (v3) drives those labels, the runtime adapters, and the\n capability hash. Executing LSP or native code must change that policy,\n which changes the capability hash and forces re-review. v1 and v2 trust\n receipts fail closed as `capabilities-changed`.\n- **`native` under the experimental extension host.** With\n `[features] extension_host` on, the policy becomes v4 and `native` is an\n active adapter: each entry is one `.mjs`, `.js` or `.mts` ES module file that the\n TypeScript extension host imports. A directory or any other file reports\n an error in `/plugin validate` and review and prevents activation. Its tools\n always use `Required` approval;\n Full Access, Bypass, or an exact session grant for the reviewed build can\n satisfy that gate without a prompt. Toggling the flag\n re-reviews every plugin. See\n [the design](design/TS_EXTENSION_HOST.md#as-built-phase-1-2026-09-25).\n The [extension author guide](EXTENSIONS.md) includes a tested typed example,\n diagnostic workflow and Node's erasable-TypeScript restrictions.\n- A **recognized-but-inactive** declaration (`lsp`, `native`, a non-empty\n `capabilities.filesystem_roots`, or\n `capabilities.lifecycle_mutation = true`) parses and is validated like any\n component (contained, present, link-free). It is counted in the inventory,\n hashed into the capability receipt, shown in review and `/plugin show` as\n inactive, and never executed (for `native`, only while the extension host\n flag is off). A reviewed, trusted, applicable mixed bundle\n can still be enabled: supported declarative components become active, and\n the inactive surfaces stay named as inactive.\n- An **all-unsupported** bundle can be reviewed and trusted, but `/plugin\n enable` fails closed and names the inactive surfaces. There is nothing\n Codewhale can honestly activate.\n- An **unrecognized** section or field is a validation failure, not an\n inventory entry: unknown top-level TOML tables, unknown MCP server fields,\n unknown `plugin.json` root keys, and unknown keys inside\n `extensions[\"net.codewhale\"]` are all rejected outright. The single\n ignore-without-error case is another vendor's `extensions` namespace in the\n Agent Plugins format, which the standard requires clients to skip.\n- `capabilities.network_hosts` is not a future surface: it is enforced today,\n and must exactly match the normalized host set of the bundle's remote MCP\n endpoints (so it cannot be declared without them, or omitted with them).\n\nA successful environment or health check is never treated as trust.\n\n## Review, trust, and enablement\n\nUse the in-session command surface:\n\n```text\n/plugin list\n/plugin validate example\n/plugin show example\n/plugin enable example\n```\n\nThe first `enable` opens a review showing source, component inventory,\nrequested permissions, sanitized MCP endpoints, full content and capability\nhashes, and inactive declarations. It also prints an exact confirmation:\n\n```text\n/plugin trust example .\n```\n\nRun that exact command only after reviewing the bundle. The confirmation token\nuses both complete SHA-256 receipts rather than display prefixes. The\ncapability receipt is the v3 digest: it still hashes the complete inventory\nand also binds this build's activation policy (which adapters are executable\nversus inventoried-only). Trust first\ncopies the complete reviewed tree into a Codewhale-owned, content-addressed\nruntime snapshot and records the matching receipt; it does not activate\nanything.\nThen run `/plugin enable example` again. Trust and enablement are separate:\n\n- `/plugin disable example` stops contribution while preserving trust.\n- `/plugin revoke example` removes trust while preserving the enablement bit;\n the bundle remains inactive until reviewed again.\n- `/plugin reload` rebuilds the current workspace registry when files have\n changed on disk.\n\n(`/plugin install`, `update`, and `uninstall` place, replace, and remove the\nbits themselves and always drop into this same review — see\n[PLUGINS.md](PLUGINS.md). `/plugin suggest` ranks installed bundles and\nany locally added marketplace catalogs; sending a matching task can toast the\nsame next step without installing anything. Nothing is written into the\nmodel's request to advertise plugins; the full offering policy is in\n[PLUGINS.md](PLUGINS.md#how-codewhale-offers-plugins).)\n\nTrust, enable, disable, revoke, and reload rebuild the current workspace's\nSkills, MCP, Commands, Agent profiles, and Hooks immediately. Each persisted\ntransition advances a per-bundle generation under a stable cross-process lock.\nA generation change cancels in-flight MCP work, removes cached catalog\nentries, terminates an idle plugin stdio child, and denies persisted queued\nSkills carrying the older authority receipt.\n\nThe review distinguishes remote MCP endpoints from local stdio MCP servers.\nA local stdio server is a child process running with the Codewhale user's host\nfilesystem and network authority; plugin trust is not an OS sandbox. The\nreview therefore shows the command, argument count, working directory,\nenvironment-variable names, and this host-authority warning without printing\nenvironment or header values. MCP tool approval still applies after the\nserver starts.\n\nTrust receipts live in `~/.codewhale/plugins/state.json`. Atomic owner-only\nwrites record the full content hash, capability hash, reviewed capability\ninventory, generation, and review time, with the latest 32 reviews retained as\na bounded audit trail. Malformed or unsupported state is not overwritten: all\nbundles fail closed until the state file is repaired or moved.\n\nThe content hash covers the manifest, complete bundle tree, and executable\nshape in deterministic path order, including local MCP entrypoints and\ncompanion assets. Staging is bounded, rejects symbolic links and unsupported\nfile kinds (plus every Windows reparse point and hard-linked files), uses an\natomic destination swap, and applies owner-only runtime permissions or ACLs\nthrough validated object handles on Windows. The capability hash covers the\nnormalized component and permission inventory. A source or staged-content\nedit, capability change, or unsafe runtime-root replacement invalidates the\nreceipt deterministically; an already-enabled bundle becomes inactive until\nit is reviewed again. This is the same invalidation `/plugin update` relies\non: replaced bytes stop matching the receipt, forcing re-review.\n\n## Runtime behavior\n\nAn active bundle must be enabled, trusted for its current hashes, applicable to\nthe host, and free of validation errors. A reviewed mixed bundle may be\nactive, but only supported components in the reviewed v3 activation mask are\nconsumable. Unsupported components remain listed, hashed, reviewed, and\ninactive.\n\n- Skills are exposed only as `:`. The model-facing catalogue and\n `load_skill` use an in-memory snapshot bound to the reviewed staged tree,\n rather than reading a mutable source path at execution time. `load_skill`\n revalidates source, stage, receipt, workspace, and generation immediately\n before releasing content and fails closed on drift. Queued messages persist\n the same provenance and repeat that check at dispatch. `/skills inspect`\n identifies the reviewed bundle without exposing its mutable source path.\n- MCP server names are exposed as\n `plugin---` so hyphens in either\n component cannot create an authority collision. Disabled or untrusted\n bundles are denied again at the headless MCP adapter. Authority is checked\n before connection, immediately before every lazy stdio spawn, after\n transport construction, and before each tool/resource/prompt operation.\n Persisted generation/enablement/trust state is also watched while an\n operation is in flight, so disable, revoke, or another cross-process state\n transition cancels the operation and terminates a plugin stdio child. Full\n source and staged-tree hashes are revalidated at dispatch/catalogue\n boundaries; the runtime does not continuously re-hash those trees during an\n already-running MCP call. Source or stage drift therefore fails the next\n boundary and drops the stale connection/catalogue entry, but is not claimed\n to interrupt a call already executing. Every failure includes instructions\n to reload, review, trust, and enable the bundle again.\n- Commands load after ordinary user/workspace commands and saved workflows, so\n existing definitions keep precedence and collisions are visible. The\n palette hides a revoked command immediately; dispatch rechecks the full\n receipt before expanding its body and reports a visible denial on stale\n input.\n- Agent profiles join the Fleet roster below explicit config, personal, and\n workspace profiles but above built-ins. Roster collisions retain the\n existing visible shadow record. Every Agent spawn rebuilds from the current\n registry and rechecks the selected plugin profile's authority before its\n prompt or route can be used.\n- Hooks merge after global hooks and before trusted project hooks. Foreground\n Hooks recheck authority immediately before process spawn; background Hooks\n check before enqueue and again at dequeue so a queued, revoked Hook cannot\n start later.\n- Plain launch, resume, fork, exec, and serve each construct an immutable\n workspace-scoped registry before constructing their plugin-backed catalogues.\n- Constitution, repository instructions, permission rules, sandbox policy,\n and MCP tool approval continue to outrank plugin instructions.\n\n`/plugin list`, `show`, `suggest`, and `validate` perform no network requests,\nprocess launches, credential reads, or configuration writes. Reviews render\nstructural argv as lossless JSON strings and environment provenance without\nvalues. Credential-bearing argv is rejected at manifest validation;\nplugin-originated errors suppress URL query, authentication, argv, and\nenvironment material. Legacy executable tools under `[tools].plugin_dir`\nremain a distinct system and are listed under `/plugin tools`; they cannot\napprove themselves or replace built-in tools (see\n[CONFIGURATION.md](CONFIGURATION.md#script-tools-and-overrides)).\n\n## Explicit non-goals as of v0.9.10\n\nFederated marketplace catalogs (`/plugin marketplace add|list|show|remove|install`)\nparse local Kimi-, Claude-, Codex-, and Codewhale-format catalog documents; see\nthe marketplace section below (`/plugin install` fetches\none reviewed source, and `/plugin suggest` ranks only what is already\ninstalled), no ambient compatibility discovery, no automatic trust, no\nplugin-contributed MCP OAuth, no LSP adapter or MCP subscription adapter, no\nnative extension runtime outside the experimental `extension_host` flag, no\nforeign executable plugin runtime import, and no on-disk auto-migration of a\nlegacy `plugin.toml` to `plugin.json`. The explicit offline\n[OpenCode/DSH converter](PLUGIN_AUTHORING.md#convert-an-existing-plugin) supports\nselected portable Skills, static Streamable HTTP MCP declarations, and\nexplicitly packaged Node `.mjs`, `.js`, or `.cjs` MCP servers selected with `--stdio-root`.\nLocal source and dependencies are copied for the same native installation,\ncapability review, hash-bound trust and enable flow; conversion executes no\ncode or package manager. It does not migrate arbitrary bundles or reproduce\nanother client's runtime or policy.\nThe other capabilities above remain later work rather than implied support.\n\n## Marketplace catalogs (#5311)\n\n`/plugin marketplace` reads LOCAL catalog documents in the real published\nschemas (Kimi, Claude, Codex, Codewhale native; Codex via its policy markers)\nand renders every candidate with an honest install plan:\n\n```text\n/plugin marketplace add # parse a local catalog file (no network)\n/plugin marketplace list # catalogs + candidates + diagnostics\n/plugin marketplace show # one catalog in detail\n/plugin marketplace remove # forget a catalog (plugins unaffected)\n/plugin marketplace install \n```\n\n- `add` never fetches anything: it reads one local JSON file (≤4 MiB, regular\n files only, symlinks refused) and stores the parsed catalog next to the\n plugin state file.\n- Catalog tiers and provenance (`official`, `curated`, …) are **display\n only** — they never grant trust, enablement, or installation.\n- Foreign policies are visibly ignored: a Codex `INSTALLED_BY_DEFAULT` entry\n is listed with a `NO_AUTO_INSTALL` warning and nothing is installed until\n an operator runs the install verb.\n- Sources Codewhale cannot fetch (npm packages, `command:` sources, non-tarball\n URLs) are listed as `not installable` with the reason.\n- `install` routes through the same reviewed installer as `/plugin install`:\n the bundle lands disabled and untrusted, and enters the hash-bound trust\n review before anything activates.\n" + "sha256": "5595f972609b6fcc425b830eb13bfc75095107e77152149a1cd6f890751b205c", + "content": "# Plugin bundles\n\n> 阅读简体中文版:[zh_hans/PLUGIN_BUNDLES.md](zh_hans/PLUGIN_BUNDLES.md)。\n\nCodewhale supports a deliberately small plugin-bundle boundary. The boundary\nwas drawn in v0.9.1 and is extended deliberately in v0.9.10: a bundle may\ncontribute declarative Skills, MCP configuration, Commands, Agent profiles,\nand Hooks through Codewhale's existing engines. Unsupported declarations stay\ninventoried instead of disabling a mixed bundle. Discovery alone never\nexecutes, enables, trusts, downloads, updates, or installs anything.\n\nThis document owns the bundle formats, discovery,\nvalidation, and the trust/enable/runtime contract. [PLUGINS.md](PLUGINS.md)\nowns how bits get onto and off disk — the `/plugin install`, `update`,\n`uninstall`, and `suggest` on-ramp added in v0.9.4 (#5182). Compatible Claude Code bundles use the same native adapters; their supported subset is\n[CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md).\nFor a runnable native example and explicit OpenCode/DSH data conversion, see\n[Write your first plugin](PLUGIN_AUTHORING.md).\n\n## Discovery and precedence\n\nCodewhale scans only its own roots, looking in each `/` directory for a\nmanifest named `plugin.json` (the native Agent Plugins v1.0.0 format, since\nv0.9.4), `kimi.plugin.json` (the compatible Kimi Skills/MCP subset, since\nv0.9.8), `plugin.toml` (the legacy Codewhale format, still fully readable), or\n`.claude-plugin/plugin.json` (the compatible Claude subset, since v0.9.13):\n\n- User: `~/.codewhale/plugins//`\n- Workspace: `/.codewhale/plugins//`\n\nA bundle that publishes multiple formats is read through `plugin.json` first,\nthen `kimi.plugin.json`, then the legacy `plugin.toml`, then\n`.claude-plugin/plugin.json`.\nComputer Use installs from the first-party catalog as a normal user plugin.\nIt updates independently of the Engine and requires review and enablement.\nThe native Mac helper is installed separately for permission setup and the\ncomputer-control indicator. See the [Computer Use guide](https://github.com/codewhale-hq/codewhale-cu-plugin).\n\nUser plugins take precedence over workspace plugins. Symbolic-link roots,\nmanifests, component paths, and nested component files fail closed.\nHistorical built-in snapshots and receipts remain untouched for older binaries;\nthe current Engine neither creates nor discovers a built-in Computer Use copy.\nInstall and review the catalog plugin once when upgrading from that layout.\n\nNew user and workspace bundles are always untrusted and disabled. Discovery is\nread-only and does not inspect any other application's extension or credential\ndirectories: ambient roots such as `.claude/plugins` or `.cursor/plugins` are\nnever scanned.\n\nPre-v0.9.1 `overrides.json` enablement was intentionally not imported as\ntrust; every bundle activates only through the content-hash and\n`codewhale-plugin-capabilities-v3` activation-policy review below.\n\n## Manifest\n\nAll supported encodings parse into the same internal manifest, so validation, hashing,\nreview, and runtime behavior are identical downstream. On-disk auto-migration\nbetween them is deliberately not performed; `/plugin export \n` publishes a loaded bundle as a spec-valid Agent Plugins v1.0.0\ndirectory without modifying the installed one.\n\n### `plugin.json` (Agent Plugins v1.0.0)\n\nThe standard's manifest root is closed: `$schema`, `name`, and the optional\nwell-known fields (`version`, `description`, `author`, `homepage`,\n`repository`, `license`, `keywords`, `extensions`). An unknown root key is a\nparse error. Client-specific data lives under `extensions`, keyed by\nreverse-domain namespace: unknown vendor namespaces are ignored, never\nrejected — that is what lets a bundle authored for another client load here —\nwhile unknown keys inside Codewhale's own `extensions[\"net.codewhale\"]`\nnamespace are rejected rather than silently dropped.\n\nNames follow the standard's rule: 1–64 lowercase ASCII letters, digits, or\ninternal single `-`/`.`, starting and ending alphanumeric, never `--` or `..`.\nA `skills/` directory in the bundle root is picked up automatically; other\ncomponent locations, `capabilities`, `when`, and `display_name` ride in\n`extensions[\"net.codewhale\"]`.\n\nMCP servers cannot live in `plugin.json` (the root is closed); they live in a\nsibling `mcp.json` under `mcpServers`, with `stdio`, `streamable-http`, or\n`sse` transports (`type` may be omitted and is inferred from `command` vs\n`url`). Codewhale-only server options — timeouts, tool filters, env-backed\ncredentials, enablement — ride per-server under `extensions[\"net.codewhale\"]`.\nThe `env` names `PLUGIN_ROOT` and `PLUGIN_DATA` are reserved by the standard\nfor the host runtime and are rejected in plugin definitions.\n\n### `plugin.toml` (legacy Codewhale format)\n\n```toml\nschema_version = 1\n\n[plugin]\nname = \"example\"\nversion = \"0.1.0\"\ndescription = \"Example instruction and MCP bundle\"\nauthor = \"Example Author\"\n\n[skills]\npath = \"skills\"\n\n[commands]\npath = \"commands\"\n\n[agents]\npath = \"agents\"\n\n[hooks]\npath = \"hooks\"\n\n[mcp_servers.local]\ncommand = \"node\"\nargs = [\"server.js\"]\ncwd = \"mcp\"\n\n[mcp_servers.remote]\nurl = \"https://example.invalid/mcp\"\n\n[capabilities]\nnetwork_hosts = [\"example.invalid\"]\n\n[when]\nos = [\"macos\", \"linux\", \"windows\"]\nbinaries = [\"node\"]\n```\n\nLegacy TOML names are 1–64 lowercase ASCII letters, digits, or internal\nhyphens. A pre-versioned manifest without `schema_version` still parses, with\na migration warning from `/plugin validate` (and `0.0.0` displayed when\n`[plugin].version` is missing). An unknown top-level table or field is a\nparse error (`deny_unknown_fields`), reported by byte offset without echoing\nmanifest values.\n\n### Validation (both formats)\n\nComponent paths must be relative, contained, present, and free of symbolic\nlinks or Windows reparse points (including junctions and mount points). The v1\nschema rejects unknown MCP fields, ambiguous local/remote\ntransport combinations, unbounded lists/timeouts, and overlapping tool\nfilters.\n\nRemote MCP URLs must use HTTPS, except for explicit loopback HTTP endpoints.\nThey cannot contain user information, a query, or a fragment. Literal headers\nare rejected: authentication must name a source environment variable through\n`env_headers` or `bearer_token_env_var`. A remote bundle must declare exactly\nthe normalized host set used by its endpoints in\n`capabilities.network_hosts`; endpoint scheme, normalized host, port, and path\nremain bound to the review. Redirects are limited and must retain that exact\nnormalized origin. Reviewed remote transports use an explicit no-proxy HTTP\nclient: plugin bundles never read or use ambient `HTTP_PROXY`, `HTTPS_PROXY`,\nor `NO_PROXY` values, because proxy credentials and proxy observation are\noutside the reviewed authority. User-authored MCP configuration keeps its\nexisting explicit proxy support.\n\nLocal stdio environment entries must use exact `${SOURCE_ENV}` references.\nThe review shows destination and source names, but never reads or prints their\nvalues. Plugin children inherit only Codewhale's base secret-scrubbed child\nenvironment plus those reviewed mappings; credential-capable proxy variables\nand the broader compatibility environment used by user-authored MCP\nconfiguration are not inherited ambiently. Absolute arguments and parent\ntraversal are rejected; contained bundle entrypoints are frozen to their\nstaged paths before spawn.\n\nEvery stdio argument is shown losslessly as a JSON string during review.\nCommon credential-bearing flags and known literal token shapes are rejected\nfrom argv; credentials must instead use a reviewed environment mapping.\nPlugin-contributed MCP OAuth has been disabled since v0.9.1 and remains\ndisabled as of v0.9.6, including discovery, login, refresh, and token storage;\na manifest declaring OAuth fields on a plugin MCP server fails validation.\n\n### Active and inactive component surfaces\n\nCodewhale 0.9.10 activates declarative `[skills]`, `[mcp_servers.*]`,\n`[commands]`, `[agents]`, and `[hooks]` components from its content-addressed\nruntime snapshot. Commands use markdown command files, Agents use Fleet TOML\nprofiles, and Hooks use `HooksConfig` TOML files. A component may name one file\nor a directory of the corresponding files. Ordinary user/workspace commands\nand Agent profiles keep precedence over plugin contributions; trusted project\nhooks run after plugin hooks.\n\nThe manifest can additionally inventory the following inactive surfaces.\nThose declarations stay hashed, reviewed, and displayed, but do not activate\nand no longer disable the whole bundle:\n\n```toml\n[lsp] # TOML alias: [lsp_servers]\npath = \"lsp\"\n\n[native] # TOML alias: [native_extension]\npath = \"native/index.mjs\"\n\n[capabilities]\nfilesystem_roots = [\"workspace\"]\nnetwork_hosts = [\"api.example.invalid\"]\nlifecycle_mutation = true\n```\n\n(In a `plugin.json` bundle the same tables ride under\n`extensions[\"net.codewhale\"]`.)\n\nThe accept/reject behavior is deliberately loud, never silent:\n\n- Compatibility is per-component: `full` when every declared surface has an\n adapter (or the bundle is empty), `partial` when supported components can\n activate beside named inactive surfaces, and `unsupported` when the bundle\n only declares surfaces Codewhale cannot activate yet. The same versioned\n activation policy (v3) drives those labels, the runtime adapters, and the\n capability hash. Executing LSP or native code must change that policy,\n which changes the capability hash and forces re-review. v1 and v2 trust\n receipts fail closed as `capabilities-changed`.\n- **`native` under the experimental extension host.** With\n `[features] extension_host` on, the policy becomes v4 and `native` is an\n active adapter: each entry is one `.mjs`, `.js` or `.mts` ES module file that the\n TypeScript extension host imports. A directory or any other file reports\n an error in `/plugin validate` and review and prevents activation. Its tools\n always use `Required` approval;\n Full Access, Bypass, or an exact session grant for the reviewed build can\n satisfy that gate without a prompt. Toggling the flag\n re-reviews every plugin. See\n [the design](design/TS_EXTENSION_HOST.md#as-built-phase-1-2026-09-25).\n The [extension author guide](EXTENSIONS.md) includes a tested typed example,\n diagnostic workflow and Node's erasable-TypeScript restrictions.\n- A **recognized-but-inactive** declaration (`lsp`, `native`, a non-empty\n `capabilities.filesystem_roots`, or\n `capabilities.lifecycle_mutation = true`) parses and is validated like any\n component (contained, present, link-free). It is counted in the inventory,\n hashed into the capability receipt, shown in review and `/plugin show` as\n inactive, and never executed (for `native`, only while the extension host\n flag is off). A reviewed, trusted, applicable mixed bundle\n can still be enabled: supported declarative components become active, and\n the inactive surfaces stay named as inactive.\n- An **all-unsupported** bundle can be reviewed and trusted, but `/plugin\n enable` fails closed and names the inactive surfaces. There is nothing\n Codewhale can honestly activate.\n- An **unrecognized** section or field is a validation failure, not an\n inventory entry: unknown top-level TOML tables, unknown MCP server fields,\n unknown `plugin.json` root keys, and unknown keys inside\n `extensions[\"net.codewhale\"]` are all rejected outright. The single\n ignore-without-error case is another vendor's `extensions` namespace in the\n Agent Plugins format, which the standard requires clients to skip.\n- `capabilities.network_hosts` is not a future surface: it is enforced today,\n and must exactly match the normalized host set of the bundle's remote MCP\n endpoints (so it cannot be declared without them, or omitted with them).\n\nA successful environment or health check is never treated as trust.\n\n## Review, trust, and enablement\n\nUse the in-session command surface:\n\n```text\n/plugin list\n/plugin validate example\n/plugin show example\n/plugin enable example\n```\n\nThe first `enable` opens a review showing source, component inventory,\nrequested permissions, sanitized MCP endpoints, full content and capability\nhashes, and inactive declarations. It also prints an exact confirmation:\n\n```text\n/plugin trust example .\n```\n\nRun that exact command only after reviewing the bundle. The confirmation token\nuses both complete SHA-256 receipts rather than display prefixes. The\ncapability receipt is the v3 digest: it still hashes the complete inventory\nand also binds this build's activation policy (which adapters are executable\nversus inventoried-only). Trust first\ncopies the complete reviewed tree into a Codewhale-owned, content-addressed\nruntime snapshot and records the matching receipt; it does not activate\nanything.\nThen run `/plugin enable example` again. Trust and enablement are separate:\n\n- `/plugin disable example` stops contribution while preserving trust.\n- `/plugin revoke example` removes trust while preserving the enablement bit;\n the bundle remains inactive until reviewed again.\n- `/plugin reload` rebuilds the current workspace registry when files have\n changed on disk.\n\n(`/plugin install`, `update`, and `uninstall` place, replace, and remove the\nbits themselves and always drop into this same review — see\n[PLUGINS.md](PLUGINS.md). `/plugin suggest` ranks installed bundles and\nany locally added marketplace catalogs; sending a matching task can toast the\nsame next step without installing anything. Nothing is written into the\nmodel's request to advertise plugins; the full offering policy is in\n[PLUGINS.md](PLUGINS.md#how-codewhale-offers-plugins).)\n\nTrust, enable, disable, revoke, and reload rebuild the current workspace's\nSkills, MCP, Commands, Agent profiles, and Hooks immediately. Each persisted\ntransition advances a per-bundle generation under a stable cross-process lock.\nA generation change cancels in-flight MCP work, removes cached catalog\nentries, terminates an idle plugin stdio child, and denies persisted queued\nSkills carrying the older authority receipt.\n\nThe review distinguishes remote MCP endpoints from local stdio MCP servers.\nA local stdio server is a child process running with the Codewhale user's host\nfilesystem and network authority; plugin trust is not an OS sandbox. The\nreview therefore shows the command, argument count, working directory,\nenvironment-variable names, and this host-authority warning without printing\nenvironment or header values. MCP tool approval still applies after the\nserver starts.\n\nTrust receipts live in `~/.codewhale/plugins/state.json`. Atomic owner-only\nwrites record the full content hash, capability hash, reviewed capability\ninventory, generation, and review time, with the latest 32 reviews retained as\na bounded audit trail. Malformed or unsupported state is not overwritten: all\nbundles fail closed until the state file is repaired or moved.\n\nThe content hash covers the manifest, complete bundle tree, and executable\nshape in deterministic path order, including local MCP entrypoints and\ncompanion assets. Staging is bounded, rejects symbolic links and unsupported\nfile kinds (plus every Windows reparse point and hard-linked files), uses an\natomic destination swap, and applies owner-only runtime permissions or ACLs\nthrough validated object handles on Windows. The capability hash covers the\nnormalized component and permission inventory. A source or staged-content\nedit, capability change, or unsafe runtime-root replacement invalidates the\nreceipt deterministically; an already-enabled bundle becomes inactive until\nit is reviewed again. This is the same invalidation `/plugin update` relies\non: replaced bytes stop matching the receipt, forcing re-review.\n\n## Runtime behavior\n\nAn active bundle must be enabled, trusted for its current hashes, applicable to\nthe host, and free of validation errors. A reviewed mixed bundle may be\nactive, but only supported components in the reviewed v3 activation mask are\nconsumable. Unsupported components remain listed, hashed, reviewed, and\ninactive.\n\n- Skills are exposed only as `:`. The model-facing catalogue and\n `load_skill` use an in-memory snapshot bound to the reviewed staged tree,\n rather than reading a mutable source path at execution time. `load_skill`\n revalidates source, stage, receipt, workspace, and generation immediately\n before releasing content and fails closed on drift. Queued messages persist\n the same provenance and repeat that check at dispatch. `/skills inspect`\n identifies the reviewed bundle without exposing its mutable source path.\n- MCP server names are exposed as\n `plugin---` so hyphens in either\n component cannot create an authority collision. Disabled or untrusted\n bundles are denied again at the headless MCP adapter. Authority is checked\n before connection, immediately before every lazy stdio spawn, after\n transport construction, and before each tool/resource/prompt operation.\n Persisted generation/enablement/trust state is also watched while an\n operation is in flight, so disable, revoke, or another cross-process state\n transition cancels the operation and terminates a plugin stdio child. Full\n source and staged-tree hashes are revalidated at dispatch/catalogue\n boundaries; the runtime does not continuously re-hash those trees during an\n already-running MCP call. Source or stage drift therefore fails the next\n boundary and drops the stale connection/catalogue entry, but is not claimed\n to interrupt a call already executing. Every failure includes instructions\n to reload, review, trust, and enable the bundle again.\n- Commands load after ordinary user/workspace commands and saved workflows, so\n existing definitions keep precedence and collisions are visible. The\n palette hides a revoked command immediately; dispatch rechecks the full\n receipt before expanding its body and reports a visible denial on stale\n input.\n- Agent profiles join the Fleet roster below explicit config, personal, and\n workspace profiles but above built-ins. Roster collisions retain the\n existing visible shadow record. Every Agent spawn rebuilds from the current\n registry and rechecks the selected plugin profile's authority before its\n prompt or route can be used.\n- Hooks merge after global hooks and before trusted project hooks. Foreground\n Hooks recheck authority immediately before process spawn; background Hooks\n check before enqueue and again at dequeue so a queued, revoked Hook cannot\n start later.\n- Plain launch, resume, fork, exec, and serve each construct an immutable\n workspace-scoped registry before constructing their plugin-backed catalogues.\n- Constitution, repository instructions, permission rules, sandbox policy,\n and MCP tool approval continue to outrank plugin instructions.\n\n`/plugin list`, `show`, `suggest`, and `validate` perform no network requests,\nprocess launches, credential reads, or configuration writes. Reviews render\nstructural argv as lossless JSON strings and environment provenance without\nvalues. Credential-bearing argv is rejected at manifest validation;\nplugin-originated errors suppress URL query, authentication, argv, and\nenvironment material. Legacy executable tools under `[tools].plugin_dir`\nremain a distinct system and are listed under `/plugin tools`; they cannot\napprove themselves or replace built-in tools (see\n[CONFIGURATION.md](CONFIGURATION.md#script-tools-and-overrides)).\n\n## Explicit non-goals as of v0.9.10\n\nFederated marketplace catalogs (`/plugin marketplace add|list|show|remove|install`)\nparse local Kimi-, Claude-, Codex-, and Codewhale-format catalog documents; see\nthe marketplace section below (`/plugin install` fetches\none reviewed source, and `/plugin suggest` ranks only what is already\ninstalled), no ambient compatibility discovery, no automatic trust, no\nplugin-contributed MCP OAuth, no LSP adapter or MCP subscription adapter, no\nnative extension runtime outside the experimental `extension_host` flag, no\nforeign executable plugin runtime import, and no on-disk auto-migration of a\nlegacy `plugin.toml` to `plugin.json`. The explicit offline\n[OpenCode/DSH converter](PLUGIN_AUTHORING.md#convert-an-existing-plugin) supports\nselected portable Skills, static Streamable HTTP MCP declarations, and\nexplicitly packaged Node `.mjs`, `.js`, or `.cjs` MCP servers selected with `--stdio-root`.\nLocal source and dependencies are copied for the same native installation,\ncapability review, hash-bound trust and enable flow; conversion executes no\ncode or package manager. It does not migrate arbitrary bundles or reproduce\nanother client's runtime or policy.\nThe other capabilities above remain later work rather than implied support.\n\n## Marketplace catalogs (#5311)\n\n`/plugin marketplace` reads LOCAL catalog documents in the real published\nschemas (Kimi, Claude, Codex, Codewhale native; Codex via its policy markers)\nand renders every candidate with an honest install plan:\n\n```text\n/plugin marketplace add # parse a local catalog file (no network)\n/plugin marketplace list # catalogs + candidates + diagnostics\n/plugin marketplace show # one catalog in detail\n/plugin marketplace remove # forget a catalog (plugins unaffected)\n/plugin marketplace install \n```\n\n- `add` never fetches anything: it reads one local JSON file (≤4 MiB, regular\n files only, symlinks refused) and stores the parsed catalog next to the\n plugin state file.\n- Catalog tiers and provenance (`official`, `curated`, …) are **display\n only** — they never grant trust, enablement, or installation.\n- Foreign policies are visibly ignored: a Codex `INSTALLED_BY_DEFAULT` entry\n is listed with a `NO_AUTO_INSTALL` warning and nothing is installed until\n an operator runs the install verb.\n- Catalog npm package declarations, `command:` sources and non-tarball URLs\n are listed as `not installable` with the reason. Explicit\n `/plugin install npm:package@1.2.3` and `codewhale install npm:package@1.2.3`\n instead fetch an exact-version npm registry tarball containing a native plugin\n manifest. They do not run npm lifecycle scripts or install dependencies; see\n [install sources](PLUGINS.md#sources).\n- `install` routes through the same reviewed installer as `/plugin install`:\n the bundle lands disabled and untrusted, and enters the hash-bound trust\n review before anything activates.\n" + }, + { + "path": "docs/PLUGIN_MARKETPLACE.md", + "sha256": "93e8eeb319da86064d8288940e5c13ccfc47c99e1c6a50e0d20604212651bfef", + "content": "# First-party plugin marketplace\n\nCodewhale includes an offline snapshot of the `codewhale` catalog in the same\nmarketplace store consumed by the terminal, Extensions, recommendations, and\nRuntime API. Browse the [public first-party marketplace](https://github.com/codewhale-hq/codewhale-plugin-marketplace)\nfor current entries and their prerequisites. Its contents can be newer than\nthe snapshot in your installed CLI. Browsing a catalog does not fetch or\nexecute a plugin.\n\n```text\n/plugin marketplace list\n/plugin marketplace show codewhale\n/plugin marketplace install codewhale whalewiki\n/plugin show whalewiki\n/plugin trust whalewiki\n```\n\nThe last command prints a review. Run its exact confirmation command containing\nboth full hashes, then `/plugin enable whalewiki`. Review the manifest and\ncapabilities before trust and enablement. Installation\nuses the existing size-limited, traversal-safe installer and starts disabled\nand untrusted. An update with changed bytes requires review again. Official\ncatalog provenance grants no execution or network permission. Removing the\ncatalog persists the choice and leaves installed plugins untouched; a locally\nadded catalog named `codewhale` takes precedence over the bundled snapshot.\n\nThe bundle source uses a gzip tarball URL with `#path=plugins/whalewiki` (or\n`#path=skills`). The fragment selects exactly one bundle inside the shared\nrepository archive. Only that subtree is installed. Empty paths, traversal,\nambiguous roots, links, oversized archives, and changed plugin identities are\nrejected. The install receipt preserves the source, including its selector,\nso `/plugin update` retains the same bundle selector and reviewed revision.\n\n## Catalog freshness and pinned installs\n\nThe published v0.10.2 CLI contains the 19-entry catalog from marketplace revision\n`2e3a6fba453c9b34d5b1444ab5bd6edc7b119189`. Current source and the public\nmarketplace include later additions, such as Office and Studio; that does not\nmake them part of the v0.10.2 snapshot. Use `/plugin marketplace show codewhale`\nto inspect the catalog your session actually has.\n\nThe bundled catalog is a build-time snapshot. It does not poll the public\nrepository. To use a separately reviewed catalog revision, obtain a local copy\nof the public marketplace, inspect its changes, then add its local document:\n\n```text\n/plugin marketplace add codewhale /absolute/path/codewhale-plugin-marketplace/marketplace.json\n/plugin marketplace show codewhale\n```\n\nRe-adding the same document refreshes its saved contents. If `codewhale` already\nnames a different local source, remove that catalog first with\n`/plugin marketplace remove codewhale`, then add the intended document. Catalog\nremoval does not remove installed plugins or change their trust.\n\nRepository entries in the bundled snapshot pin an immutable marketplace commit.\nRemote entries keep their declared URL and any archive checksum. The current\npublic Computer Use entry pins a versioned `computer-use.tar.gz` asset and its\nSHA-256; it does not follow GitHub's latest-release address. `/plugin update`\nre-reads the installed bundle's recorded source, including its pin. It does not\nselect a new source from a refreshed catalog.\n\n### Changing an installed pin\n\nFor a user-scope bundle installed by Codewhale, retain its original source and\nany local changes before replacing it: uninstall deletes the installed copy.\nAfter refreshing and reviewing the catalog, this example selects its current\nWhaleWiki entry:\n\n```text\n/plugin show whalewiki\n/plugin disable whalewiki\n/plugin uninstall whalewiki\n/plugin marketplace install codewhale whalewiki\n/plugin trust whalewiki\n```\n\nRun the exact hash-bound confirmation printed by the new review, then\n`/plugin enable whalewiki`. Do not apply this sequence to built-in or workspace\nbundles: update and uninstall accept only user-scope installed bundles. A local\ncatalog's `path:` source records that local directory, so keep it available for\nfuture `/plugin update` calls. A changed source still needs review and does not\nsilently inherit an earlier receipt.\n\n## Computer Use across versions\n\nFor the current helper with published CLI v0.10.2, follow the [direct MCP setup](https://github.com/codewhale-hq/codewhale-cu-plugin#codewhale-cli-setup), which uses the app’s bundled runtime. Disable an enabled older built-in before using that connection. Direct MCP supplies tools separately from plugin commands and skills.\n\nThe published v0.10.2 CLI embeds Computer Use 0.12.1. To inspect that older bundle, start with\n`/plugin show computer-use`, review with `/plugin trust computer-use`, run the\nprinted hash-bound confirmation, then `/plugin enable computer-use`. A catalog\ninstall of the same name reports that the built-in already exists; it does not\nreplace it with a newer release. Direct installation of a second bundle with\nthat name is refused. Built-in bundles cannot be updated or uninstalled through\nthe user-plugin commands.\n\nThe current Engine source candidate no longer creates or discovers the embedded\ncopy. On that layout, install `computer-use` from the catalog as an ordinary\nuser plugin, then review, trust and enable it. Its old snapshots and receipts\nremain intact; a built-in receipt cannot authorize the downloaded bundle.\nThis source transition does not establish a published CLI migration path.\n\nComputer Use has its own [published releases](https://github.com/codewhale-hq/codewhale-cu-plugin/releases)\nand [contribution guide](https://github.com/codewhale-hq/codewhale-cu-plugin/blob/main/CONTRIBUTING.md).\nInstall the native Mac helper separately using the\n[setup guide](https://codewhale.net/computer-use) for system permissions and\nPause/Stop controls. A helper release, a plugin bundle and an Engine release\nhave separate versions; their presence alone does not qualify a combination.\n\n## Chromewhale\n\nChromewhale (listed as \"Codewhale for Chrome\") is in the catalog bundled with\nCore from marketplace revision `ae3dd22` on, where its checks pass on macOS,\nLinux and Windows. It is a developer preview: installing the plugin does not\ninstall the browser side, so you load its bundled Chrome extension unpacked\nyourself. It then reads and acts on the tab you are looking at, one granted\nsite at a time. Like every catalog entry, it installs disabled and untrusted\nuntil you review it.\n\n## Keeping the repositories current\n\n| Content | Authoritative source | Copies to check |\n| --- | --- | --- |\n| Catalog, WhaleWiki, Whalesong, Cloudflare Docs | `codewhale-hq/codewhale-plugin-marketplace` | Core catalog snapshot |\n| Bundled skills | Core active catalog and `crates/tui/assets/skills` | Marketplace `skills` and `skills/upstream.json` |\n| Computer Use | `codewhale-hq/codewhale-cu-plugin` | Versioned release archive and checksum in the catalog; no current runtime mirror |\n\nThe `Marketplace connection` workflow validates the exact catalog revision on\ncatalog changes. Its weekly and manual runs compare the current public\nmarketplace, bundled snapshot, and skills. Drift fails\nthe check with a maintenance instruction; it does not rewrite user installs\nor grant new permissions. It uses read-only repository access.\n\nFor each intentional update, review the upstream diff, synchronize the\nsource-owned copies, and run the marketplace checks:\n\n```sh\n# From codewhale-plugin-marketplace, with sibling source checkouts:\n# After committing canonical skill changes, when intentionally updating skills:\nnpm run sync:skills\nnpm run check -- --core ../codewhale\nnpm test && npm run check:web\n```\n\nCommit the reviewed marketplace changes, then update Core from that committed\nrevision (the generator never copies an uncommitted marketplace document):\n\n```sh\n# From codewhale:\npython3 scripts/sync-marketplace.py --marketplace ../codewhale-plugin-marketplace\npython3 scripts/sync-marketplace.py --marketplace ../codewhale-plugin-marketplace --check\nnpm test && npm run check:web\n```\n\nReview the generated snapshot and rebuild Core. Its provenance records the\nexact marketplace commit. Local bundle URLs pin that immutable revision; remote\nentries retain their declared source. Updating the bundled catalog requires a\nnew Core snapshot; users may explicitly add a reviewed local catalog without\nrebuilding Core. Neither change retargets an existing pinned install. Push the\nreviewed marketplace revision before publishing a Core release that references it. Hosted CI must be green for the\nactual published revisions; local checks do not prove a public URL works.\n\nComputer Use source and checks belong to its canonical repository. Publish its\nqualified `computer-use.tar.gz` release asset before publishing a catalog that\nreferences that address. Do not copy its runtime or native helper into Core.\n\nSkill wording changes also need behavioral evaluation before claiming better\noutcomes. See [Skill evaluation](SKILL_EVALUATION.md).\n" }, { "path": "docs/EXTENSIONS.md", diff --git a/web/gt-catalog/en.json b/web/gt-catalog/en.json index 9615d79de..033a0645d 100644 --- a/web/gt-catalog/en.json +++ b/web/gt-catalog/en.json @@ -804,7 +804,7 @@ "codeTerms": true }, { - "p": "Keys, endpoints, provider choice, MCP servers, hooks, skills, and extra instruction files always come from your own config, so a cloned repository cannot point Codewhale at its own server or files. Start with `--no-project-config` to ignore a repository's file for one launch." + "p": "Provider keys, endpoints and provider choice come from your own config. MCP servers can also come from `.codewhale/mcp.json` after you trust the workspace in user-owned settings; project entries override same-named global entries. Review that executable authority before trusting a repository. See [MCP configuration](/docs/mcp#config)." }, { "p": "Project instructions — how an agent should work in this repository — belong in `AGENTS.md` instead. Run `/init` to create one." @@ -949,27 +949,74 @@ }, "docs-mcp": { "metaTitle": "Connect tools with MCP · Codewhale Docs", - "metaDescription": "Add Model Context Protocol servers so Codewhale can use more tools, sign in to remote servers, run Codewhale itself as an MCP server, and try code mode.", + "metaDescription": "Connect local and remote MCP servers, understand authentication and plugin compatibility, and troubleshoot missing tools.", "bodyClassName": "text-ink-soft leading-relaxed", "title": "Connect tools with MCP", - "lede": "MCP servers give Codewhale more tools — a database, an issue tracker, a browser. Add a local server that Codewhale starts for you, or a remote server by URL. Its tools then go through the same approvals as built-in ones.", + "lede": "Connect a local tool server or a remote service through Model Context Protocol. Codewhale discovers its tools and applies your tool permissions and approvals when you use them.", "sections": [ { "id": "add", - "title": "Add a server", + "title": "Try the Cloudflare Docs server", "blocks": [ { - "code": "codewhale mcp add git --command \"uvx\" --arg \"mcp-server-git\"\ncodewhale mcp add docs --url \"https://example.com/mcp\"\ncodewhale mcp list\ncodewhale mcp validate", + "p": "The public [Cloudflare Documentation server](https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/) provides documentation search and a Pages-to-Workers migration guide. [View its official source](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/docs-vectorize). Add it without credentials:" + }, + { + "code": "codewhale mcp add cloudflare-docs --url \"https://docs.mcp.cloudflare.com/mcp\"\ncodewhale mcp tools cloudflare-docs", "lang": "Terminal" }, { - "p": "`--command` starts a local server over stdio; repeat `--arg` for each argument. `--url` connects to a remote server over Streamable HTTP, with legacy SSE as a fallback. `mcp validate` checks the config and the servers you require." + "p": "The tool list should include `search_cloudflare_documentation` and `migrate_pages_to_workers_guide`. In an existing TUI session, run `/mcp reload` to load the new configuration into that session." + }, + { + "note": "Checked on November 7, 2026 with published CLI v0.10.2: both commands exited successfully and listed those two tools without credentials. This checked setup and discovery, not tool execution. The [released guide](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MCP.md) describes that version; the [main source guide](https://github.com/codewhale-hq/Codewhale/blob/main/docs/MCP.md) may include unreleased changes." }, { "p": "Inside a session, `/mcp` opens the MCP manager: each server's state, transport, timeouts, errors, and discovered tools. The same actions are available there, for example `/mcp add stdio ` and `/mcp add http `." }, { - "note": "An MCP server runs with your permissions. Add only servers you trust, as you would any program you install." + "note": "Local MCP servers run with the permissions of the Codewhale process; remote calls use the access you grant that service. Add only servers you trust." + } + ] + }, + { + "id": "compatibility", + "title": "What can connect", + "blocks": [ + { + "rows": [ + [ + "Local stdio", + "Codewhale starts the executable you configure with `--command`; repeat `--arg` for its arguments. Install the server and its required runtime first." + ], + [ + "Remote HTTP / SSE", + "Use `--url` for Streamable HTTP, with legacy SSE fallback. A legacy SSE endpoint can use `--transport sse`. A remote entry does not need `command`." + ], + [ + "Direct remote authentication", + "Your own MCP configuration can use environment-backed headers, bearer tokens or OAuth. OAuth needs compatible server metadata and client registration." + ], + [ + "Plugin-contributed MCP", + "A reviewed, enabled plugin can supply stdio or remote MCP servers. Plugin MCP supports environment-backed authentication, but plugin-contributed MCP OAuth is unsupported." + ], + [ + "Tools, resources and prompts", + "Codewhale discovers what the server actually advertises. Resources, templates and prompts are available when the server implements them." + ], + [ + "Optional extensions and MCP Apps", + "General MCP Apps UI support is not available. Basic MCP transport support does not guarantee support for every optional extension or server." + ], + [ + "Server requirements", + "Each server keeps its own OS, executable, browser, dependency and account requirements. A local server runs on the computer hosting Codewhale; Computer Use has its own platform limits." + ] + ] + }, + { + "p": "A direct MCP entry does not require a plugin. [Plugins](/plugins) can package MCP tools with instructions and other components; each component still needs compatible dependencies, review and permissions." } ] }, @@ -978,12 +1025,15 @@ "title": "Sign in to a remote server", "blocks": [ { - "p": "For a server that uses OAuth, add it by URL and log in. For a bearer token, keep the token in an environment variable instead of the config file:" + "p": "For a directly configured OAuth server, add its real URL, then log in using the name you gave it:" }, { - "code": "codewhale mcp login docs\ncodewhale mcp add tracker --url \"https://example.com/mcp\" --bearer-token-env-var TRACKER_TOKEN", + "code": "codewhale mcp login \ncodewhale mcp logout ", "lang": "Terminal" }, + { + "p": "For a bearer token, use `--bearer-token-env-var TOKEN_NAME` when adding the server, and set that variable in the environment launching Codewhale. `env_headers` maps header names to environment variable names. Literal `headers` values do not expand `${TOKEN_NAME}`. Plugin MCP entries cannot use OAuth; configure that server directly instead." + }, { "p": "An explicit Authorization header always wins: headers from config apply first, then the bearer-token variable, then a stored OAuth login. `codewhale mcp logout ` removes the stored login on this machine; the provider may keep its own grant until you revoke it there." } @@ -994,14 +1044,20 @@ "title": "Edit the config file", "blocks": [ { - "p": "Servers live in `~/.codewhale/mcp.json`. `codewhale mcp init` writes a starter file. The `mcpServers` key used by other clients works too, so you can paste an existing entry." + "p": "Global servers live in `~/.codewhale/mcp.json`; the legacy `~/.deepseek/mcp.json` is read when the Codewhale file is absent. `codewhale mcp init` writes a starter file. Use an absolute `mcp_config_path` or `CODEWHALE_MCP_CONFIG` to override it. The environment also accepts `DEEPSEEK_MCP_CONFIG`; the Codewhale name wins when both are set." }, { - "code": "{\n \"servers\": {\n \"example\": {\n \"command\": \"node\",\n \"args\": [\"./path/to/your-mcp-server.js\"],\n \"env\": {},\n \"disabled\": false\n }\n }\n}", + "code": "{\n \"servers\": {\n \"cloudflare-docs\": {\n \"url\": \"https://docs.mcp.cloudflare.com/mcp\"\n }\n }\n}", "lang": "mcp.json" }, { - "p": "After editing the file, run `/mcp reload` in the session; no restart is needed. A server starts only when a turn needs one of its tools, unless you mark it `\"required\": true` to connect at startup." + "p": "Use either `servers` or its `mcpServers` alias, never both in one file. `env_http_headers` is an alias for `env_headers`. Adapt the server's documented command, arguments and auth fields rather than assuming every client's configuration format is interchangeable." + }, + { + "p": "A trusted workspace can add `.codewhale/mcp.json`. Its entries override same-named global servers; the repository cannot grant itself that trust or override your global config path. Explicit global or trusted-project entries win over a colliding plugin server identity." + }, + { + "p": "After editing, run `/mcp reload` in the TUI; no restart is needed. Enabled, allowed servers connect at session start by default. Set `\"lazy\": true` on a user-configured server to connect on demand; `\"required\": true` still connects it at startup. Plugin MCP servers have no lazy manifest option. Headless runtimes need a restart after config changes." } ] }, @@ -1010,10 +1066,44 @@ "title": "Find the tools", "blocks": [ { - "p": "Each tool appears to the model as `mcp__`: a server named `git` with a `status` tool becomes `mcp_git_status`. `codewhale mcp tools ` lists what a server offers. A server that fails to connect or is disabled never shows up as an available tool." + "p": "Each tool appears to the model as `mcp__`. For this example, documentation search is `mcp_cloudflare-docs_search_cloudflare_documentation`. `codewhale mcp tools ` lists discovered tools in its own process; it does not attach those connections to a running session." }, { - "p": "MCP tools follow your [approval setting](/docs/modes): listing and reading a server's resources and prompts can run without a prompt when policy allows, and tools with side effects ask first. Full Access does not override repository rules or managed policy." + "p": "MCP tools follow your [approval setting](/docs/modes), allow/deny rules and network policy. Whether a call asks first depends on that policy and posture. Full Access does not override repository rules or managed policy." + } + ] + }, + { + "id": "troubleshooting", + "title": "When tools do not appear", + "blocks": [ + { + "rows": [ + [ + "Check configuration", + "Run `codewhale doctor` for static path and configuration diagnostics. Shell `codewhale mcp validate` connects enabled, allowed servers in a separate process; it is a live check." + ], + [ + "Refresh this session", + "TUI `/mcp validate` shows its last snapshot without reconnecting. Use `/mcp reload` after config changes, or `/mcp retry ` for one server." + ], + [ + "Connected, but missing a tool", + "Check `codewhale mcp tools `, the server's advertised tools, `enabled_tools` / `disabled_tools`, and the session's allow/deny rules. A connected transport alone does not mean a tool is available or permitted." + ], + [ + "Authentication required", + "For a direct OAuth server, run `/mcp login `. For token auth, check that the variable is present in Codewhale's launch environment. A successful login in one process does not attach its transport to another." + ], + [ + "Local command fails", + "Check the server's executable, arguments, working directory and runtime. On Windows, package runners may need their `.cmd` name. Follow the server vendor's OS and dependency instructions." + ], + [ + "Plugin server is absent", + "Check the owning plugin's review, trust and enabled state. MCP settings cannot activate an untrusted plugin or bypass a rejected manifest." + ] + ] } ] }, @@ -1022,14 +1112,14 @@ "title": "Run Codewhale as an MCP server", "blocks": [ { - "p": "Other MCP clients can use Codewhale as a tool server. By default, it exposes only read-only `file_read` and `search` tools." + "p": "A client supporting Codewhale's advertised MCP protocol revisions can launch it as a stdio tool server. By default, it exposes only read-only `file_read` and `search` tools." }, { "code": "codewhale mcp add-self\ncodewhale mcp tools codewhale", "lang": "Terminal" }, { - "p": "`add-self` registers `codewhale serve --mcp` over stdio; `codewhale mcp-server` is the equivalent entry point. Write tools stay withheld unless the operator explicitly permits them in server configuration. [Runtime API](/docs/runtime-api) HTTP is a separate transport." + "p": "`add-self` registers `codewhale serve --mcp`; `codewhale mcp-server` is the equivalent entry point. This server reads its separate `[server]` settings from the legacy `~/.deepseek/mcp_server.toml`, not the client `mcp.json`. Write or shell tools require explicit exposure and `require_approval = false`; there is no interactive approval channel. [Runtime API](/docs/runtime-api) HTTP is separate from MCP." } ] }, @@ -1037,6 +1127,9 @@ "id": "code-mode", "title": "Compose tool calls with code mode (experimental)", "blocks": [ + { + "note": "This section follows the main candidate's source and can include behavior newer than published v0.10.2." + }, { "p": "Code mode lets the model write one short JavaScript program that calls several tools, loops, and filters results, instead of making each call as a separate step. Only the program's final value goes back to the model, which keeps long lookups compact. It is on by default — `execute_tools` is advertised from the first request — and MCP and plugin tools compose inside a program the same way built-in ones do. Turning it off does not remove code mode: `execute_tools` goes back to being deferred, and the model can still find it through `tool_search`. To turn it off for one session, or in config:" }, @@ -2075,7 +2168,7 @@ }, { "title": "Connect your agent", - "body": "In Codewhale, install Computer Use with /plugin marketplace install codewhale computer-use, then review and enable the plugin. Other compatible MCP hosts connect to the helper using the setup guide." + "body": "Choose the Codewhale setup below for your CLI version. Other compatible MCP hosts use the repository setup guide." } ], "controlsTitle": "Stay in control", @@ -2085,7 +2178,19 @@ "help": "Setup and troubleshooting", "notes": "Release notes", "demo": "See the background check", - "source": "Other hosts and platforms", - "platforms": "The Mac helper is a beta, and the marketplace plugin supports Mac. Published releases also include an unsigned experimental Windows x64 preview; Linux requires experimental source or Docker setup. Windows and Linux input uses the shared desktop." + "source": "View on GitHub", + "platforms": "The Mac helper is a beta, and the marketplace plugin supports Mac. Published releases also include an unsigned experimental Windows x64 preview; Linux requires experimental source or Docker setup. Windows and Linux input uses the shared desktop.", + "connectTitle": "Connect Codewhale", + "connectVersion": "Codewhale 0.10.2 includes an older Computer Use plugin. Installing the catalog entry does not upgrade it. Use the installed Mac helper directly over MCP.", + "connectLimit": "Direct MCP adds tools without plugin skills. Do not use it alongside the older built-in plugin. If that plugin is enabled, run /plugin disable computer-use first.", + "connectRuntime": "The app includes Node; no separate Node install is needed. If installed elsewhere, set APP to the actual app bundle path.", + "connectReload": "In a running terminal session, run /mcp reload, switch to Operate, and ask:", + "connectPrompt": "Check Computer Use permissions with request_access. Do not send input.", + "connectExpected": "Check for via: app, the helper version and any missing permissions. If the helper is missing, stop and follow the setup hint. This checks the connection and permissions only.", + "connectGuide": "MCP setup guide", + "contributeTitle": "Help improve Computer Use", + "contributeBody": "Improve the docs or test an app workflow and share reproducible results.", + "contributeGuide": "Contribution guide", + "contributeIssue": "Report an issue" } } diff --git a/web/gt-catalog/zh.json b/web/gt-catalog/zh.json index 0629035ad..153da7878 100644 --- a/web/gt-catalog/zh.json +++ b/web/gt-catalog/zh.json @@ -804,7 +804,7 @@ "codeTerms": true }, { - "p": "密钥、服务端点、提供商选择、MCP 服务器、钩子、技能和额外的说明文件,始终来自你自己的配置,因此克隆下来的仓库无法让 Codewhale 指向它自己的服务器或文件。启动时加上 `--no-project-config`,可以在这一次运行中忽略仓库的配置文件。" + "p": "提供商密钥、服务端点及提供商选择来自你自己的配置。在用户设置中信任工作区后,MCP 服务器也可来自 `.codewhale/mcp.json`;项目条目覆盖同名的全局条目。信任仓库前,请审阅这些可执行权限。参阅 [MCP 配置](/docs/mcp#config)。" }, { "p": "项目说明——智能体在这个仓库里应该如何工作——请写在 `AGENTS.md` 中。运行 `/init` 即可生成一份。" @@ -949,27 +949,74 @@ }, "docs-mcp": { "metaTitle": "用 MCP 连接工具 · Codewhale 文档", - "metaDescription": "添加 Model Context Protocol 服务器让 Codewhale 使用更多工具,登录远程服务器,把 Codewhale 本身作为 MCP 服务器运行,并试用代码模式。", + "metaDescription": "连接本地和远程 MCP 服务器,了解认证与插件兼容范围,并排查工具未显示的问题。", "bodyClassName": "text-ink-soft leading-[1.9] tracking-wide", "title": "用 MCP 连接工具", - "lede": "MCP 服务器能为 Codewhale 带来更多工具——数据库、问题跟踪系统、浏览器等。你可以添加一个由 Codewhale 替你启动的本地服务器,也可以通过 URL 添加远程服务器。这些工具与内置工具走同一套审批流程。", + "lede": "通过 Model Context Protocol 连接本地工具服务器或远程服务。Codewhale 会发现服务器提供的工具,并在使用时执行你的工具权限和审批规则。", "sections": [ { "id": "add", - "title": "添加服务器", + "title": "试用 Cloudflare Docs 服务器", "blocks": [ { - "code": "codewhale mcp add git --command \"uvx\" --arg \"mcp-server-git\"\ncodewhale mcp add docs --url \"https://example.com/mcp\"\ncodewhale mcp list\ncodewhale mcp validate", + "p": "公开的 [Cloudflare 文档服务器](https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/)提供文档搜索和 Pages 迁移到 Workers 的指南。[查看官方源码](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/docs-vectorize)。无需凭据即可添加:" + }, + { + "code": "codewhale mcp add cloudflare-docs --url \"https://docs.mcp.cloudflare.com/mcp\"\ncodewhale mcp tools cloudflare-docs", "lang": "终端" }, { - "p": "`--command` 通过 stdio 启动本地服务器,每个参数用一个 `--arg`。`--url` 通过 Streamable HTTP 连接远程服务器,必要时回退到旧版 SSE。`mcp validate` 会检查配置文件以及你标记为必需的服务器。" + "p": "工具列表应包含 `search_cloudflare_documentation` 和 `migrate_pages_to_workers_guide`。如果 TUI 会话已在运行,请在其中执行 `/mcp reload`,把新配置加载到该会话。" + }, + { + "note": "2026 年 11 月 7 日使用已发布的 CLI v0.10.2 检查:两条命令均成功退出,无需凭据即可列出上述两个工具。这验证了配置和发现流程,没有执行工具。[发布版指南](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MCP.md)对应此版本;[main 源码指南](https://github.com/codewhale-hq/Codewhale/blob/main/docs/MCP.md)可能包含尚未发布的变更。" }, { "p": "在会话中,`/mcp` 会打开 MCP 管理器:显示每个服务器的状态、传输方式、超时设置、错误信息和已发现的工具。同样的操作在那里也能完成,例如 `/mcp add stdio ` 和 `/mcp add http `。" }, { - "note": "MCP 服务器以你的权限运行。只添加你信任的服务器,就像只安装你信任的程序一样。" + "note": "本地 MCP 服务器以 Codewhale 进程的权限运行;远程调用使用你授予该服务的访问权限。只添加你信任的服务器。" + } + ] + }, + { + "id": "compatibility", + "title": "可以连接哪些服务器", + "blocks": [ + { + "rows": [ + [ + "本地 stdio", + "Codewhale 启动 `--command` 指定的程序,每个参数用一个 `--arg`。请先安装服务器及其所需运行时。" + ], + [ + "远程 HTTP / SSE", + "`--url` 使用 Streamable HTTP,并可回退到旧版 SSE。旧版 SSE 端点可设置 `--transport sse`。远程配置不需要 `command`。" + ], + [ + "直接配置的远程认证", + "你自己的 MCP 配置可以使用环境变量请求头、bearer token 或 OAuth。OAuth 需要服务器提供兼容的认证元数据和客户端注册方式。" + ], + [ + "插件提供的 MCP", + "经过审查并启用的插件可以提供 stdio 或远程 MCP 服务器。插件 MCP 支持基于环境变量的认证,但不支持插件提供的 MCP OAuth。" + ], + [ + "工具、资源与提示词", + "Codewhale 发现服务器实际声明的能力。只有服务器实现了资源、模板或提示词功能,才能使用这些功能。" + ], + [ + "可选扩展与 MCP Apps", + "尚不提供通用的 MCP Apps 界面支持。支持基本 MCP 传输不等于兼容所有可选扩展或服务器。" + ], + [ + "服务器要求", + "每个服务器都有自己的操作系统、程序、浏览器、依赖和账号要求。本地服务器运行在承载 Codewhale 的电脑上;Computer Use 有其独立的平台限制。" + ] + ] + }, + { + "p": "直接添加 MCP 不需要插件。[插件](/plugins)可以把 MCP 工具、指令和其他组件打包在一起;各组件仍需要兼容的依赖、审查和权限。" } ] }, @@ -978,12 +1025,15 @@ "title": "登录远程服务器", "blocks": [ { - "p": "使用 OAuth 的服务器,先用 URL 添加再登录。使用 bearer token 的服务器,把令牌放在环境变量里,而不是写进配置文件:" + "p": "直接配置 OAuth 服务器时,先添加它的真实 URL,再用你设置的服务器名称登录:" }, { - "code": "codewhale mcp login docs\ncodewhale mcp add tracker --url \"https://example.com/mcp\" --bearer-token-env-var TRACKER_TOKEN", + "code": "codewhale mcp login \ncodewhale mcp logout ", "lang": "终端" }, + { + "p": "使用 bearer token 时,添加服务器时指定 `--bearer-token-env-var TOKEN_NAME`,并在启动 Codewhale 的环境中设置该变量。`env_headers` 把请求头名称映射到环境变量名。字面量 `headers` 不会展开 `${TOKEN_NAME}`。插件 MCP 不能使用 OAuth;需要 OAuth 时,请直接配置该服务器。" + }, { "p": "显式设置的 Authorization 请求头始终优先:先应用配置中的请求头,其次是 bearer token 环境变量,最后才是已保存的 OAuth 登录。`codewhale mcp logout ` 会删除本机保存的登录信息;提供商那边的授权可能仍然有效,需要到提供商处撤销。" } @@ -994,14 +1044,20 @@ "title": "编辑配置文件", "blocks": [ { - "p": "服务器配置保存在 `~/.codewhale/mcp.json`,`codewhale mcp init` 会生成一个初始文件。其他客户端使用的 `mcpServers` 键同样可用,现成的配置可以直接粘贴进来。" + "p": "全局服务器配置保存在 `~/.codewhale/mcp.json`;如果该文件不存在,仍会读取旧版 `~/.deepseek/mcp.json`。`codewhale mcp init` 会生成初始文件。可用绝对路径 `mcp_config_path` 或 `CODEWHALE_MCP_CONFIG` 覆盖默认位置。环境变量也接受 `DEEPSEEK_MCP_CONFIG`;两者都设置时,Codewhale 名称优先。" }, { - "code": "{\n \"servers\": {\n \"example\": {\n \"command\": \"node\",\n \"args\": [\"./path/to/your-mcp-server.js\"],\n \"env\": {},\n \"disabled\": false\n }\n }\n}", + "code": "{\n \"servers\": {\n \"cloudflare-docs\": {\n \"url\": \"https://docs.mcp.cloudflare.com/mcp\"\n }\n }\n}", "lang": "mcp.json" }, { - "p": "修改文件后,在会话中运行 `/mcp reload` 即可,无需重启。服务器只在某个回合需要它的工具时才启动;如果希望它在启动时就连接,把它标记为 `\"required\": true`。" + "p": "`servers` 和别名 `mcpServers` 二选一,不要在同一文件中同时使用。`env_http_headers` 是 `env_headers` 的别名。请按服务器文档调整命令、参数和认证字段,不要假定所有客户端的配置格式都能直接互换。" + }, + { + "p": "受信任的工作区可以提供 `.codewhale/mcp.json`,其中同名服务器会覆盖全局配置;仓库不能自行授予信任,也不能覆盖你的全局配置路径。全局或受信任项目中的显式配置,优先于标识冲突的插件服务器。" + }, + { + "p": "修改后在 TUI 中运行 `/mcp reload` 即可,无需重启。默认情况下,已启用且获准的服务器会在会话启动时连接。用户配置的服务器可设置 `\"lazy\": true` 按需连接;设置 `\"required\": true` 后仍会在启动时连接。插件 MCP 清单没有 lazy 选项。无界面的运行时在配置变更后需要重启。" } ] }, @@ -1010,10 +1066,44 @@ "title": "找到这些工具", "blocks": [ { - "p": "每个工具在模型眼中的名字是 `mcp__`:名为 `git` 的服务器提供的 `status` 工具,就叫 `mcp_git_status`。`codewhale mcp tools ` 会列出某个服务器提供的工具。连接失败或已禁用的服务器,其工具永远不会显示为可用。" + "p": "每个工具在模型眼中的名字是 `mcp__`。本例中的文档搜索是 `mcp_cloudflare-docs_search_cloudflare_documentation`。`codewhale mcp tools ` 在自己的进程中列出发现的工具,不会把连接附加到正在运行的会话。" }, { - "p": "MCP 工具遵循你的[审批设置](/docs/modes):在策略允许时,列出和读取服务器的资源与提示词无需确认;有副作用的工具会先征求你的同意。Full Access 也不会绕过仓库规则或托管策略。" + "p": "MCP 工具遵循你的[审批设置](/docs/modes)、允许/拒绝规则及网络策略。调用前是否询问,取决于策略与权限姿态。Full Access 不会绕过仓库规则或托管策略。" + } + ] + }, + { + "id": "troubleshooting", + "title": "工具没有出现时", + "blocks": [ + { + "rows": [ + [ + "检查配置", + "`codewhale doctor` 提供静态路径和配置诊断。终端中的 `codewhale mcp validate` 会在独立进程中连接已启用且获准的服务器,属于实际连接检查。" + ], + [ + "刷新当前会话", + "TUI 中的 `/mcp validate` 只显示上次快照,不会重新连接。配置变更后使用 `/mcp reload`;单个服务器可用 `/mcp retry `。" + ], + [ + "已连接但缺少工具", + "检查 `codewhale mcp tools `、服务器声明的工具、`enabled_tools` / `disabled_tools`,以及会话的允许/拒绝规则。传输已连接不代表某个工具可用或已获准。" + ], + [ + "需要认证", + "直接配置的 OAuth 服务器可运行 `/mcp login `。令牌认证请检查变量是否存在于 Codewhale 的启动环境中。在一个进程中登录成功,不会把该进程的连接附加到另一个进程。" + ], + [ + "本地命令失败", + "检查服务器程序、参数、工作目录和运行时。Windows 上的包运行器可能需要使用 `.cmd` 名称。请遵循服务器提供方的操作系统和依赖说明。" + ], + [ + "插件服务器缺失", + "检查所属插件的审查、信任和启用状态。MCP 设置不能激活不受信任的插件,也不能绕过被拒绝的清单。" + ] + ] } ] }, @@ -1022,14 +1112,14 @@ "title": "把 Codewhale 作为 MCP 服务器运行", "blocks": [ { - "p": "其他 MCP 客户端可以把 Codewhale 作为工具服务器。默认仅暴露只读的 `file_read` 和 `search` 工具。" + "p": "支持 Codewhale 声明的 MCP 协议版本的客户端,可以把它作为 stdio 工具服务器启动。默认仅暴露只读的 `file_read` 和 `search` 工具。" }, { "code": "codewhale mcp add-self\ncodewhale mcp tools codewhale", "lang": "终端" }, { - "p": "`add-self` 注册通过 stdio 运行的 `codewhale serve --mcp`;`codewhale mcp-server` 是等价入口。除非操作者在服务器配置中明确允许,否则不会提供写入工具。[Runtime API](/docs/runtime-api) HTTP 是单独的传输方式。" + "p": "`add-self` 注册 `codewhale serve --mcp`;`codewhale mcp-server` 是等价入口。这个服务器从旧版路径 `~/.deepseek/mcp_server.toml` 读取独立的 `[server]` 设置,而不是客户端的 `mcp.json`。写入或 shell 工具需要显式列出,并设置 `require_approval = false`;这里没有交互式审批通道。[Runtime API](/docs/runtime-api) HTTP 与 MCP 分开。" } ] }, @@ -1037,6 +1127,9 @@ "id": "code-mode", "title": "用代码模式组合工具调用(实验性)", "blocks": [ + { + "note": "本节依据 main 候选版本的源码,可能包含比已发布 v0.10.2 更新的行为。" + }, { "p": "代码模式让模型写一段简短的 JavaScript 程序,在其中调用多个工具、循环和筛选结果,而不必把每次调用都作为单独的一步。只有程序的最终结果会返回给模型,长时间的查找因此更紧凑。它默认开启——`execute_tools` 从第一条请求起就会提供——MCP 和插件工具与内置工具一样可以在程序中组合调用。关闭它并不会移除代码模式:`execute_tools` 会恢复为延迟加载,模型仍可通过 `tool_search` 找到它。要关闭它,可以只在一次会话中关闭,也可以在配置中关闭:" }, @@ -2075,7 +2168,7 @@ }, { "title": "连接智能体", - "body": "在 Codewhale 中运行 /plugin marketplace install codewhale computer-use,审阅并启用插件。其他兼容 MCP 的宿主可按设置指南连接助手。" + "body": "选择下方适用于你的 CLI 版本的 Codewhale 设置方式。其他兼容 MCP 的宿主请参阅仓库中的设置指南。" } ], "controlsTitle": "随时掌握控制权", @@ -2085,7 +2178,19 @@ "help": "设置与故障排查", "notes": "版本说明", "demo": "查看后台检查演示", - "source": "其他宿主与平台", - "platforms": "Mac 助手为测试版,市场插件支持 Mac。已发布版本另含未签名的 Windows x64 实验性预览包;Linux 需要实验性源码或 Docker 设置。Windows 和 Linux 的输入使用共享桌面。" + "source": "在 GitHub 上查看", + "platforms": "Mac 助手为测试版,市场插件支持 Mac。已发布版本另含未签名的 Windows x64 实验性预览包;Linux 需要实验性源码或 Docker 设置。Windows 和 Linux 的输入使用共享桌面。", + "connectTitle": "连接 Codewhale", + "connectVersion": "Codewhale 0.10.2 内置较旧的 Computer Use 插件,安装目录中的同名条目不会将其升级。请通过 MCP 直接连接已安装的 Mac 助手。", + "connectLimit": "直接连接 MCP 只添加工具,不包含插件技能。请勿与旧版内置插件同时使用;如果该插件已启用,请先运行 /plugin disable computer-use。", + "connectRuntime": "应用自带 Node,无需另行安装。如果应用安装在其他位置,请将 APP 设为实际应用包路径。", + "connectReload": "在正在运行的终端会话中执行 /mcp reload,切换到 Operate,然后输入:", + "connectPrompt": "使用 request_access 检查 Computer Use 权限。不要发送任何输入。", + "connectExpected": "检查回复中的 via: app、助手版本及缺少的权限。如果找不到助手,请停止并按设置提示操作。此步骤仅检查连接和权限。", + "connectGuide": "MCP 设置指南", + "contributeTitle": "一起改进 Computer Use", + "contributeBody": "完善文档,或测试应用操作流程并分享可复现的结果。", + "contributeGuide": "贡献指南", + "contributeIssue": "报告问题" } } diff --git a/web/lib/content/plugins.ts b/web/lib/content/plugins.ts index 395e653eb..b4aa0ee5e 100644 --- a/web/lib/content/plugins.ts +++ b/web/lib/content/plugins.ts @@ -1,16 +1,16 @@ import type { LocalizedText } from "./vocabulary"; export const PLUGINS_COPY = { - metaTitle: { en: "Plugins and marketplace · Codewhale", zh: "插件与市场 · Codewhale" }, + metaTitle: { en: "Plugins, skills and MCP servers · Codewhale", zh: "插件与市场 · Codewhale" }, metaDescription: { - en: "Let the agent operate the desktop apps you choose with Computer Use, and install reviewed plugins from the Codewhale catalog, a GitHub repository, or a tarball.", - zh: "用 Computer Use 让代理操作你选定的桌面应用,并从 Codewhale 目录、GitHub 仓库或压缩包安装经过审阅的插件。", + en: "Connect local and remote MCP servers, add reusable skills, and install reviewed plugins for browser work, documentation, code review and desktop apps.", + zh: "连接本地与远程 MCP 服务器、加载可复用的技能,并安装用于浏览器、文档、代码审阅和桌面应用的已审阅插件。", }, kicker: { en: "Plugins and marketplace", zh: "插件与市场" }, - title: { en: "Add tools and skills with plugins", zh: "通过插件添加工具和技能" }, + title: { en: "Connect your tools to Codewhale", zh: "将你的工具连接到 Codewhale" }, lead: { - en: "Add skills and tools from the Codewhale catalog, a GitHub repository or a tarball. Review, trust and enable each plugin before use.", - zh: "从 Codewhale 目录、GitHub 仓库或压缩包添加技能和工具。使用前,先审阅、确认信任并启用插件。", + en: "Connect an MCP server for tools, load a skill for instructions, or install a plugin that packages both. You choose which integrations run.", + zh: "连接 MCP 服务器来添加工具,加载技能来复用指令,或安装将两者打包的插件。由你决定运行哪些集成。", }, marketplaceCta: { en: "Browse the Codewhale catalog", zh: "浏览 Codewhale 目录" }, docsCta: { en: "Plugin authoring guide", zh: "插件编写指南" }, @@ -22,7 +22,6 @@ export const PLUGINS_COPY = { en: "Let an agent work in Mac apps while you keep using your computer. Connect the Computer Use helper and plugin to Codewhale or another compatible MCP host.", zh: "智能体在 Mac 应用中后台操作,你可以继续使用电脑。将 Computer Use 助手和插件连接到 Codewhale 或其他兼容 MCP 的宿主。", }, - cuInstall: { en: "/plugin marketplace install codewhale computer-use", zh: "/plugin marketplace install codewhale computer-use" }, cuFeatures: [ { title: { en: "Works in the background", zh: "后台工作" }, @@ -89,53 +88,27 @@ export const PLUGINS_COPY = { catalogLabel: { en: "First-party catalog", zh: "官方目录" }, catalogTitle: { en: "Browse Codewhale plugins", zh: "浏览 Codewhale 插件" }, catalogLead: { - en: "An offline snapshot of the Codewhale catalog ships with the terminal, so browsing never fetches or runs anything. Install from it with one command.", - zh: "终端内置 Codewhale 目录的离线快照,浏览时不会拉取或运行任何内容。一条命令即可安装。", + en: "These are examples from the public Codewhale catalog. The terminal ships an offline snapshot: available entries and pinned versions depend on your CLI release. Browsing the catalog runs nothing.", + zh: "以下是公开 Codewhale 目录中的部分示例。终端内置离线快照,条目及固定版本取决于 CLI 版本;浏览目录不会运行任何内容。", }, catalogBrowse: { en: "/plugin marketplace list", zh: "/plugin marketplace list" }, - catalogEntries: [ - { - title: { en: "WhaleWiki", zh: "WhaleWiki" }, - detail: { - en: "Repository and concept documentation the agent can consult while it works.", - zh: "供代理在工作时查阅的仓库与概念文档。", - }, - reference: "/plugin marketplace install codewhale whalewiki", - }, - { - title: { en: "Cloudflare Docs", zh: "Cloudflare 文档" }, - detail: { - en: "Cloudflare documentation lookup inside a session.", - zh: "在会话内查询 Cloudflare 文档。", - }, - reference: "/plugin marketplace install codewhale cloudflare-docs", - }, - { - title: { en: "Skill bundle", zh: "技能包" }, - detail: { - en: "The bundled Codewhale skills the terminal ships with, kept in sync with the catalog.", - zh: "终端内置的 Codewhale 技能集,与目录保持同步。", - }, - reference: "#path=skills", - }, - ], catalogNote: { en: "Catalog source: github.com/codewhale-hq/codewhale-plugin-marketplace. A local catalog named codewhale takes precedence over the bundled snapshot.", zh: "目录来源:github.com/codewhale-hq/codewhale-plugin-marketplace。名为 codewhale 的本地目录优先于内置快照。", }, sourcesLabel: { en: "Install sources", zh: "安装来源" }, - sourcesTitle: { en: "Install from a repository, folder, or tarball.", zh: "从仓库、文件夹或压缩包安装。" }, + sourcesTitle: { en: "Install a bundle from its source", zh: "从来源安装插件包" }, sourcesLead: { - en: "Point /plugin install at a directory, a repository, or a tarball. The fetched tree must hold exactly one bundle root: a plugin.json, a compatible kimi.plugin.json or .claude-plugin/plugin.json, or a legacy plugin.toml.", - zh: "/plugin install 可以指向目录、仓库或压缩包。获取的目录树必须恰好包含一个 bundle 根:plugin.json、兼容的 kimi.plugin.json 或 .claude-plugin/plugin.json,或旧版 plugin.toml。", + en: "Point /plugin install at a directory, a GitHub repository, a tarball, or an exact-version npm package. The fetched tree must hold exactly one bundle root: a plugin.json, a compatible kimi.plugin.json or .claude-plugin/plugin.json, or a legacy plugin.toml.", + zh: "/plugin install 可以指向目录、GitHub 仓库、压缩包或指定版本的 npm 包。获取的目录树必须恰好包含一个 bundle 根:plugin.json、兼容的 kimi.plugin.json 或 .claude-plugin/plugin.json,或旧版 plugin.toml。", }, sources: [ { title: { en: "GitHub repository", zh: "GitHub 仓库" }, detail: { - en: "Fetches an archive of the default branch, the same mechanism the marketplace catalog uses.", - zh: "获取默认分支的归档,市场目录也使用这一机制。", + en: "Fetches a repository archive. Pin a commit for repeatable installs.", + zh: "获取仓库归档。固定提交可实现可重复安装。", }, reference: "/plugin install github:owner/repo", }, @@ -155,6 +128,11 @@ export const PLUGINS_COPY = { }, reference: "/plugin install https://example.com/x.tar.gz", }, + { + title: { en: "Version-pinned npm package", zh: "固定版本的 npm 包" }, + detail: { en: "Fetches the exact tarball without lifecycle scripts or dependency installation. It must contain a supported native plugin manifest.", zh: "获取指定版本的压缩包,不运行生命周期脚本或安装依赖。包中须包含受支持的原生插件清单。" }, + reference: "/plugin install npm:@example/plugin@1.0.0", + }, ], trustLabel: { en: "Trust lifecycle", zh: "信任生命周期" }, @@ -181,8 +159,8 @@ export const PLUGINS_COPY = { { title: { en: "Enable", zh: "启用" }, detail: { - en: "/plugin enable activates the bundle. Plugin-contributed MCP servers run under namespaced - identities with the stricter boundary.", - zh: "/plugin enable 激活 bundle。插件提供的 MCP 服务以 - 命名空间身份运行,适用更严格的边界。", + en: "/plugin enable activates the bundle. Plugin-contributed MCP servers use namespaced identities and the plugin’s reviewed permissions.", + zh: "/plugin enable 激活 bundle。插件提供的 MCP 服务器使用带命名空间的身份,受已审阅的插件权限约束。", }, }, ], @@ -215,3 +193,39 @@ export const PLUGIN_COMMANDS = [ "/plugin update ", "/plugin suggest ", ] as const; + +export const INTEGRATIONS_COPY = { + chooseTitle: { en: "Plugin, skill, or MCP server?", zh: "插件、技能,还是 MCP 服务器?" }, + chooseLead: { en: "Start with what you want to add.", zh: "根据你想添加的能力选择。" }, + mcpTitle: { en: "Connect an MCP server directly", zh: "直接连接 MCP 服务器" }, + mcpLead: { en: "Use local tools over stdio or a remote service over Streamable HTTP. Direct remote connections can use OAuth or environment-backed tokens. A plugin is optional.", zh: "通过 stdio 使用本地工具,或通过 Streamable HTTP 连接远程服务。直接连接的远程服务器可使用 OAuth 或环境变量令牌,无需安装插件。" }, + mcpLink: { en: "MCP setup, authentication and troubleshooting", zh: "MCP 设置、认证与故障排查" }, + compatibilityTitle: { en: "Which plugin formats work?", zh: "支持哪些插件格式?" }, + compatibilityLead: { en: "Compatibility depends on the capabilities inside a bundle. A recognized manifest does not mean every feature from another host will run.", zh: "兼容性取决于插件包含的能力。能识别清单,并不代表其他宿主的每项功能都能运行。" }, + contributeTitle: { en: "Share an integration", zh: "分享你的集成" }, + contributeLead: { en: "Package a useful workflow, document its dependencies, and show what you verified. The authoring guide covers a local bundle; the catalog repository is where to propose an entry or improve an existing one.", zh: "将实用工作流打包,说明依赖并提供验证结果。编写指南介绍本地插件包;新增目录条目或改进现有条目,请前往目录仓库。" }, + catalogLink: { en: "Contribute to the catalog", zh: "参与目录贡献" }, + cuSetupNote: { en: "The published CLI v0.10.2 includes an older Computer Use bundle. Installing the catalog entry does not upgrade that built-in copy. Follow the version-specific setup before connecting the current Mac helper.", zh: "已发布的 CLI v0.10.2 内置较旧的 Computer Use 插件。安装目录条目不会升级内置副本。连接当前 Mac 助手前,请按对应版本的指南设置。" }, +} satisfies Record; + +export const INTEGRATION_CHOICES = [ + { title: { en: "Skill", zh: "技能" }, detail: { en: "Reusable Markdown instructions for a task. Uses the tools already available; it does not create a new service connection.", zh: "可复用的 Markdown 任务指令。使用已有工具,不会自行创建服务连接。" }, href: "/docs/skills" }, + { title: { en: "MCP server", zh: "MCP 服务器" }, detail: { en: "An external program or service that exposes tools, resources or prompts. Configure its command or URL and authentication.", zh: "提供工具、资源或提示词的外部程序或服务。配置命令或 URL 以及认证方式。" }, href: "/docs/mcp" }, + { title: { en: "Plugin", zh: "插件" }, detail: { en: "A reviewed bundle of skills, commands, agent profiles, hooks, MCP servers or provider declarations. Install, review, trust, then enable.", zh: "将技能、命令、代理配置、钩子、MCP 服务器或模型提供商声明打包。安装、审阅、信任后再启用。" }, href: "#trust-title" }, +]; + +export const PLUGIN_EXAMPLES = [ + { name: "cloudflare-docs", title: "Cloudflare Docs", detail: { en: "Look up Cloudflare documentation with a remote MCP server and skill. No credentials required.", zh: "通过远程 MCP 服务器与技能查阅 Cloudflare 文档,无需凭据。" } }, + { name: "chromewhale", title: "Codewhale for Chrome", detail: { en: "Work in signed-in Chrome through local MCP. Requires Node and the separate browser extension, with per-site grants.", zh: "通过本地 MCP 操作已登录的 Chrome。需要 Node 和独立浏览器扩展,并按站点授权。" } }, + { name: "github-workflow", title: "GitHub Workflow", detail: { en: "Review issues and pull requests with skills, commands and GitHub’s remote MCP. Requires your GitHub token; review its scopes.", zh: "使用技能、命令及 GitHub 远程 MCP 审阅问题与拉取请求。需要 GitHub 令牌,请审阅其权限范围。" } }, + { name: "linear-workflow", title: "Linear Workflow", detail: { en: "Turn issue context into a plan with skills, commands and remote MCP. Requires a Linear API key.", zh: "通过技能、命令及远程 MCP 根据问题上下文制定计划。需要 Linear API 密钥。" } }, + { name: "review-toolkit", title: "Review Toolkit", detail: { en: "Use focused review agent profiles, a command and a skill. Adds no MCP server or service login.", zh: "使用专门的审阅代理配置、命令和技能。不添加 MCP 服务器,也无需服务登录。" } }, + { name: "whalewiki", title: "WhaleWiki", detail: { en: "Research a repository with a skill, command, agent profile and local MCP. Requires Node.", zh: "通过技能、命令、代理配置及本地 MCP 研究仓库。需要 Node。" } }, +]; + +export const PLUGIN_COMPATIBILITY = [ + { title: { en: "Codewhale / Agent Plugins v1", zh: "Codewhale / Agent Plugins v1" }, detail: { en: "Declarative skills, commands, agent profiles, hooks, MCP and provider declarations. Native TypeScript extensions are experimental and require extension_host; declaring LSP or filesystem roots does not activate them.", zh: "支持声明式技能、命令、代理配置、钩子、MCP 及模型提供商声明。原生 TypeScript 扩展为实验功能,需启用 extension_host;声明 LSP 或文件系统根目录不会启用对应能力。" }, href: "https://github.com/codewhale-hq/Codewhale/blob/main/docs/PLUGIN_AUTHORING.md" }, + { title: { en: "Claude Code and Kimi bundles", zh: "Claude Code 与 Kimi 插件包" }, detail: { en: "Compatible skills, commands, agent profiles and MCP declarations can be imported. Claude hooks, LSP, custom MCP paths and plugin-root expansion are rejected. Review the imported capabilities.", zh: "可导入兼容的技能、命令、代理配置及 MCP 声明。Claude 钩子、LSP、自定义 MCP 路径及插件根路径展开会被拒绝。请审阅导入的能力。" }, href: "/docs/claude-code" }, + { title: { en: "DeepSeek Harness (DSH)", zh: "DeepSeek Harness(DSH)" }, detail: { en: "Import work is in progress. Full published-plugin workflows and interactive views are not release-qualified; do not assume a DSH package runs unchanged.", zh: "导入兼容工作进行中。已发布插件的完整工作流及交互视图尚未通过发布验证,请勿假定 DSH 包可直接运行。" }, href: "https://github.com/codewhale-hq/Codewhale/blob/main/docs/DSH_PLUGIN_COMPAT.md" }, + { title: { en: "MCP authentication and UI", zh: "MCP 认证与界面" }, detail: { en: "Plugin-bundled MCP does not support OAuth: configure that server directly instead. MCP Apps UI and every optional protocol feature are not generally supported.", zh: "插件内的 MCP 不支持 OAuth,请改为直接配置服务器。MCP Apps 界面及所有可选协议功能并非普遍受支持。" }, href: "/docs/mcp#compatibility" }, +]; diff --git a/web/lib/docs-map.ts b/web/lib/docs-map.ts index 811bdd3e2..b038babd7 100644 --- a/web/lib/docs-map.ts +++ b/web/lib/docs-map.ts @@ -251,11 +251,12 @@ export const DOC_TOPICS: DocTopic[] = [ slug: "plugins", label: { en: "Add plugins", zh: "添加插件" }, description: { - en: "Find, install, review, and trust plugin bundles.", - zh: "查找、安装、审查并信任插件包。", + en: "Choose plugin formats, browse catalog examples, and review, trust and enable bundles.", + zh: "选择插件格式,浏览目录示例,并审阅、信任和启用插件包。", }, - repoSource: ["docs/PLUGINS.md", "docs/PLUGIN_BUNDLES.md", "docs/EXTENSIONS.md"], - hasPage: false, + repoSource: ["docs/PLUGINS.md", "docs/PLUGIN_BUNDLES.md", "docs/PLUGIN_MARKETPLACE.md", "docs/EXTENSIONS.md"], + hasPage: true, + sitePath: "plugins", category: "extending", }, { diff --git a/web/lib/i18n/dictionaries/ar/computer-use.ts b/web/lib/i18n/dictionaries/ar/computer-use.ts index dcb048ada..aa0424c76 100644 --- a/web/lib/i18n/dictionaries/ar/computer-use.ts +++ b/web/lib/i18n/dictionaries/ar/computer-use.ts @@ -26,7 +26,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "ثبّت التطبيق", body: "افتح صورة القرص واسحب Codewhale Computer Use إلى مجلد «التطبيقات». افتحه من «التطبيقات»، ثم اختر Computer Use من أيقونة الحوت في شريط القوائم." }, { title: "راجع الأذونات", body: "استخدم أزرار الإعداد لفتح «تسهيلات الاستخدام» و«تسجيل الشاشة» في إعدادات النظام. أنت من يقرر الأذونات التي تمنحها." }, - { title: "اربط وكيلك", body: "في Codewhale، ثبّت Computer Use بالأمر /plugin marketplace install codewhale computer-use، ثم راجع الإضافة وفعّلها. يُثبّت المساعد بشكل منفصل. يمكن لمضيفي MCP المتوافقين الآخرين الاتصال به باتباع دليل الإعداد." }, + { title: "اربط وكيلك", body: "اختر إعداد Codewhale أدناه المناسب لإصدار CLI لديك. أما مضيفات MCP الأخرى المتوافقة فتستخدم دليل الإعداد في المستودع." }, ], controlsTitle: "التحكم بيدك", controlsBody: "بعد منح أذونات macOS، يعمل الوضع القياسي دون طلب إذن لكل تطبيق. تظل التطبيقات التي تحظرها خارج نطاق الوصول؛ يعلّق Pause استخدام الكمبيوتر، وينهي Stop computer use الجلسات الحالية. وضع الموافقة الصارم اختياري. اطلب معاينة مباشرة عندما تريد متابعة العمل.", @@ -35,6 +35,18 @@ export const computerUse: ComputerUseDict = { help: "الإعداد واستكشاف الأخطاء وإصلاحها", notes: "ملاحظات الإصدار", demo: "شاهد فحص الخلفية", - source: "المصدر والمنصات الأخرى", + source: "عرض على GitHub", platforms: "مساعد Mac في مرحلة بيتا، وإضافة السوق متاحة لنظام Mac فقط. تتضمن الإصدارات المنشورة أيضًا معاينة تجريبية غير موقّعة لنظام Windows x64؛ ويتطلب Linux إعدادًا تجريبيًا من المصدر أو عبر Docker. يستخدم الإدخال في Windows وLinux سطح المكتب المشترك.", + connectTitle: "ربط Codewhale", + connectVersion: "يتضمن Codewhale 0.10.2 إضافة Computer Use قديمة. تثبيت مدخل الكتالوج لا يحدّثها. اتصل بمساعد Mac المثبّت مباشرة عبر MCP.", + connectLimit: "يضيف اتصال MCP المباشر أدوات دون مهارات الإضافة. لا تستخدمه بالتزامن مع الإضافة المدمجة القديمة. إذا كانت مفعّلة، فنفّذ /plugin disable computer-use أولاً.", + connectRuntime: "يتضمن التطبيق Node؛ لا حاجة إلى تثبيته بشكل منفصل. إذا كان التطبيق مثبّتاً في مكان آخر، فاضبط APP على المسار الفعلي لحزمة التطبيق.", + connectReload: "في جلسة طرفية قيد التشغيل، نفّذ /mcp reload، ثم انتقل إلى Operate واطلب:", + connectPrompt: "تحقّق من أذونات Computer Use باستخدام request_access. لا ترسل أي إدخال.", + connectExpected: "تحقّق من via: app وإصدار المساعد والأذونات الناقصة في الرد. إذا كان المساعد غير موجود، فتوقّف واتبع إرشادات الإعداد. يتحقّق هذا من الاتصال والأذونات فقط.", + connectGuide: "دليل إعداد MCP", + contributeTitle: "ساهم في تحسين Computer Use", + contributeBody: "حسّن التوثيق أو اختبر سير عمل في أحد التطبيقات وشارك نتائج قابلة لإعادة الإنتاج.", + contributeGuide: "دليل المساهمة", + contributeIssue: "الإبلاغ عن مشكلة", }; diff --git a/web/lib/i18n/dictionaries/ca/computer-use.ts b/web/lib/i18n/dictionaries/ca/computer-use.ts index 33b4ab129..98447531e 100644 --- a/web/lib/i18n/dictionaries/ca/computer-use.ts +++ b/web/lib/i18n/dictionaries/ca/computer-use.ts @@ -27,7 +27,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "Instal·la l’aplicació", body: "Obre la imatge de disc i arrossega Codewhale Computer Use a «Aplicacions». Obre-la des d’«Aplicacions» i tria Computer Use a la icona de la balena de la barra de menús." }, { title: "Revisa els permisos", body: "Fes servir els botons de configuració per obrir «Accessibilitat» i «Gravació de pantalla» a Configuració del Sistema. Tu decideixes quins permisos concedeixes." }, - { title: "Connecta el teu agent", body: "A Codewhale, instal·la Computer Use amb /plugin marketplace install codewhale computer-use i, després, revisa i activa el connector. L’ajudant s’instal·la per separat. Altres amfitrions MCP compatibles s’hi connecten seguint la guia de configuració." }, + { title: "Connecta el teu agent", body: "Tria a continuació la configuració de Codewhale per a la teva versió de la CLI. Els altres clients MCP compatibles poden fer servir la guia de configuració del repositori." }, ], controlsTitle: "Mantén el control", controlsBody: "Després de concedir els permisos de macOS, el mode estàndard funciona sense demanar permís per a cada app. Les apps que bloquegis continuen fora de l’abast; Pause suspèn l’ús de l’ordinador i Stop computer use finalitza les sessions actuals. L’aprovació estricta és opcional. Demana una previsualització en directe quan vulguis veure què fa.", @@ -36,6 +36,18 @@ export const computerUse: ComputerUseDict = { help: "Configuració i resolució de problemes", notes: "Notes de la versió", demo: "Mira la comprovació en segon pla", - source: "Codi font i altres plataformes", + source: "Veure a GitHub", platforms: "L’ajudant del Mac és una beta i el connector del catàleg només admet Mac. Les versions publicades també inclouen una previsualització experimental sense signar per a Windows x64; Linux requereix una configuració experimental des del codi font o amb Docker. L’entrada a Windows i Linux utilitza l’escriptori compartit.", + connectTitle: "Connecta Codewhale", + connectVersion: "Codewhale 0.10.2 inclou un connector Computer Use antic. Instal·lar l’entrada del catàleg no l’actualitza. Connecta directament l’auxiliar de Mac instal·lat mitjançant MCP.", + connectLimit: "La connexió MCP directa afegeix eines sense les habilitats del connector. No la facis servir alhora que el connector integrat antic. Si està activat, executa primer /plugin disable computer-use.", + connectRuntime: "L’aplicació inclou Node; no cal instal·lar-lo per separat. Si està instal·lada en una altra ubicació, assigna a APP el camí real del paquet de l’aplicació.", + connectReload: "En una sessió de terminal en execució, executa /mcp reload, canvia a Operate i demana:", + connectPrompt: "Comprova els permisos de Computer Use amb request_access. No enviïs cap entrada.", + connectExpected: "Comprova via: app, la versió de l’auxiliar i els permisos que faltin a la resposta. Si no es troba l’auxiliar, atura’t i segueix la indicació de configuració. Això només comprova la connexió i els permisos.", + connectGuide: "Guia de configuració MCP", + contributeTitle: "Ajuda a millorar Computer Use", + contributeBody: "Millora la documentació o prova un flux de treball en una aplicació i comparteix resultats reproduïbles.", + contributeGuide: "Guia de contribució", + contributeIssue: "Informar d’un problema", }; diff --git a/web/lib/i18n/dictionaries/de/computer-use.ts b/web/lib/i18n/dictionaries/de/computer-use.ts index 46d7a0cc0..b75fa9651 100644 --- a/web/lib/i18n/dictionaries/de/computer-use.ts +++ b/web/lib/i18n/dictionaries/de/computer-use.ts @@ -27,7 +27,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "App installieren", body: "Öffne das Disk-Image und ziehe Codewhale Computer Use in den Ordner „Programme“. Öffne die App aus „Programme“ und wähle dann Computer Use über das Wal-Symbol in deiner Menüleiste." }, { title: "Berechtigungen prüfen", body: "Über die Einrichtungsschaltflächen öffnest du „Bedienungshilfen“ und „Bildschirmaufnahme“ in den Systemeinstellungen. Welche Berechtigungen du erteilst, entscheidest du." }, - { title: "Deinen Agenten verbinden", body: "Installiere Computer Use in Codewhale mit /plugin marketplace install codewhale computer-use, prüfe das Plugin und aktiviere es. Der Helfer wird separat installiert. Andere kompatible MCP-Hosts verbinden sich über die Einrichtungsanleitung mit ihm." }, + { title: "Deinen Agenten verbinden", body: "Wähle unten die Codewhale-Einrichtung für deine CLI-Version. Andere kompatible MCP-Clients nutzen die Einrichtungsanleitung im Repository." }, ], controlsTitle: "Du behältst die Kontrolle", controlsBody: "Nach Erteilung der macOS-Berechtigungen arbeitet der Standardmodus ohne Rückfragen für jede App. Von dir gesperrte Apps bleiben gesperrt; Pause unterbricht die Computernutzung und Stop computer use beendet laufende Sitzungen. Strenge Genehmigungen sind optional. Bitte um eine Live-Vorschau, wenn du zusehen möchtest.", @@ -36,6 +36,18 @@ export const computerUse: ComputerUseDict = { help: "Einrichtung und Fehlerbehebung", notes: "Versionshinweise", demo: "Hintergrundprüfung ansehen", - source: "Quellcode und weitere Plattformen", + source: "Auf GitHub ansehen", platforms: "Der Mac-Helfer ist eine Beta; das Marketplace-Plugin unterstützt nur Mac. Veröffentlichte Versionen enthalten auch eine unsignierte experimentelle Vorschau für Windows x64; Linux erfordert eine experimentelle Einrichtung aus dem Quellcode oder mit Docker. Eingaben unter Windows und Linux nutzen den gemeinsamen Desktop.", + connectTitle: "Codewhale verbinden", + connectVersion: "Codewhale 0.10.2 enthält ein älteres Computer-Use-Plugin. Die Installation des Katalogeintrags aktualisiert es nicht. Verbinde den installierten Mac-Helfer direkt über MCP.", + connectLimit: "Die direkte MCP-Verbindung fügt Werkzeuge ohne Plugin-Skills hinzu. Nutze sie nicht parallel zum älteren integrierten Plugin. Falls dieses aktiviert ist, führe zuerst /plugin disable computer-use aus.", + connectRuntime: "Die App enthält Node; eine separate Installation ist nicht nötig. Falls sie woanders installiert ist, setze APP auf den tatsächlichen Pfad des App-Bundles.", + connectReload: "Führe in einer laufenden Terminalsitzung /mcp reload aus, wechsle zu Operate und frage:", + connectPrompt: "Prüfe die Computer-Use-Berechtigungen mit request_access. Sende keine Eingaben.", + connectExpected: "Prüfe die Antwort auf via: app, die Helferversion und fehlende Berechtigungen. Falls der Helfer fehlt, halte an und folge dem Einrichtungshinweis. Das prüft nur Verbindung und Berechtigungen.", + connectGuide: "MCP-Einrichtungsanleitung", + contributeTitle: "Computer Use mitgestalten", + contributeBody: "Verbessere die Dokumentation oder teste einen Ablauf in einer App und teile reproduzierbare Ergebnisse.", + contributeGuide: "Leitfaden zum Mitwirken", + contributeIssue: "Problem melden", }; diff --git a/web/lib/i18n/dictionaries/en/computer-use.ts b/web/lib/i18n/dictionaries/en/computer-use.ts index 95ed295fd..f989eccc2 100644 --- a/web/lib/i18n/dictionaries/en/computer-use.ts +++ b/web/lib/i18n/dictionaries/en/computer-use.ts @@ -26,7 +26,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "Install the app", body: "Open the disk image and drag Codewhale Computer Use into Applications. Open it from Applications, then choose Computer Use from the whale icon in your menu bar." }, { title: "Review permissions", body: "Use the setup buttons to open Accessibility and Screen Recording in System Settings. You choose which permissions to grant." }, - { title: "Connect your agent", body: "In Codewhale, install Computer Use with /plugin marketplace install codewhale computer-use, then review and enable the plugin. Other compatible MCP hosts connect to the helper using the setup guide." }, + { title: "Connect your agent", body: "Choose the Codewhale setup below for your CLI version. Other compatible MCP hosts use the repository setup guide." }, ], controlsTitle: "Stay in control", controlsBody: "After you grant the macOS permissions, standard mode works without per-app prompts. Apps you deny stay off limits; Pause suspends computer use and Stop computer use ends current sessions. Strict approval is optional. Request a live preview when you want to watch.", @@ -35,6 +35,18 @@ export const computerUse: ComputerUseDict = { help: "Setup and troubleshooting", notes: "Release notes", demo: "See the background check", - source: "Other hosts and platforms", + source: "View on GitHub", platforms: "The Mac helper is a beta, and the marketplace plugin supports Mac. Published releases also include an unsigned experimental Windows x64 preview; Linux requires experimental source or Docker setup. Windows and Linux input uses the shared desktop.", + connectTitle: "Connect Codewhale", + connectVersion: "Codewhale 0.10.2 includes an older Computer Use plugin. Installing the catalog entry does not upgrade it. Use the installed Mac helper directly over MCP.", + connectLimit: "Direct MCP adds tools without plugin skills. Do not use it alongside the older built-in plugin. If that plugin is enabled, run /plugin disable computer-use first.", + connectRuntime: "The app includes Node; no separate Node install is needed. If installed elsewhere, set APP to the actual app bundle path.", + connectReload: "In a running terminal session, run /mcp reload, switch to Operate, and ask:", + connectPrompt: "Check Computer Use permissions with request_access. Do not send input.", + connectExpected: "Check for via: app, the helper version and any missing permissions. If the helper is missing, stop and follow the setup hint. This checks the connection and permissions only.", + connectGuide: "MCP setup guide", + contributeTitle: "Help improve Computer Use", + contributeBody: "Improve the docs or test an app workflow and share reproducible results.", + contributeGuide: "Contribution guide", + contributeIssue: "Report an issue", }; diff --git a/web/lib/i18n/dictionaries/en/docs-configuration.ts b/web/lib/i18n/dictionaries/en/docs-configuration.ts index 2a2a8e7fb..9c1c33b35 100644 --- a/web/lib/i18n/dictionaries/en/docs-configuration.ts +++ b/web/lib/i18n/dictionaries/en/docs-configuration.ts @@ -81,7 +81,7 @@ codewhale config doctor # unknown keys, empty secrets, malformed URLs`, codeTerms: true, }, { - p: "Keys, endpoints, provider choice, MCP servers, hooks, skills, and extra instruction files always come from your own config, so a cloned repository cannot point Codewhale at its own server or files. Start with `--no-project-config` to ignore a repository's file for one launch.", + p: "Provider keys, endpoints and provider choice come from your own config. MCP servers can also come from `.codewhale/mcp.json` after you trust the workspace in user-owned settings; project entries override same-named global entries. Review that executable authority before trusting a repository. See [MCP configuration](/docs/mcp#config).", }, { p: "Project instructions — how an agent should work in this repository — belong in `AGENTS.md` instead. Run `/init` to create one.", diff --git a/web/lib/i18n/dictionaries/en/docs-mcp.ts b/web/lib/i18n/dictionaries/en/docs-mcp.ts index 0d678a55f..e60bf2bb5 100644 --- a/web/lib/i18n/dictionaries/en/docs-mcp.ts +++ b/web/lib/i18n/dictionaries/en/docs-mcp.ts @@ -10,31 +10,55 @@ import type { DocsMcpDict } from "../types"; export const docsMcp: DocsMcpDict = { metaTitle: "Connect tools with MCP · Codewhale Docs", metaDescription: - "Add Model Context Protocol servers so Codewhale can use more tools, sign in to remote servers, run Codewhale itself as an MCP server, and try code mode.", + "Connect local and remote MCP servers, understand authentication and plugin compatibility, and troubleshoot missing tools.", bodyClassName: "text-ink-soft leading-relaxed", title: "Connect tools with MCP", lede: - "MCP servers give Codewhale more tools — a database, an issue tracker, a browser. Add a local server that Codewhale starts for you, or a remote server by URL. Its tools then go through the same approvals as built-in ones.", + "Connect a local tool server or a remote service through Model Context Protocol. Codewhale discovers its tools and applies your tool permissions and approvals when you use them.", sections: [ { id: "add", - title: "Add a server", + title: "Try the Cloudflare Docs server", blocks: [ { - code: `codewhale mcp add git --command "uvx" --arg "mcp-server-git" -codewhale mcp add docs --url "https://example.com/mcp" -codewhale mcp list -codewhale mcp validate`, + p: "The public [Cloudflare Documentation server](https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/) provides documentation search and a Pages-to-Workers migration guide. [View its official source](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/docs-vectorize). Add it without credentials:", + }, + { + code: `codewhale mcp add cloudflare-docs --url "https://docs.mcp.cloudflare.com/mcp" +codewhale mcp tools cloudflare-docs`, lang: "Terminal", }, { - p: "`--command` starts a local server over stdio; repeat `--arg` for each argument. `--url` connects to a remote server over Streamable HTTP, with legacy SSE as a fallback. `mcp validate` checks the config and the servers you require.", + p: "The tool list should include `search_cloudflare_documentation` and `migrate_pages_to_workers_guide`. In an existing TUI session, run `/mcp reload` to load the new configuration into that session.", + }, + { + note: "Checked on November 7, 2026 with published CLI v0.10.2: both commands exited successfully and listed those two tools without credentials. This checked setup and discovery, not tool execution. The [released guide](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MCP.md) describes that version; the [main source guide](https://github.com/codewhale-hq/Codewhale/blob/main/docs/MCP.md) may include unreleased changes.", }, { p: "Inside a session, `/mcp` opens the MCP manager: each server's state, transport, timeouts, errors, and discovered tools. The same actions are available there, for example `/mcp add stdio ` and `/mcp add http `.", }, { - note: "An MCP server runs with your permissions. Add only servers you trust, as you would any program you install.", + note: "Local MCP servers run with the permissions of the Codewhale process; remote calls use the access you grant that service. Add only servers you trust.", + }, + ], + }, + { + id: "compatibility", + title: "What can connect", + blocks: [ + { + rows: [ + ["Local stdio", "Codewhale starts the executable you configure with `--command`; repeat `--arg` for its arguments. Install the server and its required runtime first."], + ["Remote HTTP / SSE", "Use `--url` for Streamable HTTP, with legacy SSE fallback. A legacy SSE endpoint can use `--transport sse`. A remote entry does not need `command`."], + ["Direct remote authentication", "Your own MCP configuration can use environment-backed headers, bearer tokens or OAuth. OAuth needs compatible server metadata and client registration."], + ["Plugin-contributed MCP", "A reviewed, enabled plugin can supply stdio or remote MCP servers. Plugin MCP supports environment-backed authentication, but plugin-contributed MCP OAuth is unsupported."], + ["Tools, resources and prompts", "Codewhale discovers what the server actually advertises. Resources, templates and prompts are available when the server implements them."], + ["Optional extensions and MCP Apps", "General MCP Apps UI support is not available. Basic MCP transport support does not guarantee support for every optional extension or server."], + ["Server requirements", "Each server keeps its own OS, executable, browser, dependency and account requirements. A local server runs on the computer hosting Codewhale; Computer Use has its own platform limits."], + ], + }, + { + p: "A direct MCP entry does not require a plugin. [Plugins](/plugins) can package MCP tools with instructions and other components; each component still needs compatible dependencies, review and permissions.", }, ], }, @@ -43,13 +67,16 @@ codewhale mcp validate`, title: "Sign in to a remote server", blocks: [ { - p: "For a server that uses OAuth, add it by URL and log in. For a bearer token, keep the token in an environment variable instead of the config file:", + p: "For a directly configured OAuth server, add its real URL, then log in using the name you gave it:", }, { - code: `codewhale mcp login docs -codewhale mcp add tracker --url "https://example.com/mcp" --bearer-token-env-var TRACKER_TOKEN`, + code: `codewhale mcp login +codewhale mcp logout `, lang: "Terminal", }, + { + p: "For a bearer token, use `--bearer-token-env-var TOKEN_NAME` when adding the server, and set that variable in the environment launching Codewhale. `env_headers` maps header names to environment variable names. Literal `headers` values do not expand `${TOKEN_NAME}`. Plugin MCP entries cannot use OAuth; configure that server directly instead.", + }, { p: "An explicit Authorization header always wins: headers from config apply first, then the bearer-token variable, then a stored OAuth login. `codewhale mcp logout ` removes the stored login on this machine; the provider may keep its own grant until you revoke it there.", }, @@ -60,23 +87,26 @@ codewhale mcp add tracker --url "https://example.com/mcp" --bearer-token-env-var title: "Edit the config file", blocks: [ { - p: "Servers live in `~/.codewhale/mcp.json`. `codewhale mcp init` writes a starter file. The `mcpServers` key used by other clients works too, so you can paste an existing entry.", + p: "Global servers live in `~/.codewhale/mcp.json`; the legacy `~/.deepseek/mcp.json` is read when the Codewhale file is absent. `codewhale mcp init` writes a starter file. Use an absolute `mcp_config_path` or `CODEWHALE_MCP_CONFIG` to override it. The environment also accepts `DEEPSEEK_MCP_CONFIG`; the Codewhale name wins when both are set.", }, { code: `{ "servers": { - "example": { - "command": "node", - "args": ["./path/to/your-mcp-server.js"], - "env": {}, - "disabled": false + "cloudflare-docs": { + "url": "https://docs.mcp.cloudflare.com/mcp" } } }`, lang: "mcp.json", }, { - p: "After editing the file, run `/mcp reload` in the session; no restart is needed. A server starts only when a turn needs one of its tools, unless you mark it `\"required\": true` to connect at startup.", + p: "Use either `servers` or its `mcpServers` alias, never both in one file. `env_http_headers` is an alias for `env_headers`. Adapt the server's documented command, arguments and auth fields rather than assuming every client's configuration format is interchangeable.", + }, + { + p: "A trusted workspace can add `.codewhale/mcp.json`. Its entries override same-named global servers; the repository cannot grant itself that trust or override your global config path. Explicit global or trusted-project entries win over a colliding plugin server identity.", + }, + { + p: "After editing, run `/mcp reload` in the TUI; no restart is needed. Enabled, allowed servers connect at session start by default. Set `\"lazy\": true` on a user-configured server to connect on demand; `\"required\": true` still connects it at startup. Plugin MCP servers have no lazy manifest option. Headless runtimes need a restart after config changes.", }, ], }, @@ -85,10 +115,26 @@ codewhale mcp add tracker --url "https://example.com/mcp" --bearer-token-env-var title: "Find the tools", blocks: [ { - p: "Each tool appears to the model as `mcp__`: a server named `git` with a `status` tool becomes `mcp_git_status`. `codewhale mcp tools ` lists what a server offers. A server that fails to connect or is disabled never shows up as an available tool.", + p: "Each tool appears to the model as `mcp__`. For this example, documentation search is `mcp_cloudflare-docs_search_cloudflare_documentation`. `codewhale mcp tools ` lists discovered tools in its own process; it does not attach those connections to a running session.", }, { - p: "MCP tools follow your [approval setting](/docs/modes): listing and reading a server's resources and prompts can run without a prompt when policy allows, and tools with side effects ask first. Full Access does not override repository rules or managed policy.", + p: "MCP tools follow your [approval setting](/docs/modes), allow/deny rules and network policy. Whether a call asks first depends on that policy and posture. Full Access does not override repository rules or managed policy.", + }, + ], + }, + { + id: "troubleshooting", + title: "When tools do not appear", + blocks: [ + { + rows: [ + ["Check configuration", "Run `codewhale doctor` for static path and configuration diagnostics. Shell `codewhale mcp validate` connects enabled, allowed servers in a separate process; it is a live check."], + ["Refresh this session", "TUI `/mcp validate` shows its last snapshot without reconnecting. Use `/mcp reload` after config changes, or `/mcp retry ` for one server."], + ["Connected, but missing a tool", "Check `codewhale mcp tools `, the server's advertised tools, `enabled_tools` / `disabled_tools`, and the session's allow/deny rules. A connected transport alone does not mean a tool is available or permitted."], + ["Authentication required", "For a direct OAuth server, run `/mcp login `. For token auth, check that the variable is present in Codewhale's launch environment. A successful login in one process does not attach its transport to another."], + ["Local command fails", "Check the server's executable, arguments, working directory and runtime. On Windows, package runners may need their `.cmd` name. Follow the server vendor's OS and dependency instructions."], + ["Plugin server is absent", "Check the owning plugin's review, trust and enabled state. MCP settings cannot activate an untrusted plugin or bypass a rejected manifest."], + ], }, ], }, @@ -97,7 +143,7 @@ codewhale mcp add tracker --url "https://example.com/mcp" --bearer-token-env-var title: "Run Codewhale as an MCP server", blocks: [ { - p: "Other MCP clients can use Codewhale as a tool server. By default, it exposes only read-only `file_read` and `search` tools.", + p: "A client supporting Codewhale's advertised MCP protocol revisions can launch it as a stdio tool server. By default, it exposes only read-only `file_read` and `search` tools.", }, { code: `codewhale mcp add-self @@ -105,7 +151,7 @@ codewhale mcp tools codewhale`, lang: "Terminal", }, { - p: "`add-self` registers `codewhale serve --mcp` over stdio; `codewhale mcp-server` is the equivalent entry point. Write tools stay withheld unless the operator explicitly permits them in server configuration. [Runtime API](/docs/runtime-api) HTTP is a separate transport.", + p: "`add-self` registers `codewhale serve --mcp`; `codewhale mcp-server` is the equivalent entry point. This server reads its separate `[server]` settings from the legacy `~/.deepseek/mcp_server.toml`, not the client `mcp.json`. Write or shell tools require explicit exposure and `require_approval = false`; there is no interactive approval channel. [Runtime API](/docs/runtime-api) HTTP is separate from MCP.", }, ], }, @@ -113,6 +159,9 @@ codewhale mcp tools codewhale`, id: "code-mode", title: "Compose tool calls with code mode (experimental)", blocks: [ + { + note: "This section follows the main candidate's source and can include behavior newer than published v0.10.2.", + }, { p: "Code mode lets the model write one short JavaScript program that calls several tools, loops, and filters results, instead of making each call as a separate step. Only the program's final value goes back to the model, which keeps long lookups compact. It is on by default — `execute_tools` is advertised from the first request — and MCP and plugin tools compose inside a program the same way built-in ones do. Turning it off does not remove code mode: `execute_tools` goes back to being deferred, and the model can still find it through `tool_search`. To turn it off for one session, or in config:", }, diff --git a/web/lib/i18n/dictionaries/es/computer-use.ts b/web/lib/i18n/dictionaries/es/computer-use.ts index b8301a292..8995014a5 100644 --- a/web/lib/i18n/dictionaries/es/computer-use.ts +++ b/web/lib/i18n/dictionaries/es/computer-use.ts @@ -28,7 +28,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "Instala la aplicación", body: "Abre la imagen de disco y arrastra Codewhale Computer Use a «Aplicaciones». Ábrela desde «Aplicaciones» y luego elige Computer Use en el icono de la ballena de la barra de menús." }, { title: "Revisa los permisos", body: "Usa los botones de configuración para abrir «Accesibilidad» y «Grabación de pantalla» en Ajustes del Sistema. Tú decides qué permisos conceder." }, - { title: "Conecta tu agente", body: "En Codewhale, instala Computer Use con /plugin marketplace install codewhale computer-use y después revisa y activa el plugin. El asistente se instala por separado. Otros hosts MCP compatibles se conectan al asistente siguiendo la guía de configuración." }, + { title: "Conecta tu agente", body: "Elige a continuación la configuración de Codewhale para tu versión de la CLI. Otros clientes MCP compatibles pueden usar la guía de configuración del repositorio." }, ], controlsTitle: "Mantén el control", controlsBody: "Tras conceder los permisos de macOS, el modo estándar funciona sin pedir permiso para cada app. Las apps que bloquees siguen fuera de su alcance; Pause suspende el uso del ordenador y Stop computer use termina las sesiones actuales. La aprobación estricta es opcional. Pide una vista previa en directo cuando quieras ver lo que hace.", @@ -37,6 +37,18 @@ export const computerUse: ComputerUseDict = { help: "Configuración y solución de problemas", notes: "Notas de la versión", demo: "Ver la comprobación en segundo plano", - source: "Código fuente y otras plataformas", + source: "Ver en GitHub", platforms: "El asistente para Mac está en beta y el plugin del catálogo solo admite Mac. Las versiones publicadas también incluyen una vista previa experimental sin firmar para Windows x64; Linux requiere una configuración experimental desde el código fuente o con Docker. La entrada en Windows y Linux utiliza el escritorio compartido.", + connectTitle: "Conecta Codewhale", + connectVersion: "Codewhale 0.10.2 incluye un plugin Computer Use antiguo. Instalar la entrada del catálogo no lo actualiza. Conecta directamente el asistente de Mac instalado mediante MCP.", + connectLimit: "La conexión MCP directa añade herramientas sin las habilidades del plugin. No la uses junto con el plugin integrado antiguo. Si está activado, ejecuta primero /plugin disable computer-use.", + connectRuntime: "La aplicación incluye Node; no hace falta instalarlo por separado. Si está instalada en otra ubicación, asigna a APP la ruta real del paquete de la aplicación.", + connectReload: "En una sesión de terminal en ejecución, ejecuta /mcp reload, cambia a Operate y pide:", + connectPrompt: "Comprueba los permisos de Computer Use con request_access. No envíes ninguna entrada.", + connectExpected: "Comprueba que la respuesta indique via: app, la versión del asistente y los permisos que falten. Si no se encuentra el asistente, detente y sigue la indicación de configuración. Esto solo comprueba la conexión y los permisos.", + connectGuide: "Guía de configuración de MCP", + contributeTitle: "Ayuda a mejorar Computer Use", + contributeBody: "Mejora la documentación o prueba un flujo de trabajo en una aplicación y comparte resultados reproducibles.", + contributeGuide: "Guía de contribución", + contributeIssue: "Informar de un problema", }; diff --git a/web/lib/i18n/dictionaries/fr/computer-use.ts b/web/lib/i18n/dictionaries/fr/computer-use.ts index ea2d80219..ce7fd257f 100644 --- a/web/lib/i18n/dictionaries/fr/computer-use.ts +++ b/web/lib/i18n/dictionaries/fr/computer-use.ts @@ -27,7 +27,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "Installer l’app", body: "Ouvrez l’image disque et faites glisser Codewhale Computer Use dans « Applications ». Lancez-la depuis « Applications », puis choisissez Computer Use dans l’icône de baleine de votre barre des menus." }, { title: "Vérifier les autorisations", body: "Les boutons de configuration ouvrent « Accessibilité » et « Enregistrement de l’écran » dans Réglages Système. C’est vous qui décidez des autorisations à accorder." }, - { title: "Connecter votre agent", body: "Dans Codewhale, installez Computer Use avec /plugin marketplace install codewhale computer-use, puis examinez et activez le plugin. L’assistant s’installe séparément. Les autres hôtes MCP compatibles s’y connectent en suivant le guide de configuration." }, + { title: "Connecter votre agent", body: "Choisissez ci-dessous la configuration de Codewhale adaptée à votre version de la CLI. Les autres clients MCP compatibles utilisent le guide de configuration du dépôt." }, ], controlsTitle: "Gardez la main", controlsBody: "Après l’octroi des autorisations macOS, le mode standard fonctionne sans demander une autorisation pour chaque app. Les apps que vous bloquez restent inaccessibles ; Pause suspend l’utilisation de l’ordinateur et Stop computer use termine les sessions en cours. L’approbation stricte est facultative. Demandez un aperçu en direct pour suivre les opérations.", @@ -36,6 +36,18 @@ export const computerUse: ComputerUseDict = { help: "Configuration et dépannage", notes: "Notes de version", demo: "Voir la vérification en arrière-plan", - source: "Code source et autres plateformes", + source: "Voir sur GitHub", platforms: "L’assistant Mac est en bêta et le plugin du catalogue ne prend en charge que Mac. Les versions publiées comprennent aussi un aperçu expérimental non signé pour Windows x64 ; Linux nécessite une configuration expérimentale depuis les sources ou avec Docker. Les entrées sous Windows et Linux utilisent le bureau partagé.", + connectTitle: "Connecter Codewhale", + connectVersion: "Codewhale 0.10.2 intègre une ancienne version du plugin Computer Use. Installer son entrée dans le catalogue ne la met pas à jour. Connectez directement l’application auxiliaire Mac installée via MCP.", + connectLimit: "La connexion MCP directe ajoute des outils sans les compétences du plugin. Ne l’utilisez pas avec l’ancien plugin intégré. S’il est activé, exécutez d’abord /plugin disable computer-use.", + connectRuntime: "L’application inclut Node ; aucune installation séparée n’est nécessaire. Si elle est installée ailleurs, attribuez à APP le chemin réel du paquet de l’application.", + connectReload: "Dans une session de terminal en cours, exécutez /mcp reload, passez à Operate, puis demandez :", + connectPrompt: "Vérifie les autorisations de Computer Use avec request_access. N’envoie aucune saisie.", + connectExpected: "Vérifiez via: app, la version de l’application auxiliaire et les autorisations manquantes dans la réponse. Si l’application auxiliaire est absente, arrêtez-vous et suivez l’indication de configuration. Ce test vérifie uniquement la connexion et les autorisations.", + connectGuide: "Guide de configuration MCP", + contributeTitle: "Contribuez à améliorer Computer Use", + contributeBody: "Améliorez la documentation ou testez un scénario dans une application et partagez des résultats reproductibles.", + contributeGuide: "Guide de contribution", + contributeIssue: "Signaler un problème", }; diff --git a/web/lib/i18n/dictionaries/hi/computer-use.ts b/web/lib/i18n/dictionaries/hi/computer-use.ts index b889c169f..3ebfc0b9f 100644 --- a/web/lib/i18n/dictionaries/hi/computer-use.ts +++ b/web/lib/i18n/dictionaries/hi/computer-use.ts @@ -26,7 +26,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "ऐप इंस्टॉल करें", body: "डिस्क इमेज खोलें और Codewhale Computer Use को “ऐप्लिकेशन” में खींचकर छोड़ें। इसे “ऐप्लिकेशन” से खोलें, फिर मेनू बार में व्हेल के आइकन से Computer Use चुनें।" }, { title: "अनुमतियों की समीक्षा करें", body: "सेटअप बटनों से सिस्टम सेटिंग्ज़ में “ऐक्सेसिबिलिटी” और “स्क्रीन रिकॉर्डिंग” खोलें। कौन-सी अनुमतियाँ देनी हैं, यह आप तय करते हैं।" }, - { title: "अपना एजेंट कनेक्ट करें", body: "Codewhale में /plugin marketplace install codewhale computer-use से Computer Use इंस्टॉल करें, फिर प्लगइन की समीक्षा करके उसे सक्षम करें। हेल्पर अलग से इंस्टॉल होता है। अन्य संगत MCP होस्ट सेटअप गाइड के अनुसार उससे जुड़ते हैं।" }, + { title: "अपना एजेंट कनेक्ट करें", body: "अपने CLI संस्करण के लिए नीचे दिया गया Codewhale सेटअप चुनें। अन्य संगत MCP होस्ट के लिए रिपॉज़िटरी की सेटअप मार्गदर्शिका देखें।" }, ], controlsTitle: "नियंत्रण आपके हाथ में", controlsBody: "macOS की अनुमतियाँ देने के बाद मानक मोड हर ऐप के लिए अनुमति माँगे बिना काम करता है। आपके प्रतिबंधित ऐप पहुँच से बाहर रहते हैं; Pause कंप्यूटर का इस्तेमाल रोकता है और Stop computer use मौजूदा सेशन समाप्त करता है। सख्त स्वीकृति मोड वैकल्पिक है। काम देखना चाहें, तो लाइव प्रीव्यू माँगें।", @@ -35,6 +35,18 @@ export const computerUse: ComputerUseDict = { help: "सेटअप और समस्या-समाधान", notes: "रिलीज़ नोट्स", demo: "बैकग्राउंड जाँच देखें", - source: "सोर्स और अन्य प्लेटफ़ॉर्म", + source: "GitHub पर देखें", platforms: "Mac हेल्पर बीटा में है और मार्केटप्लेस प्लगइन केवल Mac का समर्थन करता है। प्रकाशित संस्करणों में Windows x64 का बिना हस्ताक्षर वाला प्रयोगात्मक प्रीव्यू भी है; Linux के लिए सोर्स या Docker से प्रयोगात्मक सेटअप चाहिए। Windows और Linux पर इनपुट साझा डेस्कटॉप का उपयोग करता है।", + connectTitle: "Codewhale कनेक्ट करें", + connectVersion: "Codewhale 0.10.2 में Computer Use का पुराना प्लगइन शामिल है। कैटलॉग की प्रविष्टि इंस्टॉल करने से वह अपडेट नहीं होता। इंस्टॉल किए गए Mac सहायक से सीधे MCP के ज़रिए कनेक्ट करें।", + connectLimit: "सीधा MCP कनेक्शन टूल जोड़ता है, प्लगइन स्किल नहीं। इसे पुराने अंतर्निर्मित प्लगइन के साथ न चलाएँ। यदि वह सक्षम है, तो पहले /plugin disable computer-use चलाएँ।", + connectRuntime: "ऐप में Node शामिल है; उसे अलग से इंस्टॉल करने की ज़रूरत नहीं है। यदि ऐप कहीं और इंस्टॉल है, तो APP में ऐप बंडल का वास्तविक पथ दें।", + connectReload: "चल रहे टर्मिनल सत्र में /mcp reload चलाएँ, Operate पर जाएँ और यह कहें:", + connectPrompt: "request_access से Computer Use की अनुमतियाँ जाँचें। कोई इनपुट न भेजें।", + connectExpected: "जवाब में via: app, सहायक का संस्करण और अनुपलब्ध अनुमतियाँ देखें। यदि सहायक नहीं मिलता, तो रुकें और सेटअप संकेत का पालन करें। इससे केवल कनेक्शन और अनुमतियाँ जाँची जाती हैं।", + connectGuide: "MCP सेटअप मार्गदर्शिका", + contributeTitle: "Computer Use को बेहतर बनाने में मदद करें", + contributeBody: "दस्तावेज़ सुधारें या किसी ऐप की कार्यप्रणाली जाँचें और दोहराए जा सकने वाले परिणाम साझा करें।", + contributeGuide: "योगदान मार्गदर्शिका", + contributeIssue: "समस्या रिपोर्ट करें", }; diff --git a/web/lib/i18n/dictionaries/id/computer-use.ts b/web/lib/i18n/dictionaries/id/computer-use.ts index 17770d990..76b5b78ec 100644 --- a/web/lib/i18n/dictionaries/id/computer-use.ts +++ b/web/lib/i18n/dictionaries/id/computer-use.ts @@ -26,7 +26,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "Instal aplikasi", body: "Buka disk image, lalu seret Codewhale Computer Use ke folder “Aplikasi”. Buka aplikasinya dari “Aplikasi”, kemudian pilih Computer Use dari ikon paus di bar menu Anda." }, { title: "Tinjau izin", body: "Gunakan tombol penyiapan untuk membuka “Aksesibilitas” dan “Perekaman Layar” di Pengaturan Sistem. Anda yang menentukan izin mana yang diberikan." }, - { title: "Hubungkan agen Anda", body: "Di Codewhale, instal Computer Use dengan /plugin marketplace install codewhale computer-use, lalu tinjau dan aktifkan plugin. Aplikasi pembantu diinstal secara terpisah. Host MCP lain yang kompatibel menghubungkannya melalui panduan penyiapan." }, + { title: "Hubungkan agen Anda", body: "Pilih pengaturan Codewhale di bawah yang sesuai dengan versi CLI Anda. Host MCP lain yang kompatibel dapat mengikuti panduan pengaturan di repositori." }, ], controlsTitle: "Kendali tetap di tangan Anda", controlsBody: "Setelah izin macOS diberikan, mode standar bekerja tanpa meminta izin untuk setiap aplikasi. Aplikasi yang Anda blokir tetap tidak dapat diakses; Pause menjeda penggunaan komputer dan Stop computer use mengakhiri sesi saat ini. Persetujuan ketat bersifat opsional. Minta pratinjau langsung saat Anda ingin melihat prosesnya.", @@ -35,6 +35,18 @@ export const computerUse: ComputerUseDict = { help: "Penyiapan dan pemecahan masalah", notes: "Catatan rilis", demo: "Lihat pemeriksaan latar belakang", - source: "Kode sumber dan platform lain", + source: "Lihat di GitHub", platforms: "Aplikasi pembantu Mac masih beta dan plugin katalog hanya mendukung Mac. Rilis yang diterbitkan juga menyediakan pratinjau eksperimental tanpa tanda tangan untuk Windows x64; Linux memerlukan penyiapan eksperimental dari kode sumber atau Docker. Input pada Windows dan Linux menggunakan desktop bersama.", + connectTitle: "Hubungkan Codewhale", + connectVersion: "Codewhale 0.10.2 menyertakan plugin Computer Use versi lama. Memasang entri katalog tidak memperbaruinya. Hubungkan langsung aplikasi pembantu Mac yang terpasang melalui MCP.", + connectLimit: "Koneksi MCP langsung menambahkan alat tanpa keterampilan plugin. Jangan gunakan bersamaan dengan plugin bawaan lama. Jika plugin itu aktif, jalankan /plugin disable computer-use terlebih dahulu.", + connectRuntime: "Aplikasi sudah menyertakan Node; tidak perlu pemasangan terpisah. Jika terpasang di lokasi lain, isi APP dengan jalur bundel aplikasi yang sebenarnya.", + connectReload: "Dalam sesi terminal yang sedang berjalan, jalankan /mcp reload, beralih ke Operate, lalu minta:", + connectPrompt: "Periksa izin Computer Use dengan request_access. Jangan kirim input.", + connectExpected: "Periksa via: app, versi aplikasi pembantu, dan izin yang belum diberikan dalam respons. Jika aplikasi pembantu tidak ditemukan, berhenti dan ikuti petunjuk pengaturan. Ini hanya memeriksa koneksi dan izin.", + connectGuide: "Panduan pengaturan MCP", + contributeTitle: "Bantu tingkatkan Computer Use", + contributeBody: "Perbaiki dokumentasi atau uji alur kerja aplikasi dan bagikan hasil yang dapat direproduksi.", + contributeGuide: "Panduan kontribusi", + contributeIssue: "Laporkan masalah", }; diff --git a/web/lib/i18n/dictionaries/it/computer-use.ts b/web/lib/i18n/dictionaries/it/computer-use.ts index f32caa4a4..c45050095 100644 --- a/web/lib/i18n/dictionaries/it/computer-use.ts +++ b/web/lib/i18n/dictionaries/it/computer-use.ts @@ -26,7 +26,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "Installa l’app", body: "Apri l’immagine disco e trascina Codewhale Computer Use in “Applicazioni”. Aprila da “Applicazioni”, poi scegli Computer Use dall’icona della balena nella barra dei menu." }, { title: "Controlla i permessi", body: "Usa i pulsanti di configurazione per aprire “Accessibilità” e “Registrazione schermo” nelle Impostazioni di Sistema. Decidi tu quali permessi concedere." }, - { title: "Collega il tuo agente", body: "In Codewhale, installa Computer Use con /plugin marketplace install codewhale computer-use, poi esamina e attiva il plugin. L’helper si installa separatamente. Gli altri host MCP compatibili si collegano all’helper seguendo la guida di configurazione." }, + { title: "Collega il tuo agente", body: "Scegli qui sotto la configurazione di Codewhale per la tua versione della CLI. Gli altri client MCP compatibili possono usare la guida di configurazione del repository." }, ], controlsTitle: "Mantieni il controllo", controlsBody: "Dopo aver concesso i permessi macOS, la modalità standard funziona senza chiedere l’autorizzazione per ogni app. Le app che blocchi restano inaccessibili; Pause sospende l’uso del computer e Stop computer use termina le sessioni attuali. L’approvazione rigorosa è facoltativa. Chiedi un’anteprima dal vivo per seguire le operazioni.", @@ -35,6 +35,18 @@ export const computerUse: ComputerUseDict = { help: "Configurazione e risoluzione dei problemi", notes: "Note di rilascio", demo: "Guarda il test in background", - source: "Codice sorgente e altre piattaforme", + source: "Visualizza su GitHub", platforms: "L’helper per Mac è in beta e il plugin del catalogo supporta solo Mac. Le versioni pubblicate includono anche un’anteprima sperimentale non firmata per Windows x64; Linux richiede una configurazione sperimentale dai sorgenti o con Docker. L’input su Windows e Linux usa il desktop condiviso.", + connectTitle: "Collega Codewhale", + connectVersion: "Codewhale 0.10.2 include una vecchia versione del plugin Computer Use. Installare la voce del catalogo non lo aggiorna. Collega direttamente l’app di supporto Mac installata tramite MCP.", + connectLimit: "La connessione MCP diretta aggiunge strumenti senza le skill del plugin. Non usarla insieme al vecchio plugin integrato. Se è attivo, esegui prima /plugin disable computer-use.", + connectRuntime: "L’app include Node; non serve installarlo separatamente. Se è installata altrove, imposta APP sul percorso effettivo del bundle dell’app.", + connectReload: "In una sessione del terminale in esecuzione, esegui /mcp reload, passa a Operate e chiedi:", + connectPrompt: "Controlla le autorizzazioni di Computer Use con request_access. Non inviare alcun input.", + connectExpected: "Controlla via: app, la versione dell’app di supporto e le autorizzazioni mancanti nella risposta. Se l’app di supporto non è presente, fermati e segui le indicazioni di configurazione. Questo verifica solo la connessione e le autorizzazioni.", + connectGuide: "Guida alla configurazione MCP", + contributeTitle: "Aiuta a migliorare Computer Use", + contributeBody: "Migliora la documentazione o prova un flusso di lavoro in un’app e condividi risultati riproducibili.", + contributeGuide: "Guida ai contributi", + contributeIssue: "Segnala un problema", }; diff --git a/web/lib/i18n/dictionaries/ja/computer-use.ts b/web/lib/i18n/dictionaries/ja/computer-use.ts index bdb7a57ad..fe92937c1 100644 --- a/web/lib/i18n/dictionaries/ja/computer-use.ts +++ b/web/lib/i18n/dictionaries/ja/computer-use.ts @@ -27,7 +27,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "アプリをインストールする", body: "ディスクイメージを開き、Codewhale Computer Use を「アプリケーション」フォルダにドラッグします。「アプリケーション」から起動し、メニューバーのクジラのアイコンから Computer Use を選択してください。" }, { title: "権限を確認する", body: "セットアップのボタンから、システム設定の「アクセシビリティ」と「画面収録」を開きます。どの権限を許可するかはあなたが決めます。" }, - { title: "エージェントを接続する", body: "Codewhale で /plugin marketplace install codewhale computer-use を実行し、プラグインを確認して有効にします。ヘルパーは別途インストールします。ほかの対応 MCP ホストは、セットアップガイドに従ってヘルパーに接続します。" }, + { title: "エージェントを接続する", body: "CLI のバージョンに合う Codewhale の設定を下から選んでください。ほかの対応 MCP ホストでは、リポジトリの設定ガイドを参照してください。" }, ], controlsTitle: "いつでも操作を管理", controlsBody: "macOS の権限を許可すると、標準モードではアプリごとの確認なしで動作します。拒否したアプリにはアクセスできません。Pause は操作を一時停止し、Stop computer use は現在のセッションを終了します。厳格な承認モードも選べます。作業を見たいときはライブプレビューを依頼してください。", @@ -36,6 +36,18 @@ export const computerUse: ComputerUseDict = { help: "セットアップとトラブルシューティング", notes: "リリースノート", demo: "バックグラウンドチェックを見る", - source: "ソースコードとその他のプラットフォーム", + source: "GitHub で見る", platforms: "Mac ヘルパーはベータ版で、マーケットプレイスのプラグインは Mac のみに対応しています。公開リリースには署名のない実験的な Windows x64 プレビューもあります。Linux はソースまたは Docker による実験的なセットアップが必要です。Windows と Linux の入力は共有デスクトップを使用します。", + connectTitle: "Codewhale に接続", + connectVersion: "Codewhale 0.10.2 には旧版の Computer Use プラグインが内蔵されています。カタログからインストールしても更新されません。インストール済みの Mac ヘルパーに MCP で直接接続してください。", + connectLimit: "MCP での直接接続はツールを追加しますが、プラグインのスキルは含みません。旧版の内蔵プラグインと併用しないでください。有効なら、先に /plugin disable computer-use を実行してください。", + connectRuntime: "アプリには Node が含まれるため、別途インストールする必要はありません。別の場所にインストールした場合は、APP を実際のアプリバンドルのパスに設定してください。", + connectReload: "実行中のターミナルセッションで /mcp reload を実行し、Operate に切り替えて次のように依頼します:", + connectPrompt: "request_access で Computer Use の権限を確認してください。入力操作は行わないでください。", + connectExpected: "応答の via: app、ヘルパーのバージョン、不足している権限を確認してください。ヘルパーが見つからない場合は停止し、設定の案内に従ってください。これは接続と権限のみの確認です。", + connectGuide: "MCP 設定ガイド", + contributeTitle: "Computer Use の改善に参加", + contributeBody: "ドキュメントの改善や、アプリの操作手順のテストと再現可能な結果の共有で協力できます。", + contributeGuide: "貢献ガイド", + contributeIssue: "問題を報告", }; diff --git a/web/lib/i18n/dictionaries/ko/computer-use.ts b/web/lib/i18n/dictionaries/ko/computer-use.ts index ced3b4b13..4af2476e1 100644 --- a/web/lib/i18n/dictionaries/ko/computer-use.ts +++ b/web/lib/i18n/dictionaries/ko/computer-use.ts @@ -27,7 +27,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "앱 설치", body: "디스크 이미지를 열고 Codewhale Computer Use를 “응용 프로그램”으로 드래그하세요. “응용 프로그램”에서 앱을 연 다음 메뉴 막대의 고래 아이콘에서 Computer Use를 선택합니다." }, { title: "권한 검토", body: "설정 버튼으로 시스템 설정의 “손쉬운 사용”과 “화면 기록”을 여세요. 어떤 권한을 허용할지는 당신이 결정합니다." }, - { title: "에이전트 연결", body: "Codewhale에서 /plugin marketplace install codewhale computer-use로 Computer Use를 설치한 뒤 플러그인을 검토하고 활성화하세요. 도우미는 별도로 설치합니다. 다른 호환 MCP 호스트는 설정 가이드에 따라 도우미에 연결합니다." }, + { title: "에이전트 연결", body: "CLI 버전에 맞는 Codewhale 설정을 아래에서 선택하세요. 다른 호환 MCP 호스트는 저장소의 설정 안내를 따르세요." }, ], controlsTitle: "언제든 직접 제어", controlsBody: "macOS 권한을 허용하면 표준 모드는 앱마다 허용 여부를 묻지 않고 작동합니다. 차단한 앱에는 접근할 수 없습니다. Pause는 컴퓨터 사용을 일시 중지하고 Stop computer use는 현재 세션을 종료합니다. 엄격한 승인 모드도 선택할 수 있습니다. 작업을 지켜보고 싶을 때 실시간 미리보기를 요청하세요.", @@ -36,6 +36,18 @@ export const computerUse: ComputerUseDict = { help: "설정 및 문제 해결", notes: "릴리스 노트", demo: "백그라운드 검사 보기", - source: "소스 및 다른 플랫폼", + source: "GitHub에서 보기", platforms: "Mac 도우미는 베타이며 마켓플레이스 플러그인은 Mac만 지원합니다. 공개 릴리스에는 서명되지 않은 실험용 Windows x64 프리뷰도 있습니다. Linux는 소스 또는 Docker를 통한 실험적 설정이 필요합니다. Windows와 Linux의 입력은 공유 데스크톱을 사용합니다.", + connectTitle: "Codewhale 연결", + connectVersion: "Codewhale 0.10.2에는 이전 Computer Use 플러그인이 내장되어 있습니다. 카탈로그 항목을 설치해도 이 플러그인은 업데이트되지 않습니다. 설치된 Mac 도우미에 MCP로 직접 연결하세요.", + connectLimit: "MCP 직접 연결은 도구만 추가하며 플러그인 스킬은 포함하지 않습니다. 이전 내장 플러그인과 함께 사용하지 마세요. 해당 플러그인이 켜져 있다면 먼저 /plugin disable computer-use를 실행하세요.", + connectRuntime: "앱에 Node가 포함되어 있어 별도 설치가 필요 없습니다. 다른 위치에 설치했다면 APP를 실제 앱 번들 경로로 설정하세요.", + connectReload: "실행 중인 터미널 세션에서 /mcp reload를 실행하고 Operate로 전환한 뒤 다음과 같이 요청하세요:", + connectPrompt: "request_access로 Computer Use 권한을 확인하세요. 입력은 보내지 마세요.", + connectExpected: "응답에서 via: app, 도우미 버전, 누락된 권한을 확인하세요. 도우미가 없다면 중단하고 설정 안내를 따르세요. 이 단계는 연결과 권한만 확인합니다.", + connectGuide: "MCP 설정 안내", + contributeTitle: "Computer Use 개선에 참여하기", + contributeBody: "문서를 개선하거나 앱 작업 흐름을 테스트하고 재현 가능한 결과를 공유해 주세요.", + contributeGuide: "기여 안내", + contributeIssue: "문제 신고", }; diff --git a/web/lib/i18n/dictionaries/pl/computer-use.ts b/web/lib/i18n/dictionaries/pl/computer-use.ts index 3f1f22640..01c25eaa4 100644 --- a/web/lib/i18n/dictionaries/pl/computer-use.ts +++ b/web/lib/i18n/dictionaries/pl/computer-use.ts @@ -26,7 +26,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "Zainstaluj aplikację", body: "Otwórz obraz dysku i przeciągnij Codewhale Computer Use do folderu „Programy”. Uruchom aplikację z folderu „Programy”, a następnie wybierz Computer Use z menu pod ikoną wieloryba w pasku menu." }, { title: "Sprawdź uprawnienia", body: "Przyciski konfiguracji otwierają sekcje „Dostępność” i „Nagrywanie ekranu” w Ustawieniach systemowych. To Ty decydujesz, które uprawnienia przyznać." }, - { title: "Połącz swojego agenta", body: "W Codewhale zainstaluj Computer Use poleceniem /plugin marketplace install codewhale computer-use, a następnie przejrzyj i włącz wtyczkę. Pomocnik jest instalowany osobno. Inne zgodne hosty MCP łączą się z nim zgodnie z instrukcją konfiguracji." }, + { title: "Połącz swojego agenta", body: "Wybierz poniżej konfigurację Codewhale dla swojej wersji CLI. Dla innych zgodnych klientów MCP skorzystaj z instrukcji konfiguracji w repozytorium." }, ], controlsTitle: "Zachowaj kontrolę", controlsBody: "Po przyznaniu uprawnień macOS tryb standardowy działa bez pytania o zgodę dla każdej aplikacji. Zablokowane przez Ciebie aplikacje pozostają niedostępne; Pause wstrzymuje korzystanie z komputera, a Stop computer use kończy bieżące sesje. Ścisłe zatwierdzanie jest opcjonalne. Poproś o podgląd na żywo, gdy chcesz śledzić pracę.", @@ -35,6 +35,18 @@ export const computerUse: ComputerUseDict = { help: "Konfiguracja i rozwiązywanie problemów", notes: "Informacje o wydaniu", demo: "Zobacz test w tle", - source: "Kod źródłowy i inne platformy", + source: "Zobacz na GitHubie", platforms: "Pomocnik na Maca jest w wersji beta, a wtyczka z katalogu obsługuje tylko Maca. Opublikowane wydania zawierają też niepodpisaną eksperymentalną wersję podglądową dla Windows x64; Linux wymaga eksperymentalnej konfiguracji ze źródeł lub przez Docker. Wprowadzanie danych w Windows i Linuksie korzysta ze wspólnego pulpitu.", + connectTitle: "Połącz Codewhale", + connectVersion: "Codewhale 0.10.2 zawiera starszą wtyczkę Computer Use. Instalacja pozycji z katalogu jej nie aktualizuje. Połącz zainstalowaną aplikację pomocniczą Mac bezpośrednio przez MCP.", + connectLimit: "Bezpośrednie połączenie MCP dodaje narzędzia bez umiejętności wtyczki. Nie używaj go równocześnie ze starszą wbudowaną wtyczką. Jeśli jest włączona, najpierw uruchom /plugin disable computer-use.", + connectRuntime: "Aplikacja zawiera Node; osobna instalacja nie jest potrzebna. Jeśli jest zainstalowana gdzie indziej, ustaw APP na rzeczywistą ścieżkę pakietu aplikacji.", + connectReload: "W działającej sesji terminala uruchom /mcp reload, przełącz na Operate i poproś:", + connectPrompt: "Sprawdź uprawnienia Computer Use za pomocą request_access. Nie wysyłaj żadnych danych wejściowych.", + connectExpected: "Sprawdź w odpowiedzi via: app, wersję aplikacji pomocniczej i brakujące uprawnienia. Jeśli aplikacji pomocniczej nie ma, zatrzymaj się i postępuj zgodnie ze wskazówką konfiguracji. To sprawdza tylko połączenie i uprawnienia.", + connectGuide: "Instrukcja konfiguracji MCP", + contributeTitle: "Pomóż ulepszyć Computer Use", + contributeBody: "Popraw dokumentację lub przetestuj przebieg pracy w aplikacji i podziel się powtarzalnymi wynikami.", + contributeGuide: "Przewodnik dla współtwórców", + contributeIssue: "Zgłoś problem", }; diff --git a/web/lib/i18n/dictionaries/pt-BR/computer-use.ts b/web/lib/i18n/dictionaries/pt-BR/computer-use.ts index 921b69fbd..1eb069788 100644 --- a/web/lib/i18n/dictionaries/pt-BR/computer-use.ts +++ b/web/lib/i18n/dictionaries/pt-BR/computer-use.ts @@ -26,7 +26,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "Instale o app", body: "Abra a imagem de disco e arraste o Codewhale Computer Use para a pasta Aplicativos. Abra-o a partir de Aplicativos e escolha Computer Use no ícone da baleia na barra de menus." }, { title: "Revise as permissões", body: "Use os botões de configuração para abrir Acessibilidade e Gravação de Tela nos Ajustes do Sistema. Você decide quais permissões conceder." }, - { title: "Conecte seu agente", body: "No Codewhale, instale o Computer Use com /plugin marketplace install codewhale computer-use, depois revise e ative o plugin. O assistente é instalado separadamente. Outros hosts MCP compatíveis se conectam a ele seguindo o guia de configuração." }, + { title: "Conecte seu agente", body: "Escolha abaixo a configuração do Codewhale para sua versão da CLI. Outros clientes MCP compatíveis usam o guia de configuração do repositório." }, ], controlsTitle: "Mantenha o controle", controlsBody: "Após conceder as permissões do macOS, o modo padrão funciona sem pedir autorização para cada app. Os apps que você bloquear continuam inacessíveis; Pause suspende o uso do computador e Stop computer use encerra as sessões atuais. A aprovação estrita é opcional. Peça uma prévia ao vivo quando quiser acompanhar o trabalho.", @@ -35,6 +35,18 @@ export const computerUse: ComputerUseDict = { help: "Configuração e solução de problemas", notes: "Notas de lançamento", demo: "Veja a verificação em segundo plano", - source: "Código-fonte e outras plataformas", + source: "Ver no GitHub", platforms: "O assistente para Mac está em beta e o plugin do catálogo oferece suporte apenas a Mac. As versões publicadas também incluem uma prévia experimental sem assinatura para Windows x64; o Linux exige uma configuração experimental pelo código-fonte ou com Docker. A entrada no Windows e no Linux usa a área de trabalho compartilhada.", + connectTitle: "Conecte o Codewhale", + connectVersion: "O Codewhale 0.10.2 inclui um plugin Computer Use antigo. Instalar a entrada do catálogo não o atualiza. Conecte o assistente de Mac instalado diretamente por MCP.", + connectLimit: "A conexão MCP direta adiciona ferramentas sem as habilidades do plugin. Não a use junto com o plugin integrado antigo. Se ele estiver ativado, execute /plugin disable computer-use primeiro.", + connectRuntime: "O aplicativo inclui Node; não é preciso instalá-lo separadamente. Se estiver instalado em outro local, defina APP com o caminho real do pacote do aplicativo.", + connectReload: "Em uma sessão de terminal em execução, execute /mcp reload, mude para Operate e peça:", + connectPrompt: "Verifique as permissões do Computer Use com request_access. Não envie nenhuma entrada.", + connectExpected: "Confira via: app, a versão do assistente e as permissões ausentes na resposta. Se o assistente não for encontrado, pare e siga a orientação de configuração. Isso verifica apenas a conexão e as permissões.", + connectGuide: "Guia de configuração do MCP", + contributeTitle: "Ajude a melhorar o Computer Use", + contributeBody: "Melhore a documentação ou teste um fluxo de trabalho em um aplicativo e compartilhe resultados reproduzíveis.", + contributeGuide: "Guia de contribuição", + contributeIssue: "Relatar um problema", }; diff --git a/web/lib/i18n/dictionaries/ru/computer-use.ts b/web/lib/i18n/dictionaries/ru/computer-use.ts index ba0b444b6..41dc950b8 100644 --- a/web/lib/i18n/dictionaries/ru/computer-use.ts +++ b/web/lib/i18n/dictionaries/ru/computer-use.ts @@ -27,7 +27,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "Установите приложение", body: "Откройте образ диска и перетащите Codewhale Computer Use в папку «Программы». Запустите его из папки «Программы», затем нажмите значок кита в строке меню и выберите Computer Use." }, { title: "Проверьте разрешения", body: "Кнопки настройки открывают разделы «Универсальный доступ» и «Запись экрана» в Системных настройках. Какие разрешения выдать — решаете вы." }, - { title: "Подключите агента", body: "В Codewhale установите Computer Use командой /plugin marketplace install codewhale computer-use, затем проверьте и включите плагин. Помощник устанавливается отдельно. Другие совместимые MCP-хосты подключаются к нему по инструкции настройки." }, + { title: "Подключите агента", body: "Выберите ниже настройку Codewhale для вашей версии CLI. Для других совместимых MCP-клиентов используйте руководство по настройке в репозитории." }, ], controlsTitle: "Контроль остаётся у вас", controlsBody: "После выдачи разрешений macOS стандартный режим работает без запроса для каждого приложения. Заблокированные вами приложения недоступны агенту; Pause приостанавливает работу с компьютером, а Stop computer use завершает текущие сеансы. Строгое подтверждение действий можно включить отдельно. Для наблюдения за работой запросите предпросмотр в реальном времени.", @@ -36,6 +36,18 @@ export const computerUse: ComputerUseDict = { help: "Настройка и устранение неполадок", notes: "Примечания к выпуску", demo: "Посмотреть фоновую проверку", - source: "Исходный код и другие платформы", + source: "Открыть на GitHub", platforms: "Помощник для Mac находится в бета-версии, а плагин каталога поддерживает только Mac. Опубликованные выпуски также содержат неподписанную экспериментальную предварительную версию для Windows x64; Linux требует экспериментальной настройки из исходников или через Docker. Ввод в Windows и Linux использует общий рабочий стол.", + connectTitle: "Подключите Codewhale", + connectVersion: "В Codewhale 0.10.2 встроена старая версия плагина Computer Use. Установка записи из каталога её не обновляет. Подключите установленное приложение-помощник Mac напрямую через MCP.", + connectLimit: "Прямое подключение MCP добавляет инструменты без навыков плагина. Не используйте его одновременно со старым встроенным плагином. Если тот включён, сначала выполните /plugin disable computer-use.", + connectRuntime: "Node входит в приложение; отдельная установка не нужна. Если приложение находится в другом месте, задайте APP фактический путь к пакету приложения.", + connectReload: "В работающем терминальном сеансе выполните /mcp reload, переключитесь в Operate и попросите:", + connectPrompt: "Проверь разрешения Computer Use с помощью request_access. Не отправляй ввод.", + connectExpected: "Проверьте в ответе via: app, версию помощника и недостающие разрешения. Если помощник отсутствует, остановитесь и следуйте подсказке по настройке. Это проверка только подключения и разрешений.", + connectGuide: "Руководство по настройке MCP", + contributeTitle: "Помогите улучшить Computer Use", + contributeBody: "Улучшайте документацию или проверяйте сценарии работы в приложениях и делитесь воспроизводимыми результатами.", + contributeGuide: "Руководство для участников", + contributeIssue: "Сообщить о проблеме", }; diff --git a/web/lib/i18n/dictionaries/tr/computer-use.ts b/web/lib/i18n/dictionaries/tr/computer-use.ts index 5b8c4badc..1afa8e792 100644 --- a/web/lib/i18n/dictionaries/tr/computer-use.ts +++ b/web/lib/i18n/dictionaries/tr/computer-use.ts @@ -26,7 +26,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "Uygulamayı kur", body: "Disk görüntüsünü aç ve Codewhale Computer Use'u Uygulamalar klasörüne sürükle. Uygulamalar'dan aç, ardından menü çubuğundaki balina simgesinden Computer Use'u seç." }, { title: "İzinleri gözden geçir", body: "Kurulum düğmeleriyle Sistem Ayarları'nda Erişilebilirlik ve Ekran Kaydı bölümlerini aç. Hangi izinleri vereceğine sen karar verirsin." }, - { title: "Ajanınızı bağlayın", body: "Codewhale’de /plugin marketplace install codewhale computer-use ile Computer Use’u yükleyin, ardından eklentiyi inceleyip etkinleştirin. Yardımcı ayrı olarak yüklenir. Diğer uyumlu MCP uygulamaları, kurulum kılavuzunu izleyerek yardımcıya bağlanır." }, + { title: "Ajanınızı bağlayın", body: "CLI sürümünüz için aşağıdaki Codewhale kurulumunu seçin. Diğer uyumlu MCP istemcileri için depodaki kurulum rehberini kullanın." }, ], controlsTitle: "Kontrol sende kalsın", controlsBody: "macOS izinleri verildikten sonra standart mod, her uygulama için izin istemeden çalışır. Engellediğiniz uygulamalar erişime kapalı kalır; Pause bilgisayar kullanımını duraklatır, Stop computer use mevcut oturumları sonlandırır. Sıkı onay modu isteğe bağlıdır. Çalışmayı izlemek istediğinizde canlı önizleme isteyin.", @@ -35,6 +35,18 @@ export const computerUse: ComputerUseDict = { help: "Kurulum ve sorun giderme", notes: "Sürüm notları", demo: "Arka plan kontrolünü izle", - source: "Kaynak kodu ve diğer platformlar", + source: "GitHub’da görüntüle", platforms: "Mac yardımcısı beta sürümündedir ve pazar yeri eklentisi yalnızca Mac’i destekler. Yayımlanan sürümlerde imzasız, deneysel bir Windows x64 önizlemesi de bulunur; Linux için kaynak koddan veya Docker ile deneysel kurulum gerekir. Windows ve Linux girdileri ortak masaüstünü kullanır.", + connectTitle: "Codewhale’i bağlayın", + connectVersion: "Codewhale 0.10.2 eski bir Computer Use eklentisi içerir. Katalog girdisini yüklemek bunu güncellemez. Yüklü Mac yardımcısına MCP üzerinden doğrudan bağlanın.", + connectLimit: "Doğrudan MCP bağlantısı, eklenti becerileri olmadan araçlar ekler. Eski yerleşik eklentiyle birlikte kullanmayın. Bu eklenti etkinse önce /plugin disable computer-use çalıştırın.", + connectRuntime: "Uygulama Node içerir; ayrıca yüklemeniz gerekmez. Başka bir konuma yüklendiyse APP değerini gerçek uygulama paketi yoluna ayarlayın.", + connectReload: "Açık bir terminal oturumunda /mcp reload çalıştırın, Operate moduna geçin ve şunu isteyin:", + connectPrompt: "Computer Use izinlerini request_access ile kontrol et. Hiçbir giriş gönderme.", + connectExpected: "Yanıtta via: app, yardımcı sürümü ve eksik izinleri kontrol edin. Yardımcı bulunamazsa durun ve kurulum ipucunu izleyin. Bu yalnızca bağlantıyı ve izinleri kontrol eder.", + connectGuide: "MCP kurulum rehberi", + contributeTitle: "Computer Use’u geliştirmeye katılın", + contributeBody: "Belgeleri iyileştirin veya bir uygulama iş akışını test edip tekrarlanabilir sonuçları paylaşın.", + contributeGuide: "Katkı rehberi", + contributeIssue: "Sorun bildirin", }; diff --git a/web/lib/i18n/dictionaries/types.ts b/web/lib/i18n/dictionaries/types.ts index 6201a6139..9b649eb4e 100644 --- a/web/lib/i18n/dictionaries/types.ts +++ b/web/lib/i18n/dictionaries/types.ts @@ -735,4 +735,16 @@ export interface ComputerUseDict { demo: string; source: string; platforms: string; + connectTitle: string; + connectVersion: string; + connectLimit: string; + connectRuntime: string; + connectReload: string; + connectPrompt: string; + connectExpected: string; + connectGuide: string; + contributeTitle: string; + contributeBody: string; + contributeGuide: string; + contributeIssue: string; } diff --git a/web/lib/i18n/dictionaries/uk/computer-use.ts b/web/lib/i18n/dictionaries/uk/computer-use.ts index 65fbfd767..49969067c 100644 --- a/web/lib/i18n/dictionaries/uk/computer-use.ts +++ b/web/lib/i18n/dictionaries/uk/computer-use.ts @@ -27,7 +27,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "Установіть застосунок", body: "Відкрийте образ диска й перетягніть Codewhale Computer Use у папку «Програми». Відкрийте його з папки «Програми», а потім виберіть Computer Use у меню з іконкою кита в смузі меню." }, { title: "Перегляньте дозволи", body: "За допомогою кнопок налаштування відкрийте розділи «Доступність» і «Запис екрана» в Системних параметрах. Які дозволи надати — вирішуєте ви." }, - { title: "Підключіть агента", body: "У Codewhale встановіть Computer Use командою /plugin marketplace install codewhale computer-use, потім перегляньте й увімкніть плагін. Помічник встановлюється окремо. Інші сумісні MCP-хости підключаються до нього за посібником із налаштування." }, + { title: "Підключіть агента", body: "Виберіть нижче налаштування Codewhale для вашої версії CLI. Для інших сумісних MCP-клієнтів скористайтеся посібником із налаштування в репозиторії." }, ], controlsTitle: "Контроль залишається у вас", controlsBody: "Після надання дозволів macOS стандартний режим працює без запитів для кожного застосунку. Заблоковані вами застосунки недоступні агенту; Pause призупиняє роботу з комп’ютером, а Stop computer use завершує поточні сеанси. Суворе підтвердження дій можна ввімкнути окремо. Щоб стежити за роботою, попросіть попередній перегляд наживо.", @@ -36,6 +36,18 @@ export const computerUse: ComputerUseDict = { help: "Налаштування й усунення несправностей", notes: "Примітки до випуску", demo: "Подивитися фонову перевірку", - source: "Вихідний код та інші платформи", + source: "Переглянути на GitHub", platforms: "Помічник для Mac має статус бета, а плагін каталогу підтримує лише Mac. Опубліковані випуски також містять непідписану експериментальну попередню версію для Windows x64; Linux потребує експериментального налаштування з вихідного коду або через Docker. Введення у Windows і Linux використовує спільний робочий стіл.", + connectTitle: "Підключіть Codewhale", + connectVersion: "Codewhale 0.10.2 містить старішу версію плагіна Computer Use. Встановлення запису з каталогу її не оновлює. Підключіть встановлений Mac-помічник безпосередньо через MCP.", + connectLimit: "Пряме підключення MCP додає інструменти без навичок плагіна. Не використовуйте його разом зі старим вбудованим плагіном. Якщо той увімкнений, спочатку виконайте /plugin disable computer-use.", + connectRuntime: "Node входить до складу застосунку, окремо встановлювати його не потрібно. Якщо застосунок встановлено деінде, задайте APP фактичний шлях до пакета застосунку.", + connectReload: "У запущеному термінальному сеансі виконайте /mcp reload, перейдіть у Operate та попросіть:", + connectPrompt: "Перевір дозволи Computer Use за допомогою request_access. Не надсилай ввід.", + connectExpected: "Перевірте у відповіді via: app, версію помічника й відсутні дозволи. Якщо помічника немає, зупиніться та дотримуйтеся підказки з налаштування. Це перевірка лише підключення та дозволів.", + connectGuide: "Посібник із налаштування MCP", + contributeTitle: "Допоможіть покращити Computer Use", + contributeBody: "Покращуйте документацію або перевіряйте сценарії роботи в застосунках і діліться відтворюваними результатами.", + contributeGuide: "Посібник для учасників", + contributeIssue: "Повідомити про проблему", }; diff --git a/web/lib/i18n/dictionaries/vi/computer-use.ts b/web/lib/i18n/dictionaries/vi/computer-use.ts index ab1411a62..2d4a1e6c7 100644 --- a/web/lib/i18n/dictionaries/vi/computer-use.ts +++ b/web/lib/i18n/dictionaries/vi/computer-use.ts @@ -27,7 +27,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "Cài đặt ứng dụng", body: "Mở ảnh đĩa và kéo Codewhale Computer Use vào thư mục “Ứng dụng”. Mở ứng dụng từ “Ứng dụng”, rồi chọn Computer Use từ biểu tượng cá voi trên thanh menu." }, { title: "Xem lại quyền truy cập", body: "Dùng các nút thiết lập để mở “Trợ năng” và “Ghi màn hình” trong Cài đặt Hệ thống. Bạn tự quyết định cấp những quyền nào." }, - { title: "Kết nối tác nhân của bạn", body: "Trong Codewhale, cài Computer Use bằng /plugin marketplace install codewhale computer-use, rồi xem lại và bật plugin. Ứng dụng hỗ trợ được cài riêng. Các host MCP tương thích khác kết nối với ứng dụng theo hướng dẫn thiết lập." }, + { title: "Kết nối tác nhân của bạn", body: "Chọn cách thiết lập Codewhale bên dưới phù hợp với phiên bản CLI của bạn. Các ứng dụng chủ MCP tương thích khác dùng hướng dẫn thiết lập trong kho mã." }, ], controlsTitle: "Bạn luôn nắm quyền kiểm soát", controlsBody: "Sau khi cấp quyền macOS, chế độ tiêu chuẩn hoạt động mà không hỏi quyền cho từng ứng dụng. Các ứng dụng bạn chặn vẫn không thể truy cập; Pause tạm dừng thao tác máy tính và Stop computer use kết thúc các phiên hiện tại. Chế độ phê duyệt nghiêm ngặt là tùy chọn. Yêu cầu xem trước trực tiếp khi bạn muốn theo dõi công việc.", @@ -36,6 +36,18 @@ export const computerUse: ComputerUseDict = { help: "Thiết lập và khắc phục sự cố", notes: "Ghi chú phát hành", demo: "Xem kiểm tra nền", - source: "Mã nguồn và các nền tảng khác", + source: "Xem trên GitHub", platforms: "Ứng dụng hỗ trợ trên Mac đang ở bản beta và plugin trong danh mục chỉ hỗ trợ Mac. Các bản phát hành cũng có bản xem trước thử nghiệm chưa được ký cho Windows x64; Linux cần thiết lập thử nghiệm từ mã nguồn hoặc qua Docker. Thao tác nhập trên Windows và Linux sử dụng màn hình nền dùng chung.", + connectTitle: "Kết nối Codewhale", + connectVersion: "Codewhale 0.10.2 tích hợp một plugin Computer Use cũ. Cài mục trong danh mục không nâng cấp plugin đó. Hãy kết nối trực tiếp với ứng dụng trợ giúp Mac đã cài qua MCP.", + connectLimit: "Kết nối MCP trực tiếp thêm công cụ nhưng không thêm kỹ năng của plugin. Không dùng đồng thời với plugin tích hợp cũ. Nếu plugin đó đang bật, hãy chạy /plugin disable computer-use trước.", + connectRuntime: "Ứng dụng có sẵn Node, không cần cài riêng. Nếu cài ở nơi khác, đặt APP thành đường dẫn thực tế của gói ứng dụng.", + connectReload: "Trong phiên terminal đang chạy, chạy /mcp reload, chuyển sang Operate rồi yêu cầu:", + connectPrompt: "Kiểm tra quyền Computer Use bằng request_access. Không gửi thao tác nhập liệu.", + connectExpected: "Kiểm tra via: app, phiên bản ứng dụng trợ giúp và các quyền còn thiếu trong phản hồi. Nếu thiếu ứng dụng trợ giúp, hãy dừng lại và làm theo hướng dẫn thiết lập. Bước này chỉ kiểm tra kết nối và quyền.", + connectGuide: "Hướng dẫn thiết lập MCP", + contributeTitle: "Góp phần cải thiện Computer Use", + contributeBody: "Cải thiện tài liệu hoặc thử một quy trình trong ứng dụng và chia sẻ kết quả có thể tái hiện.", + contributeGuide: "Hướng dẫn đóng góp", + contributeIssue: "Báo cáo sự cố", }; diff --git a/web/lib/i18n/dictionaries/zh/computer-use.ts b/web/lib/i18n/dictionaries/zh/computer-use.ts index 171afdd6f..e92111c8c 100644 --- a/web/lib/i18n/dictionaries/zh/computer-use.ts +++ b/web/lib/i18n/dictionaries/zh/computer-use.ts @@ -26,7 +26,7 @@ export const computerUse: ComputerUseDict = { steps: [ { title: "安装应用", body: "打开下载的磁盘映像,将 Codewhale Computer Use 拖入“应用程序”。从“应用程序”中打开它,然后点击菜单栏中的鲸鱼图标,选择 Computer Use。" }, { title: "检查权限", body: "通过设置按钮打开系统设置中的“辅助功能”和“屏幕录制”。由你决定授予哪些权限。" }, - { title: "连接智能体", body: "在 Codewhale 中运行 /plugin marketplace install codewhale computer-use,审阅并启用插件。其他兼容 MCP 的宿主可按设置指南连接助手。" }, + { title: "连接智能体", body: "选择下方适用于你的 CLI 版本的 Codewhale 设置方式。其他兼容 MCP 的宿主请参阅仓库中的设置指南。" }, ], controlsTitle: "随时掌握控制权", controlsBody: "授予 macOS 权限后,标准模式无需逐个应用询问。你禁用的应用仍不可访问;Pause 暂停电脑操作,Stop computer use 结束当前会话。也可选择严格审批模式。想观看操作过程时,再请求打开实时预览。", @@ -35,6 +35,18 @@ export const computerUse: ComputerUseDict = { help: "设置与故障排查", notes: "版本说明", demo: "查看后台检查演示", - source: "其他宿主与平台", + source: "在 GitHub 上查看", platforms: "Mac 助手为测试版,市场插件支持 Mac。已发布版本另含未签名的 Windows x64 实验性预览包;Linux 需要实验性源码或 Docker 设置。Windows 和 Linux 的输入使用共享桌面。", + connectTitle: "连接 Codewhale", + connectVersion: "Codewhale 0.10.2 内置较旧的 Computer Use 插件,安装目录中的同名条目不会将其升级。请通过 MCP 直接连接已安装的 Mac 助手。", + connectLimit: "直接连接 MCP 只添加工具,不包含插件技能。请勿与旧版内置插件同时使用;如果该插件已启用,请先运行 /plugin disable computer-use。", + connectRuntime: "应用自带 Node,无需另行安装。如果应用安装在其他位置,请将 APP 设为实际应用包路径。", + connectReload: "在正在运行的终端会话中执行 /mcp reload,切换到 Operate,然后输入:", + connectPrompt: "使用 request_access 检查 Computer Use 权限。不要发送任何输入。", + connectExpected: "检查回复中的 via: app、助手版本及缺少的权限。如果找不到助手,请停止并按设置提示操作。此步骤仅检查连接和权限。", + connectGuide: "MCP 设置指南", + contributeTitle: "一起改进 Computer Use", + contributeBody: "完善文档,或测试应用操作流程并分享可复现的结果。", + contributeGuide: "贡献指南", + contributeIssue: "报告问题", }; diff --git a/web/lib/i18n/dictionaries/zh/docs-configuration.ts b/web/lib/i18n/dictionaries/zh/docs-configuration.ts index 99463ecbb..3c0744169 100644 --- a/web/lib/i18n/dictionaries/zh/docs-configuration.ts +++ b/web/lib/i18n/dictionaries/zh/docs-configuration.ts @@ -71,7 +71,7 @@ codewhale config doctor # unknown keys, empty secrets, malformed URLs`, codeTerms: true, }, { - p: "密钥、服务端点、提供商选择、MCP 服务器、钩子、技能和额外的说明文件,始终来自你自己的配置,因此克隆下来的仓库无法让 Codewhale 指向它自己的服务器或文件。启动时加上 `--no-project-config`,可以在这一次运行中忽略仓库的配置文件。", + p: "提供商密钥、服务端点及提供商选择来自你自己的配置。在用户设置中信任工作区后,MCP 服务器也可来自 `.codewhale/mcp.json`;项目条目覆盖同名的全局条目。信任仓库前,请审阅这些可执行权限。参阅 [MCP 配置](/docs/mcp#config)。", }, { p: "项目说明——智能体在这个仓库里应该如何工作——请写在 `AGENTS.md` 中。运行 `/init` 即可生成一份。", diff --git a/web/lib/i18n/dictionaries/zh/docs-mcp.ts b/web/lib/i18n/dictionaries/zh/docs-mcp.ts index 20cf2c842..12478be7b 100644 --- a/web/lib/i18n/dictionaries/zh/docs-mcp.ts +++ b/web/lib/i18n/dictionaries/zh/docs-mcp.ts @@ -4,31 +4,55 @@ import type { DocsMcpDict } from "../types"; export const docsMcp: DocsMcpDict = { metaTitle: "用 MCP 连接工具 · Codewhale 文档", metaDescription: - "添加 Model Context Protocol 服务器让 Codewhale 使用更多工具,登录远程服务器,把 Codewhale 本身作为 MCP 服务器运行,并试用代码模式。", + "连接本地和远程 MCP 服务器,了解认证与插件兼容范围,并排查工具未显示的问题。", bodyClassName: "text-ink-soft leading-[1.9] tracking-wide", title: "用 MCP 连接工具", lede: - "MCP 服务器能为 Codewhale 带来更多工具——数据库、问题跟踪系统、浏览器等。你可以添加一个由 Codewhale 替你启动的本地服务器,也可以通过 URL 添加远程服务器。这些工具与内置工具走同一套审批流程。", + "通过 Model Context Protocol 连接本地工具服务器或远程服务。Codewhale 会发现服务器提供的工具,并在使用时执行你的工具权限和审批规则。", sections: [ { id: "add", - title: "添加服务器", + title: "试用 Cloudflare Docs 服务器", blocks: [ { - code: `codewhale mcp add git --command "uvx" --arg "mcp-server-git" -codewhale mcp add docs --url "https://example.com/mcp" -codewhale mcp list -codewhale mcp validate`, + p: "公开的 [Cloudflare 文档服务器](https://developers.cloudflare.com/agents/model-context-protocol/cloudflare/servers-for-cloudflare/)提供文档搜索和 Pages 迁移到 Workers 的指南。[查看官方源码](https://github.com/cloudflare/mcp-server-cloudflare/tree/main/apps/docs-vectorize)。无需凭据即可添加:", + }, + { + code: `codewhale mcp add cloudflare-docs --url "https://docs.mcp.cloudflare.com/mcp" +codewhale mcp tools cloudflare-docs`, lang: "终端", }, { - p: "`--command` 通过 stdio 启动本地服务器,每个参数用一个 `--arg`。`--url` 通过 Streamable HTTP 连接远程服务器,必要时回退到旧版 SSE。`mcp validate` 会检查配置文件以及你标记为必需的服务器。", + p: "工具列表应包含 `search_cloudflare_documentation` 和 `migrate_pages_to_workers_guide`。如果 TUI 会话已在运行,请在其中执行 `/mcp reload`,把新配置加载到该会话。", + }, + { + note: "2026 年 11 月 7 日使用已发布的 CLI v0.10.2 检查:两条命令均成功退出,无需凭据即可列出上述两个工具。这验证了配置和发现流程,没有执行工具。[发布版指南](https://github.com/codewhale-hq/Codewhale/blob/v0.10.2/docs/MCP.md)对应此版本;[main 源码指南](https://github.com/codewhale-hq/Codewhale/blob/main/docs/MCP.md)可能包含尚未发布的变更。", }, { p: "在会话中,`/mcp` 会打开 MCP 管理器:显示每个服务器的状态、传输方式、超时设置、错误信息和已发现的工具。同样的操作在那里也能完成,例如 `/mcp add stdio ` 和 `/mcp add http `。", }, { - note: "MCP 服务器以你的权限运行。只添加你信任的服务器,就像只安装你信任的程序一样。", + note: "本地 MCP 服务器以 Codewhale 进程的权限运行;远程调用使用你授予该服务的访问权限。只添加你信任的服务器。", + }, + ], + }, + { + id: "compatibility", + title: "可以连接哪些服务器", + blocks: [ + { + rows: [ + ["本地 stdio", "Codewhale 启动 `--command` 指定的程序,每个参数用一个 `--arg`。请先安装服务器及其所需运行时。"], + ["远程 HTTP / SSE", "`--url` 使用 Streamable HTTP,并可回退到旧版 SSE。旧版 SSE 端点可设置 `--transport sse`。远程配置不需要 `command`。"], + ["直接配置的远程认证", "你自己的 MCP 配置可以使用环境变量请求头、bearer token 或 OAuth。OAuth 需要服务器提供兼容的认证元数据和客户端注册方式。"], + ["插件提供的 MCP", "经过审查并启用的插件可以提供 stdio 或远程 MCP 服务器。插件 MCP 支持基于环境变量的认证,但不支持插件提供的 MCP OAuth。"], + ["工具、资源与提示词", "Codewhale 发现服务器实际声明的能力。只有服务器实现了资源、模板或提示词功能,才能使用这些功能。"], + ["可选扩展与 MCP Apps", "尚不提供通用的 MCP Apps 界面支持。支持基本 MCP 传输不等于兼容所有可选扩展或服务器。"], + ["服务器要求", "每个服务器都有自己的操作系统、程序、浏览器、依赖和账号要求。本地服务器运行在承载 Codewhale 的电脑上;Computer Use 有其独立的平台限制。"], + ], + }, + { + p: "直接添加 MCP 不需要插件。[插件](/plugins)可以把 MCP 工具、指令和其他组件打包在一起;各组件仍需要兼容的依赖、审查和权限。", }, ], }, @@ -37,13 +61,16 @@ codewhale mcp validate`, title: "登录远程服务器", blocks: [ { - p: "使用 OAuth 的服务器,先用 URL 添加再登录。使用 bearer token 的服务器,把令牌放在环境变量里,而不是写进配置文件:", + p: "直接配置 OAuth 服务器时,先添加它的真实 URL,再用你设置的服务器名称登录:", }, { - code: `codewhale mcp login docs -codewhale mcp add tracker --url "https://example.com/mcp" --bearer-token-env-var TRACKER_TOKEN`, + code: `codewhale mcp login +codewhale mcp logout `, lang: "终端", }, + { + p: "使用 bearer token 时,添加服务器时指定 `--bearer-token-env-var TOKEN_NAME`,并在启动 Codewhale 的环境中设置该变量。`env_headers` 把请求头名称映射到环境变量名。字面量 `headers` 不会展开 `${TOKEN_NAME}`。插件 MCP 不能使用 OAuth;需要 OAuth 时,请直接配置该服务器。", + }, { p: "显式设置的 Authorization 请求头始终优先:先应用配置中的请求头,其次是 bearer token 环境变量,最后才是已保存的 OAuth 登录。`codewhale mcp logout ` 会删除本机保存的登录信息;提供商那边的授权可能仍然有效,需要到提供商处撤销。", }, @@ -54,23 +81,26 @@ codewhale mcp add tracker --url "https://example.com/mcp" --bearer-token-env-var title: "编辑配置文件", blocks: [ { - p: "服务器配置保存在 `~/.codewhale/mcp.json`,`codewhale mcp init` 会生成一个初始文件。其他客户端使用的 `mcpServers` 键同样可用,现成的配置可以直接粘贴进来。", + p: "全局服务器配置保存在 `~/.codewhale/mcp.json`;如果该文件不存在,仍会读取旧版 `~/.deepseek/mcp.json`。`codewhale mcp init` 会生成初始文件。可用绝对路径 `mcp_config_path` 或 `CODEWHALE_MCP_CONFIG` 覆盖默认位置。环境变量也接受 `DEEPSEEK_MCP_CONFIG`;两者都设置时,Codewhale 名称优先。", }, { code: `{ "servers": { - "example": { - "command": "node", - "args": ["./path/to/your-mcp-server.js"], - "env": {}, - "disabled": false + "cloudflare-docs": { + "url": "https://docs.mcp.cloudflare.com/mcp" } } }`, lang: "mcp.json", }, { - p: "修改文件后,在会话中运行 `/mcp reload` 即可,无需重启。服务器只在某个回合需要它的工具时才启动;如果希望它在启动时就连接,把它标记为 `\"required\": true`。", + p: "`servers` 和别名 `mcpServers` 二选一,不要在同一文件中同时使用。`env_http_headers` 是 `env_headers` 的别名。请按服务器文档调整命令、参数和认证字段,不要假定所有客户端的配置格式都能直接互换。", + }, + { + p: "受信任的工作区可以提供 `.codewhale/mcp.json`,其中同名服务器会覆盖全局配置;仓库不能自行授予信任,也不能覆盖你的全局配置路径。全局或受信任项目中的显式配置,优先于标识冲突的插件服务器。", + }, + { + p: "修改后在 TUI 中运行 `/mcp reload` 即可,无需重启。默认情况下,已启用且获准的服务器会在会话启动时连接。用户配置的服务器可设置 `\"lazy\": true` 按需连接;设置 `\"required\": true` 后仍会在启动时连接。插件 MCP 清单没有 lazy 选项。无界面的运行时在配置变更后需要重启。", }, ], }, @@ -79,10 +109,26 @@ codewhale mcp add tracker --url "https://example.com/mcp" --bearer-token-env-var title: "找到这些工具", blocks: [ { - p: "每个工具在模型眼中的名字是 `mcp__`:名为 `git` 的服务器提供的 `status` 工具,就叫 `mcp_git_status`。`codewhale mcp tools ` 会列出某个服务器提供的工具。连接失败或已禁用的服务器,其工具永远不会显示为可用。", + p: "每个工具在模型眼中的名字是 `mcp__`。本例中的文档搜索是 `mcp_cloudflare-docs_search_cloudflare_documentation`。`codewhale mcp tools ` 在自己的进程中列出发现的工具,不会把连接附加到正在运行的会话。", }, { - p: "MCP 工具遵循你的[审批设置](/docs/modes):在策略允许时,列出和读取服务器的资源与提示词无需确认;有副作用的工具会先征求你的同意。Full Access 也不会绕过仓库规则或托管策略。", + p: "MCP 工具遵循你的[审批设置](/docs/modes)、允许/拒绝规则及网络策略。调用前是否询问,取决于策略与权限姿态。Full Access 不会绕过仓库规则或托管策略。", + }, + ], + }, + { + id: "troubleshooting", + title: "工具没有出现时", + blocks: [ + { + rows: [ + ["检查配置", "`codewhale doctor` 提供静态路径和配置诊断。终端中的 `codewhale mcp validate` 会在独立进程中连接已启用且获准的服务器,属于实际连接检查。"], + ["刷新当前会话", "TUI 中的 `/mcp validate` 只显示上次快照,不会重新连接。配置变更后使用 `/mcp reload`;单个服务器可用 `/mcp retry `。"], + ["已连接但缺少工具", "检查 `codewhale mcp tools `、服务器声明的工具、`enabled_tools` / `disabled_tools`,以及会话的允许/拒绝规则。传输已连接不代表某个工具可用或已获准。"], + ["需要认证", "直接配置的 OAuth 服务器可运行 `/mcp login `。令牌认证请检查变量是否存在于 Codewhale 的启动环境中。在一个进程中登录成功,不会把该进程的连接附加到另一个进程。"], + ["本地命令失败", "检查服务器程序、参数、工作目录和运行时。Windows 上的包运行器可能需要使用 `.cmd` 名称。请遵循服务器提供方的操作系统和依赖说明。"], + ["插件服务器缺失", "检查所属插件的审查、信任和启用状态。MCP 设置不能激活不受信任的插件,也不能绕过被拒绝的清单。"], + ], }, ], }, @@ -91,7 +137,7 @@ codewhale mcp add tracker --url "https://example.com/mcp" --bearer-token-env-var title: "把 Codewhale 作为 MCP 服务器运行", blocks: [ { - p: "其他 MCP 客户端可以把 Codewhale 作为工具服务器。默认仅暴露只读的 `file_read` 和 `search` 工具。", + p: "支持 Codewhale 声明的 MCP 协议版本的客户端,可以把它作为 stdio 工具服务器启动。默认仅暴露只读的 `file_read` 和 `search` 工具。", }, { code: `codewhale mcp add-self @@ -99,7 +145,7 @@ codewhale mcp tools codewhale`, lang: "终端", }, { - p: "`add-self` 注册通过 stdio 运行的 `codewhale serve --mcp`;`codewhale mcp-server` 是等价入口。除非操作者在服务器配置中明确允许,否则不会提供写入工具。[Runtime API](/docs/runtime-api) HTTP 是单独的传输方式。", + p: "`add-self` 注册 `codewhale serve --mcp`;`codewhale mcp-server` 是等价入口。这个服务器从旧版路径 `~/.deepseek/mcp_server.toml` 读取独立的 `[server]` 设置,而不是客户端的 `mcp.json`。写入或 shell 工具需要显式列出,并设置 `require_approval = false`;这里没有交互式审批通道。[Runtime API](/docs/runtime-api) HTTP 与 MCP 分开。", }, ], }, @@ -107,6 +153,9 @@ codewhale mcp tools codewhale`, id: "code-mode", title: "用代码模式组合工具调用(实验性)", blocks: [ + { + note: "本节依据 main 候选版本的源码,可能包含比已发布 v0.10.2 更新的行为。", + }, { p: "代码模式让模型写一段简短的 JavaScript 程序,在其中调用多个工具、循环和筛选结果,而不必把每次调用都作为单独的一步。只有程序的最终结果会返回给模型,长时间的查找因此更紧凑。它默认开启——`execute_tools` 从第一条请求起就会提供——MCP 和插件工具与内置工具一样可以在程序中组合调用。关闭它并不会移除代码模式:`execute_tools` 会恢复为延迟加载,模型仍可通过 `tool_search` 找到它。要关闭它,可以只在一次会话中关闭,也可以在配置中关闭:", }, diff --git a/web/lib/llms-txt.ts b/web/lib/llms-txt.ts index 9304d1653..38e010c8c 100644 --- a/web/lib/llms-txt.ts +++ b/web/lib/llms-txt.ts @@ -78,12 +78,6 @@ const EXTRA_PAGES: readonly { path: string; title: string; description: string } description: "Codewhale release record: the latest published release, the unreleased source candidate, and the notes for each version, drawn from CHANGELOG.md in the repository.", }, - { - path: "/plugins", - title: "Plugins and marketplace", - description: - "Preview Computer Use for selected desktop apps, and install reviewed plugins from the Codewhale catalog, a GitHub repository, or a tarball.", - }, { path: "/ratatui", title: "Codewhale Ratatui", @@ -174,6 +168,11 @@ export function buildLlmsTxt(): string { `- [Full documentation](${SITE_URL}/llms-full.txt): Every documentation topic as plain text.`, `- [Runtime API](${SITE_URL}/docs-markdown/runtime-api.md): HTTP/SSE API and ACP integration.`, `- [MCP](${SITE_URL}/docs-markdown/mcp.md): Connect tools or run codewhale mcp-server / codewhale serve --mcp. Default server tools are read-only file_read and search.`, + "", "## Plugins and MCP", "", + `- [Choose an integration](${SITE_URL}/en/plugins): Skills are instructions; plugins package reviewed capabilities; MCP servers supply external tools, resources and prompts.`, + `- [Connect MCP servers](${SITE_URL}/en/docs/mcp): Local stdio and remote Streamable HTTP, legacy SSE, environment-backed tokens and direct-server OAuth. Plugin-bundled MCP does not support OAuth.`, + `- [Computer Use](${SITE_URL}/en/computer-use): Mac helper setup, CLI-version limitations and its separate open-source repository.`, + `- [Public plugin catalog](https://github.com/codewhale-hq/codewhale-plugin-marketplace): Installed CLI releases bundle a dated snapshot; current catalog entries are not proof of installed versions or end-to-end qualification.`, "", "## Documentation", "", ); for (const topic of DOC_TOPICS) {