| 1 | # Configuration |
| 2 | |
| 3 | codewhale reads configuration from a TOML file plus environment variables. |
| 4 | At process startup it may also load literal built-in-provider credentials from |
| 5 | a workspace-local `.env` file. Use the tracked `.env.example` as the template; |
| 6 | copy it to `.env`, then add only credential values. |
| 7 | |
| 8 | A workspace is not configuration authority. Codewhale therefore ignores |
| 9 | config/profile/home paths, provider/model/base-URL routing, MCP/plugin state, |
| 10 | approval/sandbox/shell posture, executable paths, runtime settings, and every |
| 11 | other non-credential `.env` entry. Variable expansion is rejected so a |
| 12 | repository cannot substitute an ambient secret into a credential value. Use |
| 13 | `config.toml`, CLI flags, or values exported by the launching shell for those |
| 14 | explicit control-plane settings. `.env` is read from a stable regular-file |
| 15 | handle, is capped at 1 MiB, and symbolic links, reparse points, and multiply |
| 16 | linked files are rejected. |
| 17 | |
| 18 | ## Constitution, project instructions, and repo authority |
| 19 | |
| 20 | Codewhale has several instruction surfaces. They are deliberately separate so a |
| 21 | personal constitution, repo policy, project instructions, and runtime security |
| 22 | controls do not blur together. |
| 23 | |
| 24 | - **Bundled global Constitution** — the compiled base law in the binary. It is |
| 25 | the default floor for every session. |
| 26 | - **User-global constitution** — the normal guided setup output. Manage it with |
| 27 | `/constitution` or `/setup`; Codewhale stores structured data at |
| 28 | `$CODEWHALE_HOME/constitution.json` (default `~/.codewhale/constitution.json`) |
| 29 | and renders it into a separate `<codewhale_user_constitution>` prose block. |
| 30 | This can express preferences and stop conditions, but it does not change |
| 31 | runtime approval policy, sandbox, shell, network, trust, or MCP permissions. |
| 32 | - **Repo-local constitution** — optional project policy in |
| 33 | `.codewhale/constitution.json`, described below. |
| 34 | - **`AGENTS.md`** — cross-agent **project instructions** (prose). This is the |
| 35 | canonical file for "how should an agent work in this repo." Run `/init` to |
| 36 | scaffold one. `CLAUDE.md` and `.claude/instructions.md` are read as |
| 37 | compatibility fallbacks. |
| 38 | - **Memory and handoffs** — recalled state. Useful, but lower authority than |
| 39 | constitutions and project instructions. |
| 40 | |
| 41 | ### Managing the user-global constitution (`/setup` and `/constitution`) |
| 42 | |
| 43 | On first launch Codewhale runs a short **constitution-first** setup path: |
| 44 | language → provider/model readiness → runtime posture → create or confirm your |
| 45 | constitution. The bundled/default constitution is always valid, so you can |
| 46 | defer; reopen the hub any time with `/setup`. |
| 47 | |
| 48 | On the **Constitution** step: |
| 49 | |
| 50 | - **`1`–`6`** tune the guided draft. **`G`** previews it, and **`G`** again |
| 51 | ratifies and saves a fresh structured `constitution.json`. |
| 52 | - **`A`** (shown only when a provider is configured) asks your first configured |
| 53 | model to draft the constitution. Drafting is **not** saving: the draft is |
| 54 | rendered through the same preview and you still press **`G`** to ratify |
| 55 | before anything persists. |
| 56 | - **`K`** keeps your existing loaded constitution unchanged (shown only when a |
| 57 | valid file is already present). |
| 58 | - **`U`** (or `/constitution bundled`) records the bundled/default law. |
| 59 | |
| 60 | `/constitution` (alias `/law`) is the primary management surface once you are |
| 61 | set up. Subcommands: `status` (the default), `preview`, `review`, `repo` (the |
| 62 | repo-local law block), `explain`, `edit`/`guided`, `repair`, `posture`, and |
| 63 | `bundled`. Managing the constitution never changes runtime approval, sandbox, |
| 64 | shell, network, trust, default mode, or MCP authority — those stay in runtime |
| 65 | posture/config. |
| 66 | |
| 67 | Each repo can carry two distinct, complementary files: |
| 68 | |
| 69 | - **`AGENTS.md`** — ordinary project working instructions. |
| 70 | - **`.codewhale/constitution.json`** — Codewhale-specific **repo authority / |
| 71 | prioritization policy**: when local sources conflict, which should Codewhale |
| 72 | trust first, and what to verify before claiming a task is done. `.codewhale/` |
| 73 | lives inside the repo (like `.github/`). Example: |
| 74 | |
| 75 | ```json |
| 76 | { |
| 77 | "schema_version": 1, |
| 78 | "authority": [ |
| 79 | "current user request", |
| 80 | "live code and tests", |
| 81 | "GitHub issue/PR details", |
| 82 | "AGENTS.md", |
| 83 | "memory", |
| 84 | "old handoffs" |
| 85 | ], |
| 86 | "protected_invariants": [ |
| 87 | "do not break old-session transcript replay" |
| 88 | ], |
| 89 | "branch_policy": "PRs target the integration branch, not main", |
| 90 | "verification_policy": { |
| 91 | "before_claiming_done": ["run focused tests", "read changed files back"] |
| 92 | }, |
| 93 | "escalate_when": [ |
| 94 | "a destructive action was not explicitly authorized" |
| 95 | ] |
| 96 | } |
| 97 | ``` |
| 98 | |
| 99 | All fields are optional. When present, the file is rendered into the system |
| 100 | prompt as concise prose in a higher-authority block. Legacy `WHALE.md` files |
| 101 | are ignored and reported as migration-only diagnostics. |
| 102 | |
| 103 | Each `protected_invariants` entry may be either a plain string (advisory |
| 104 | prose, the historical shape) or an object carrying path globs, which is |
| 105 | additionally **mechanically enforced** in the tool gate. See |
| 106 | [Enforced repo-law invariants](#enforced-repo-law-invariants) below. |
| 107 | |
| 108 | This is the **repo-local law** layer in Codewhale's hierarchy: *bundled global |
| 109 | Constitution* → *user-global constitution* (`$CODEWHALE_HOME/constitution.json`, |
| 110 | rendered as prose) → *repo constitution* (`.codewhale/constitution.json`, this |
| 111 | file) → *AGENTS/project instructions* → *memory and handoffs* → *current |
| 112 | request and live evidence for the active turn*. Runtime policy |
| 113 | (permissions/sandbox/cost limits enforced in code) is separate from all of |
| 114 | these prompt layers. The repo constitution gives project decision rules; it |
| 115 | does not replace the bundled Constitution, the user-global constitution, or |
| 116 | the current user request. |
| 117 | |
| 118 | > **`WHALE.md` is deprecated.** It overlapped confusingly with `AGENTS.md`. |
| 119 | > Codewhale no longer reads `WHALE.md` as project or global context. If one is |
| 120 | > present, setup/context diagnostics report it as ignored so you can migrate it. |
| 121 | > Move ordinary instructions to `AGENTS.md` and Codewhale-specific authority |
| 122 | > policy to `.codewhale/constitution.json`. Personal standing guidance belongs |
| 123 | > in `/constitution` / `$CODEWHALE_HOME/constitution.json`. (The global |
| 124 | > Codewhale Constitution shipped in the model prompt is a separate thing and is |
| 125 | > unaffected.) |
| 126 | |
| 127 | ### Enforced repo-law invariants |
| 128 | |
| 129 | By default a `protected_invariants` entry is advisory prose: it is rendered into |
| 130 | the prompt as guidance the agent should honor, but nothing stops a write. An |
| 131 | entry written as an **object with `paths`** is different — it compiles into a |
| 132 | mechanical write hold that the engine's tool gate evaluates before the write |
| 133 | runs. The law becomes mechanism, not just a request. |
| 134 | |
| 135 | An enforced entry has this shape: |
| 136 | |
| 137 | ```json |
| 138 | { |
| 139 | "schema_version": 1, |
| 140 | "protected_invariants": [ |
| 141 | "Keep DeepSeek support first-class.", |
| 142 | { |
| 143 | "text": "The wire format is frozen; protocol changes need a human.", |
| 144 | "paths": ["crates/protocol/**"], |
| 145 | "action": "block" |
| 146 | }, |
| 147 | { |
| 148 | "text": "Release notes need human review.", |
| 149 | "paths": ["CHANGELOG.md"], |
| 150 | "action": "ask" |
| 151 | } |
| 152 | ] |
| 153 | } |
| 154 | ``` |
| 155 | |
| 156 | - `text` — required. The reason surfaced on the hold. An empty `text` is skipped. |
| 157 | - `paths` — workspace-relative globs (globset syntax, e.g. `crates/protocol/**`, |
| 158 | `**/secrets.toml`, `CHANGELOG.md`). An object with no usable `paths` stays |
| 159 | advisory-only despite the object shape. |
| 160 | - `action` — optional, defaults to `ask`. `ask` force-prompts in Ask and |
| 161 | Auto-Review; in Full Access it denies the protected write without opening a |
| 162 | modal. `block` **denies the write outright** in every posture. |
| 163 | |
| 164 | Semantics: |
| 165 | |
| 166 | - **Tighten-only.** The schema has no allow/widen shape, so law can only *add* |
| 167 | holds — a crafted constitution can never grant authority or weaken a gate |
| 168 | above it. |
| 169 | - **Not bypassable by mode.** Like the built-in safety floor, an `ask` hold |
| 170 | force-prompts in Ask and Auto-Review. Full Access never opens approval |
| 171 | modals, so the same hold fails closed as a hard block; `block` always denies. |
| 172 | Mode cannot turn a hold off. |
| 173 | - **Repo-local only.** Only the repo's `.codewhale/constitution.json` |
| 174 | participates. The user-global constitution stays advisory prose and never |
| 175 | reaches this mechanism. |
| 176 | - **Fails safe.** A missing file, parse error, or invalid glob degrades to |
| 177 | fewer or zero rules — never a hold on unprotected paths and never a poisoned |
| 178 | gate. Across matches the strongest action wins, so `block` outranks `ask`. |
| 179 | - **Leaves a receipt.** Every hold emits a `tool.repo_law_decision` tool-audit |
| 180 | event naming the invariant, the matched path, and the source file; the |
| 181 | approval/denial reason names the invariant too. |
| 182 | |
| 183 | **Coverage is deliberately limited.** Holds are evaluated only for the write |
| 184 | tools `write_file`, `edit_file`, `apply_patch`, and `fim_edit`, and only |
| 185 | against the filesystem targets named in their inputs (`path`/`target`/ |
| 186 | `destination`/`file_path`, `changes[].path`, and unified-diff / |
| 187 | `apply_patch`-envelope headers). A shell command that writes a protected path is **not** held by |
| 188 | repo law — those writes are still governed by the ordinary approval, sandbox, |
| 189 | and shell-write gates, not by this mechanism. |
| 190 | |
| 191 | ### Expert full base-prompt override (#3638) |
| 192 | |
| 193 | The global Constitution (the base system prompt, normally compiled in from |
| 194 | `crates/tui/src/prompts/text.rs` as `BASE_PROMPT`) can be replaced per-user |
| 195 | without rebuilding. This is |
| 196 | an expert escape hatch, not the normal `/constitution` guided setup output. |
| 197 | Because this is a prompt trust boundary, it takes **two deliberate steps** — a |
| 198 | file alone is not enough: |
| 199 | |
| 200 | 1. Drop the replacement at `~/.codewhale/prompts/constitution.md` (under |
| 201 | `$CODEWHALE_HOME` when set). |
| 202 | 2. Set the explicit opt-in flag `CODEWHALE_ALLOW_BASE_PROMPT_OVERRIDE=1` |
| 203 | (`true`/`on`/`yes` also accepted). |
| 204 | |
| 205 | If the file exists but the flag is unset, the override is **ignored** (with a |
| 206 | log line pointing to the flag) and the bundled Constitution stays in place. |
| 207 | This is intended for repurposing the TUI beyond software engineering — e.g. |
| 208 | long-form writing or document review — where the engineering-oriented base |
| 209 | prompt is a poor fit. It is loaded once at startup; a **missing or empty file |
| 210 | is a no-op**, so existing installs keep the bundled prompt. |
| 211 | |
| 212 | Scope is deliberately narrow: only the byte-stable **base prompt segment** is |
| 213 | overridable. Mode deltas, the approval policy, the tool taxonomy, Context |
| 214 | Management, and the Compaction Relay are still owned by Codewhale's runtime |
| 215 | assembly, so an override **cannot remove safety-relevant guidance** (sandbox, |
| 216 | approvals) — it only swaps the task/voice framing. To customize ordinary |
| 217 | personal behavior, prefer `/constitution`; to customize per-repo behavior, |
| 218 | prefer `AGENTS.md` + `.codewhale/constitution.json` above. |
| 219 | |
| 220 | ## Where It Looks |
| 221 | |
| 222 | Default config path: |
| 223 | |
| 224 | - `~/.codewhale/config.toml` |
| 225 | - Legacy fallback: `~/.deepseek/config.toml` |
| 226 | |
| 227 | Overrides: |
| 228 | |
| 229 | - CLI: `codewhale --config /path/to/config.toml` |
| 230 | - Env: `CODEWHALE_CONFIG_PATH=/path/to/config.toml` |
| 231 | - Legacy env alias: `DEEPSEEK_CONFIG_PATH=/path/to/config.toml` |
| 232 | |
| 233 | If both are set, `--config` wins. Environment variable overrides are applied after the file is loaded. |
| 234 | |
| 235 | ### TUI editability audit |
| 236 | |
| 237 | Inside the TUI, run `/config audit` to see which documented keys can be changed |
| 238 | from the current session, which ones can also be persisted, and which ones stay |
| 239 | file-only or restart-only. The audit includes current values for the high-impact |
| 240 | runtime controls such as `approval_policy`, `allow_shell`, |
| 241 | `stream_chunk_timeout_secs`, `base_url`, `mcp_config_path`, and the |
| 242 | `[subagents]` concurrency/depth/timeout keys. |
| 243 | |
| 244 | Use the command's "Command / reason" column as the source of truth before |
| 245 | editing by hand. For example, `/config approval_mode on-request --save` writes |
| 246 | top-level `approval_policy = "on-request"`, while provider base URLs are saved |
| 247 | but still require restarting the model client. |
| 248 | |
| 249 | ### User workspace entries |
| 250 | |
| 251 | Interactive Agent sessions expose shell tools by default with approval gating |
| 252 | unless you explicitly disable them. For a shell opt-in that should live in the |
| 253 | user's global config for noninteractive or durable-task profiles rather than in |
| 254 | the repository, add a workspace-scoped entry: |
| 255 | |
| 256 | ```toml |
| 257 | [workspace.'/absolute/path/to/project'] |
| 258 | allow_shell = true |
| 259 | ``` |
| 260 | |
| 261 | The entry applies only when the launched workspace path matches the table key. |
| 262 | The legacy `[projects."/absolute/path/to/project"]` table is also accepted for |
| 263 | this user-owned override. |
| 264 | |
| 265 | In interactive mode, the per-project overlay |
| 266 | `<workspace>/.codewhale/config.toml` is applied after this user entry. A |
| 267 | project-level `allow_shell = false` can still tighten the session; project-level |
| 268 | `allow_shell = true` is ignored. |
| 269 | |
| 270 | ### Per-project overlay (#485) |
| 271 | |
| 272 | When the TUI starts in a workspace that contains a regular-file |
| 273 | `<workspace>/.codewhale/config.toml`, the safe values declared in that file are |
| 274 | merged on top of the global config. Legacy |
| 275 | `<workspace>/.deepseek/config.toml` files are still read when the Codewhale path |
| 276 | is absent. Symlinked project config files are rejected. This lets a repo suggest |
| 277 | a model or tighten local safety posture without touching the user's |
| 278 | `~/.codewhale/config.toml`. Pass `--no-project-config` to skip the overlay for |
| 279 | one launch. |
| 280 | |
| 281 | Supported keys in the project overlay (top-level fields only): |
| 282 | |
| 283 | | Key | Effect | |
| 284 | |---|---| |
| 285 | | `model` | override `default_text_model` | |
| 286 | | `reasoning_effort` | force `"high"` / `"max"` for a complex repo | |
| 287 | | `approval_policy` | only values that tighten the user's current permission posture | |
| 288 | | `sandbox_mode` | only values that tighten the user's current sandbox posture | |
| 289 | | `notes_path` | keep notes in-repo | |
| 290 | | `max_subagents` | clamp sub-agent concurrency for a constrained repo (clamped to 1..=128) | |
| 291 | | `allow_shell` | `false` can disable shell access; `true` is ignored | |
| 292 | |
| 293 | The overlay is intentionally narrow — it covers the fields a repo |
| 294 | maintainer is most likely to want to standardize across contributors. |
| 295 | Credential, endpoint, provider-selection, MCP config, hooks, skills, |
| 296 | retry, hotbar bindings, and `instructions = [...]` settings stay user-global. |
| 297 | If a repo-local config declares `api_key`, `base_url`, `provider`, |
| 298 | `mcp_config_path`, `hotbar`, `allow_shell = true`, or `instructions`, |
| 299 | Codewhale ignores that key and keeps the user's global setting. |
| 300 | |
| 301 | The `codewhale` facade and `codewhale-tui` binary share the same config file for |
| 302 | DeepSeek auth and model defaults. `codewhale auth set --provider deepseek` (and |
| 303 | the legacy `codewhale login --api-key ...` alias) saves the key to |
| 304 | `~/.codewhale/config.toml` (migrating legacy `~/.deepseek/config.toml` on first |
| 305 | launch when needed), and `codewhale --model deepseek-v4-flash` is forwarded to |
| 306 | the TUI as `DEEPSEEK_MODEL`. |
| 307 | |
| 308 | That provider credential is distinct from the optional managed-product |
| 309 | account. `codewhale account login` starts the Codewhale browser device flow; |
| 310 | `codewhale account status` and `codewhale account logout` inspect or remove the |
| 311 | session for the selected `--profile`. Account sessions are stored in the OS |
| 312 | credential manager. A file-backed session store exists only as an explicit |
| 313 | development fallback. `codewhale account keys list|set|remove` manages the |
| 314 | signed-in account's BYOK vault without displaying secret values. The older |
| 315 | `codewhale cloud ...` spelling remains a command alias. |
| 316 | |
| 317 | ### Credential read precedence (#5197) |
| 318 | |
| 319 | Credential reads are **folder-independent by default**: every layer below is |
| 320 | user-global or process-scoped, so a key saved in one repo resolves identically |
| 321 | in every other repo. Repo-local config never carries credential material — the |
| 322 | project overlay above reads only its allowlisted keys and ignores `api_key`, |
| 323 | and a credential write aimed at a workspace-scoped config is rescoped to the |
| 324 | user-global `~/.codewhale/config.toml` (#5045, #5193). |
| 325 | |
| 326 | For the active provider, the runtime resolves the API key in this exact order |
| 327 | (first match wins): |
| 328 | |
| 329 | 1. **Route-specific auth contract.** Routes whose `auth_mode` disables API |
| 330 | keys stop here with no credential. OAuth routes use their explicitly |
| 331 | consented token: `openai-codex` reads `OPENAI_CODEX_ACCESS_TOKEN` or the |
| 332 | consent-granted Codex CLI login (read-only, never refreshed or rewritten); |
| 333 | `[providers.xai] auth_mode = "oauth"` reads Codewhale's own xAI |
| 334 | device-login store (or a consent-granted Grok CLI file). |
| 335 | 2. **Explicit CLI key.** `--api-key` forwarded with its source marker wins |
| 336 | over every saved slot; for `deepseek`/`deepseek-CN` it also wins over the |
| 337 | root `api_key`. |
| 338 | 3. **Config file `api_key`.** The `[providers.<name>] api_key` table slot for |
| 339 | the active provider, plus the legacy root `api_key` for |
| 340 | `deepseek`/`deepseek-CN` and the literal `provider = "custom"` route. |
| 341 | File-owned keys stay bound to their file-owned endpoint: when the |
| 342 | environment replaces the route's base URL with a custom host, the saved |
| 343 | key is not sent there. |
| 344 | 4. **`api_key_env` binding.** `[providers.<name>] api_key_env = "VAR"` reads |
| 345 | the named environment variable. For custom providers an unset or empty |
| 346 | binding is a loud error, not a silent fallback (#5104). |
| 347 | 5. **Secret store.** The durable per-provider slot written by |
| 348 | `codewhale auth set` (file-backed under `~/.codewhale/secrets/` by |
| 349 | default; the OS keyring only when explicitly selected). Skipped for named |
| 350 | custom routes, self-hosted providers, custom endpoints other than an |
| 351 | explicitly authenticated loopback, and routes whose `auth_mode` needs no |
| 352 | key. |
| 353 | 6. **Ambient environment.** The provider's own variable |
| 354 | (`DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `MOONSHOT_API_KEY`, …). |
| 355 | Ambient keys are only ever sent to the provider's official endpoint and |
| 356 | are skipped under the same conditions as the secret store. |
| 357 | 7. **Keyless fallback.** Self-hosted providers and loopback endpoints may run |
| 358 | with no credential; every other route fails with provider-specific setup |
| 359 | guidance. |
| 360 | |
| 361 | Legacy compatibility: `~/.deepseek/config.toml` is migrated into |
| 362 | `~/.codewhale/config.toml` on first launch, `DEEPSEEK_*` environment |
| 363 | variables remain accepted aliases for the `CODEWHALE_*` forms, and |
| 364 | `DEEPSEEK_SECRET_BACKEND` is the legacy alias for `CODEWHALE_SECRET_BACKEND`. |
| 365 | |
| 366 | Run `codewhale auth status` to inspect the active provider's config |
| 367 | file, OS keyring backend, environment variable, winning source, and last-four |
| 368 | label without printing the key itself. The command only probes the active |
| 369 | provider's keyring entry. |
| 370 | |
| 371 | For hosted, generic OpenAI-compatible, self-hosted, OpenAI Responses, or native |
| 372 | Anthropic providers, set `provider = "<id>"` or pass |
| 373 | `codewhale --provider <id>`. The canonical provider IDs are `deepseek`, |
| 374 | `nvidia-nim`, `openai`, `atlascloud`, `wanjie-ark`, `volcengine`, |
| 375 | `openrouter`, `xiaomi-mimo`, `novita`, `fireworks`, `siliconflow`, `arcee`, |
| 376 | `siliconflow-CN`, `moonshot`, `sglang`, `vllm`, `ollama`, `huggingface`, |
| 377 | `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, |
| 378 | `stepfun`, `minimax`, and `deepinfra`. |
| 379 | For the provider-by-provider registry, including wire protocol, auth variables, |
| 380 | default base URLs, model IDs, and capability metadata, see |
| 381 | [PROVIDERS.md](PROVIDERS.md). |
| 382 | The facade saves provider credentials to the shared user config and forwards |
| 383 | the resolved key, base URL, provider, and model to the TUI process. Use |
| 384 | `codewhale auth set --provider nvidia-nim --api-key "YOUR_NVIDIA_API_KEY"` or |
| 385 | `codewhale auth set --provider openai --api-key "YOUR_OPENAI_COMPATIBLE_API_KEY"` or |
| 386 | `codewhale auth set --provider atlascloud --api-key "YOUR_ATLASCLOUD_API_KEY"` or |
| 387 | `codewhale auth set --provider wanjie-ark --api-key "YOUR_WANJIE_API_KEY"` or |
| 388 | `codewhale auth set --provider xiaomi-mimo --api-key "YOUR_XIAOMI_KEY"` or |
| 389 | `codewhale auth set --provider fireworks --api-key "YOUR_FIREWORKS_API_KEY"` or |
| 390 | `codewhale auth set --provider siliconflow --api-key "YOUR_SILICONFLOW_API_KEY"` or |
| 391 | `codewhale auth set --provider arcee --api-key "YOUR_ARCEE_API_KEY"` or the |
| 392 | matching provider ID from [PROVIDERS.md](PROVIDERS.md) to save provider keys |
| 393 | through the facade. The generic `openai` provider defaults |
| 394 | to `https://api.openai.com/v1`, accepts `OPENAI_BASE_URL`, and defaults to |
| 395 | `deepseek-v4-pro` for OpenAI-compatible gateways. `atlascloud` defaults to |
| 396 | `https://api.atlascloud.ai/v1`, accepts `ATLASCLOUD_BASE_URL`, and uses |
| 397 | `deepseek-ai/deepseek-v4-flash` as its default model. `wanjie-ark` targets |
| 398 | Wanjie Ark's OpenAI-compatible endpoint at |
| 399 | `https://maas-openapi.wanjiedata.com/api/v1`, defaults to `deepseek-reasoner`, |
| 400 | and passes model IDs through unchanged because Wanjie model access is |
| 401 | account-scoped. SGLang, vLLM, and Ollama are |
| 402 | self-hosted and can run without an API key by default. Ollama defaults to |
| 403 | `http://localhost:11434/v1` and sends model tags such as `codewhale-coder:1.3b` |
| 404 | or `qwen2.5-coder:7b` unchanged. Self-hosted providers and loopback custom |
| 405 | URLs (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) do not read the secret store |
| 406 | unless API-key auth is explicitly requested; use an env var or config-file key |
| 407 | when a local server does require bearer auth. |
| 408 | SiliconFlow defaults to `https://api.siliconflow.com/v1`, accepts |
| 409 | `SILICONFLOW_BASE_URL`, and uses `deepseek-ai/DeepSeek-V4-Pro` by default. |
| 410 | `provider = "siliconflow-CN"` selects the China regional default |
| 411 | `https://api.siliconflow.cn/v1` with the `[providers.siliconflow_cn]` table and |
| 412 | `SILICONFLOW_API_KEY` credential slot. |
| 413 | Arcee AI defaults to `https://api.arcee.ai/api/v1`, accepts `ARCEE_BASE_URL`, |
| 414 | and uses `trinity-large-thinking` by default for Codewhale agent work. |
| 415 | `trinity-large-preview` is also listed as a direct Arcee API model; OpenRouter's |
| 416 | `arcee-ai/trinity-large-thinking` remains the OpenRouter namespaced form, while |
| 417 | the direct Arcee provider uses the bare `trinity-large-thinking` ID. Direct |
| 418 | Arcee large-model API calls are tracked as 256K-context BF16 serving; Thinking |
| 419 | is reasoning-capable, while Preview is not marked as a thinking model. |
| 420 | |
| 421 | ### Custom OpenAI-Compatible Gateways |
| 422 | |
| 423 | For a single third-party service that implements the OpenAI Chat Completions |
| 424 | API, the simplest setup is the built-in `openai` provider name pointed at the |
| 425 | gateway: |
| 426 | |
| 427 | ```toml |
| 428 | provider = "openai" |
| 429 | default_text_model = "your-model-id" |
| 430 | |
| 431 | [providers.openai] |
| 432 | api_key = "YOUR_OPENAI_COMPATIBLE_API_KEY" |
| 433 | base_url = "https://your-gateway.example/v1" |
| 434 | ``` |
| 435 | |
| 436 | Put the endpoint under `[providers.openai]`, not the legacy top-level |
| 437 | `base_url`, so the OpenAI-compatible provider receives it. `default_text_model` |
| 438 | is the model ID sent to the gateway; `[providers.openai].model` can be used as |
| 439 | the OpenAI-provider-specific override. |
| 440 | |
| 441 | If you keep several OpenAI-compatible gateways, or need a stable name for an |
| 442 | AgentProfile provider pin, define a user-named custom provider table: |
| 443 | |
| 444 | ```toml |
| 445 | provider = "lm-studio" |
| 446 | |
| 447 | [providers.lm-studio] |
| 448 | kind = "openai-compatible" |
| 449 | base_url = "http://127.0.0.1:1234/v1" |
| 450 | api_key = "lm-studio" |
| 451 | model = "qwen-2.5-7b" |
| 452 | ``` |
| 453 | |
| 454 | Custom provider names may be selected with `provider = "<name>"`, |
| 455 | `--provider <name>`, or an AgentProfile `provider = "<name>"` when the matching |
| 456 | `[providers.<name>]` table exists. |
| 457 | |
| 458 | StepFun has a first-class provider entry, so keep Coding Plan credentials and |
| 459 | base URL scoped to `[providers.stepfun]`: |
| 460 | |
| 461 | ```toml |
| 462 | provider = "stepfun" |
| 463 | |
| 464 | [providers.stepfun] |
| 465 | api_key = "YOUR_STEPFUN_API_KEY" |
| 466 | base_url = "https://api.stepfun.ai/step_plan/v1" |
| 467 | model = "step-3.7-flash" |
| 468 | ``` |
| 469 | |
| 470 | `/provider` setup asks which StepFun billing route the key belongs to — |
| 471 | pay-as-you-go (`https://api.stepfun.ai/v1`) or a Step Plan subscription |
| 472 | (`https://api.stepfun.ai/step_plan/v1`) — and validates the key against the |
| 473 | endpoint you pick before saving it. The answer is written to |
| 474 | `[providers.stepfun].base_url` and nowhere else. If that key already holds a |
| 475 | base URL Codewhale does not recognize as one of those two routes, the question |
| 476 | is skipped and your value is left untouched. |
| 477 | |
| 478 | Alibaba Bailian / Model Studio DashScope Qwen routes use the same OpenAI |
| 479 | provider shape: |
| 480 | |
| 481 | ```toml |
| 482 | provider = "openai" |
| 483 | |
| 484 | [providers.openai] |
| 485 | api_key = "YOUR_DASHSCOPE_API_KEY" |
| 486 | base_url = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1" |
| 487 | model = "qwen-plus" |
| 488 | context_window = 1000000 |
| 489 | ``` |
| 490 | |
| 491 | Use the regional DashScope `compatible-mode/v1` base URL that matches the |
| 492 | region of your API key. Codewhale keeps `qwen-plus` scoped to the `openai` |
| 493 | provider route and does not infer a different provider from the model prefix. |
| 494 | The same rule applies to all provider-prefixed model strings: a prefix such as |
| 495 | `deepseek-ai/...` or `deepseek/...` is a provider-owned wire ID under the |
| 496 | selected provider, not an automatic switch to the DeepSeek provider. |
| 497 | Set `context_window` to the gateway/model's real total context window when it |
| 498 | differs from Codewhale's static model metadata. See |
| 499 | [Context length (context window)](#context-length-context-window) for the full |
| 500 | resolution order and for how to check which value is in effect. |
| 501 | |
| 502 | If the gateway accepts `POST /chat/completions` but rejects |
| 503 | `/v1/chat/completions`, set a provider-local `path_suffix`: |
| 504 | |
| 505 | ```toml |
| 506 | [providers.openai] |
| 507 | base_url = "https://your-gateway.example/v1" |
| 508 | path_suffix = "/chat/completions" |
| 509 | ``` |
| 510 | |
| 511 | The suffix applies only to chat-completion requests. Model listing and |
| 512 | DeepSeek beta paths keep their built-in routing so a generic gateway override |
| 513 | does not accidentally rewrite `/models` or `/beta/completions`. |
| 514 | |
| 515 | For private gateways with broken or intercepted certificates, use |
| 516 | `SSL_CERT_FILE` with a trusted CA bundle. The legacy provider-table key |
| 517 | `insecure_skip_tls_verify = true` is still parsed so `codewhale doctor` can |
| 518 | report stale configs, but provider clients reject it instead of disabling TLS |
| 519 | certificate verification. |
| 520 | |
| 521 | Local HTTP endpoints such as Ollama, SGLang, and vLLM are allowed by default |
| 522 | when they use localhost or loopback addresses. For a non-local `http://` |
| 523 | gateway, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network: |
| 524 | |
| 525 | ```bash |
| 526 | DEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale |
| 527 | ``` |
| 528 | |
| 529 | Third-party OpenAI-compatible gateways that need extra request headers can set |
| 530 | `http_headers = { "X-Model-Provider-Id" = "your-model-provider" }` at the top |
| 531 | level or under a provider table such as `[providers.deepseek]`. When configured, |
| 532 | codewhale sends those custom headers on model API requests. The equivalent |
| 533 | environment override is `DEEPSEEK_HTTP_HEADERS`, using comma-separated |
| 534 | `name=value` pairs such as |
| 535 | `X-Model-Provider-Id=your-model-provider,X-Gateway-Route=dev`. `Authorization` |
| 536 | and `Content-Type` are managed by the client and are not overridden by this |
| 537 | setting. |
| 538 | |
| 539 | ### Vision Model |
| 540 | |
| 541 | Codewhale's chat provider and `image_analyze` tool are configured separately. |
| 542 | The main chat path remains the selected text/tool provider; image analysis runs |
| 543 | through `[vision_model]` when the `vision_model` feature is enabled. |
| 544 | |
| 545 | Xiaomi's current image-understanding docs include `mimo-v2.5` for image input. |
| 546 | To use MiMo for `image_analyze`, configure the vision model explicitly: |
| 547 | |
| 548 | ```toml |
| 549 | [features] |
| 550 | vision_model = true |
| 551 | |
| 552 | [vision_model] |
| 553 | model = "mimo-v2.5" |
| 554 | api_key = "YOUR_XIAOMI_KEY" |
| 555 | base_url = "https://api.xiaomimimo.com/v1" |
| 556 | ``` |
| 557 | |
| 558 | The example above uses Xiaomi MiMo's pay-as-you-go OpenAI-compatible endpoint. |
| 559 | If you are using a Token Plan key (`tp-...`) for `[vision_model]`, you must set |
| 560 | `base_url` explicitly because this generic OpenAI-compatible block does not |
| 561 | auto-select MiMo endpoints. Use |
| 562 | `https://token-plan-sgp.xiaomimimo.com/v1` for Singapore accounts, |
| 563 | `https://token-plan-cn.xiaomimimo.com/v1` for China-region accounts, or |
| 564 | `https://token-plan-ams.xiaomimimo.com/v1` for Europe/Amsterdam accounts. |
| 565 | |
| 566 | ### Auto Model Routing (`[auto.router]`) |
| 567 | |
| 568 | With `model = "auto"`, Codewhale routes each turn between a strong and a cheap |
| 569 | model. The routing decision comes from a small classifier call, or from a local |
| 570 | heuristic when no classifier route is available. |
| 571 | |
| 572 | **There is no default classifier.** With `[auto.router]` unset, Auto is local |
| 573 | and free: it uses the heuristic and makes no classifier call, whatever keys you |
| 574 | hold. Holding a DeepSeek key used to elect `deepseek-v4-flash` automatically; |
| 575 | that was removed because it spent tokens on a route the user never chose and |
| 576 | privileged one provider over the rest (`crates/tui/src/config.rs:2392-2402`). |
| 577 | Electing a network classifier is now something you write down. |
| 578 | |
| 579 | Point the classifier at any configured provider with `[auto.router]`: |
| 580 | |
| 581 | ```toml |
| 582 | [auto.router] |
| 583 | provider = "zai" |
| 584 | model = "glm-5-turbo" |
| 585 | thinking = "off" # optional; defaults to off |
| 586 | ``` |
| 587 | |
| 588 | A classifier call happens only when `[auto.router]` is set *and* that provider |
| 589 | has a key — `router_available = router_configured && has_api_key_for(...)` |
| 590 | (`crates/tui/src/model_inventory.rs:206-218`). Either condition failing means |
| 591 | the heuristic decides, not a failure. The turn's route receipt (`/status` → |
| 592 | Auto) records which one it was. |
| 593 | |
| 594 | To bootstrap MCP and skills directories at their resolved paths, run `codewhale-tui setup`. |
| 595 | To only scaffold MCP, run `codewhale-tui mcp init`. |
| 596 | |
| 597 | Note: `setup`, `doctor`, `mcp`, `features`, `sessions`, `resume`/`fork`, `exec`, |
| 598 | `review`, and `eval` are subcommands of the `codewhale-tui` binary, and the |
| 599 | `codewhale` dispatcher accepts every one of them as a passthrough |
| 600 | (`TuiPassthroughArgs`, `crates/cli/src/lib.rs:244-429`) — the dispatcher's |
| 601 | surface is a **superset**, not a distinct set. It adds commands the TUI binary |
| 602 | does not have of its own: `auth`, `config`, `model`, `thread`, `sandbox`, |
| 603 | `app-server`, `mcp-server`, `completions`, `login`/`logout`, `account`, |
| 604 | `metrics`, `update`, `lane`, `workflow`, `web`. Plain prompts are forwarded to |
| 605 | `codewhale-tui`. |
| 606 | |
| 607 | ### Startup Update Checks |
| 608 | |
| 609 | By default, the TUI starts a background check for the latest stable Codewhale |
| 610 | release and shows a short toast only when a newer release is available and the |
| 611 | official release assets are complete. The check never blocks startup, never |
| 612 | blocks a turn, and fails silently when offline. |
| 613 | |
| 614 | Disable the startup check entirely for air-gapped, corporate-proxy, or managed |
| 615 | desktop environments: |
| 616 | |
| 617 | ```toml |
| 618 | [update] |
| 619 | check_for_updates = false |
| 620 | ``` |
| 621 | |
| 622 | #### Throttling |
| 623 | |
| 624 | The answer is cached in `~/.codewhale/update-check.json` and reused for |
| 625 | `check_interval_hours` (default `24`). Only the *network request* is throttled — |
| 626 | the notice still appears on every launch while an update is outstanding. Set `0` |
| 627 | to check on every launch. |
| 628 | |
| 629 | ```toml |
| 630 | [update] |
| 631 | check_interval_hours = 24 |
| 632 | ``` |
| 633 | |
| 634 | A failed check is not cached, so an outage does not suppress the notice until |
| 635 | the interval elapses. |
| 636 | |
| 637 | #### Automatic suppression |
| 638 | |
| 639 | Checks are skipped, without contacting the network, when any of these is set to |
| 640 | a non-falsey value: |
| 641 | |
| 642 | | Variable | Why | |
| 643 | | --- | --- | |
| 644 | | `CODEWHALE_NO_UPDATE_CHECK` | Explicit opt-out. | |
| 645 | | `NO_UPDATE_NOTIFIER` | The cross-CLI convention, honored for compatibility. | |
| 646 | | `CI`, `CONTINUOUS_INTEGRATION`, `GITHUB_ACTIONS`, `GITLAB_CI`, `BUILDKITE`, `CIRCLECI`, `JENKINS_URL`, `TEAMCITY_VERSION`, `TF_BUILD` | Automated build; nobody is at the terminal. | |
| 647 | |
| 648 | Values of `""`, `0`, `false`, `no`, and `off` do not count as set, so a |
| 649 | `CI=false` export does not disable checks for ordinary users. |
| 650 | |
| 651 | #### Which update command is offered |
| 652 | |
| 653 | Codewhale never installs anything on its own — it only tells you an update |
| 654 | exists. The command it names depends on how the running binary was installed, |
| 655 | detected from its path: |
| 656 | |
| 657 | | Install | Command offered | |
| 658 | | --- | --- | |
| 659 | | GitHub release binary (including Termux) | `codewhale update` | |
| 660 | | npm (`node_modules` on the path) | `npm install -g codewhale@latest` | |
| 661 | | Homebrew (`Cellar` / `linuxbrew` prefix) | `brew upgrade deepseek-tui` | |
| 662 | | `cargo install` (`~/.cargo/bin`) | `cargo install codewhale-cli --locked --force` | |
| 663 | |
| 664 | For package-managed installs the notice also warns against `codewhale update`: |
| 665 | replacing a binary Homebrew or npm owns leaves the manager describing a version |
| 666 | that is no longer on disk, and the next upgrade silently reverts you. |
| 667 | |
| 668 | Override the detection with `CODEWHALE_INSTALL_METHOD=npm|homebrew|cargo|binary` |
| 669 | if you relocated the binary somewhere the path heuristics cannot read. |
| 670 | |
| 671 | To redirect the startup check, set `update_uri` to an internal endpoint that |
| 672 | returns GitHub-compatible latest-release JSON. Minimal mirror metadata with a |
| 673 | `tag_name` field is accepted; if `assets` are present, Codewhale requires the |
| 674 | same uploaded asset set as the official release before showing the toast. |
| 675 | |
| 676 | ```toml |
| 677 | [update] |
| 678 | check_for_updates = true |
| 679 | update_uri = "https://internal.mirror.example/codewhale/releases/latest" |
| 680 | ``` |
| 681 | |
| 682 | When `update_uri` is not set, startup checks honor release mirror environment |
| 683 | variables such as `CODEWHALE_RELEASE_BASE_URL` before falling back to the |
| 684 | official GitHub API endpoint. If a configured `update_uri` cannot be fetched or |
| 685 | parsed and a release mirror env var is set, the TUI falls back to that mirror |
| 686 | instead of failing startup. |
| 687 | |
| 688 | ## Context length (context window) |
| 689 | |
| 690 | Also called context size, context limit, max context, or window. This is the |
| 691 | total token window Codewhale budgets against, and it drives the header/footer |
| 692 | context percent, the auto-compaction trigger, context-pressure checks, and the |
| 693 | request output cap. If Codewhale compacts at 128K on a model you know serves a |
| 694 | 1M window, this is the setting to change (#5134). |
| 695 | |
| 696 | **See what is in effect, and where the value came from.** Every one of these |
| 697 | prints the resolved window *and* its source: |
| 698 | |
| 699 | - `/status` — a `Context window:` row with the percent and token counts, and a |
| 700 | `Window source:` row naming the provenance and the exact key that overrides |
| 701 | it. |
| 702 | - `/config` → Provider — `Context window` (your override, or `(not set)`) and |
| 703 | `Effective context window` (`1048576 tokens · configured`). Typing |
| 704 | `context length` in the `/config` filter jumps straight to them. |
| 705 | - `/context report` — `Window: 1048576 tokens (12.4% used, ...; source: configured)`. |
| 706 | - `/context json` — machine-readable `context_window_tokens` and |
| 707 | `context_window_source`. |
| 708 | |
| 709 | **Change it** with the provider-table key `context_window`: |
| 710 | |
| 711 | ```toml |
| 712 | [providers.moonshot] |
| 713 | context_window = 1048576 |
| 714 | ``` |
| 715 | |
| 716 | or from the CLI: |
| 717 | |
| 718 | ```bash |
| 719 | codewhale config set providers.moonshot.context_window 1048576 |
| 720 | codewhale config unset providers.moonshot.context_window # back to automatic |
| 721 | ``` |
| 722 | |
| 723 | Use the table for the provider you are actually on (`providers.openai`, |
| 724 | `providers.deepseek`, `providers.moonshot`, …); `/status` names it for you. The |
| 725 | value is a positive token count for the route's *total* window. |
| 726 | |
| 727 | ### How the effective window is resolved |
| 728 | |
| 729 | First match wins, and the source label each surface prints is exactly this |
| 730 | rung: |
| 731 | |
| 732 | 1. `configured` — `[providers.<name>] context_window` in `config.toml`. A hard |
| 733 | override: nothing below it can raise or lower the result. Read-time aliases: |
| 734 | `contextWindow`, `context_window_tokens`, `contextWindowTokens`, |
| 735 | `context_length`, `contextLength`. |
| 736 | 2. `provider-reported` — route-scoped 1M metadata a provider actually reported |
| 737 | for the Kimi Code `k3` route, when it was observed within the last 24 hours. |
| 738 | 3. `static Kimi Code safe floor` — 262,144 tokens for Kimi Code memberships, |
| 739 | because 1M access is plan-gated (Allegretto and above). |
| 740 | 4. `catalog` — the bundled route catalog (hand-curated offerings first, then |
| 741 | the bundled Models.dev rows). For `openai-codex`, a fresh (under 24 hours) |
| 742 | `$CODEX_HOME` model roster corrects this rung. |
| 743 | 5. `fallback` — the static per-provider capability table: 200,000 for |
| 744 | Anthropic-wire routes, 128,000 for `openai-codex`, 8,192 for Ollama, |
| 745 | otherwise Codewhale's static per-model metadata, and finally 128,000 when |
| 746 | the model is unknown. |
| 747 | |
| 748 | There is no environment variable for the context window, and no per-model |
| 749 | override key. The per-provider `context_window` is the only user knob, which is |
| 750 | also why it is the right one to set when a gateway or self-hosted runtime |
| 751 | serves a window Codewhale's catalog does not model. Codewhale will not invent a |
| 752 | window it cannot justify — it falls back to a conservative value and labels it |
| 753 | `fallback`. |
| 754 | |
| 755 | ### Adjacent knobs |
| 756 | |
| 757 | - `auto_compact_threshold_percent` (settings.toml; also accepted as |
| 758 | `auto_compact_threshold`; `10`–`100`, default `80`): the share of the usable |
| 759 | input budget — the window minus reserved output headroom — at which |
| 760 | auto-compaction fires. Editable from `/config`. Raising the window without |
| 761 | touching this raises the absolute compaction point along with it. |
| 762 | - `auto_compact` (settings.toml, on/off): turns automatic compaction off |
| 763 | entirely; `/compact` and Ctrl+L stay available. |
| 764 | - `CODEWHALE_MAX_OUTPUT_TOKENS` (environment variable; legacy alias |
| 765 | `DEEPSEEK_MAX_OUTPUT_TOKENS`): overrides the requested output cap, which is |
| 766 | otherwise derived from the window. There is no `max_output_tokens` key in |
| 767 | `config.toml`. |
| 768 | |
| 769 | See [Settings File](#settings-file-persistent-ui-preferences) for the |
| 770 | compaction settings and [Token Quantities and |
| 771 | Drivers](#token-quantities-and-drivers) for what each displayed token number |
| 772 | actually measures. |
| 773 | |
| 774 | ## Profiles |
| 775 | |
| 776 | You can define multiple profiles in the same file: |
| 777 | |
| 778 | ```toml |
| 779 | api_key = "PERSONAL_KEY" |
| 780 | default_text_model = "deepseek-v4-pro" |
| 781 | |
| 782 | [profiles.work] |
| 783 | api_key = "WORK_KEY" |
| 784 | base_url = "https://api.deepseek.com/beta" |
| 785 | |
| 786 | [profiles.nvidia-nim] |
| 787 | provider = "nvidia-nim" |
| 788 | api_key = "NVIDIA_KEY" |
| 789 | base_url = "https://integrate.api.nvidia.com/v1" |
| 790 | default_text_model = "deepseek-ai/deepseek-v4-pro" |
| 791 | |
| 792 | [profiles.fireworks] |
| 793 | provider = "fireworks" |
| 794 | default_text_model = "accounts/fireworks/models/deepseek-v4-pro" |
| 795 | |
| 796 | [profiles.siliconflow] |
| 797 | provider = "siliconflow" |
| 798 | default_text_model = "deepseek-ai/DeepSeek-V4-Pro" |
| 799 | |
| 800 | [profiles.siliconflow.providers.siliconflow] |
| 801 | base_url = "https://api.siliconflow.com/v1" |
| 802 | |
| 803 | [profiles.openai-compatible] |
| 804 | provider = "openai" |
| 805 | |
| 806 | [profiles.openai-compatible.providers.openai] |
| 807 | base_url = "https://openai-compatible.example/v4" |
| 808 | model = "glm-5" |
| 809 | |
| 810 | [profiles.atlascloud] |
| 811 | provider = "atlascloud" |
| 812 | |
| 813 | [profiles.atlascloud.providers.atlascloud] |
| 814 | base_url = "https://api.atlascloud.ai/v1" |
| 815 | model = "deepseek-ai/deepseek-v4-flash" |
| 816 | |
| 817 | [profiles.sglang] |
| 818 | provider = "sglang" |
| 819 | base_url = "http://localhost:30000/v1" |
| 820 | default_text_model = "deepseek-ai/DeepSeek-V4-Pro" |
| 821 | |
| 822 | [profiles.vllm] |
| 823 | provider = "vllm" |
| 824 | base_url = "http://localhost:8000/v1" |
| 825 | default_text_model = "deepseek-ai/DeepSeek-V4-Pro" |
| 826 | |
| 827 | [profiles.ollama] |
| 828 | provider = "ollama" |
| 829 | base_url = "http://localhost:11434/v1" |
| 830 | default_text_model = "codewhale-coder:1.3b" |
| 831 | ``` |
| 832 | |
| 833 | Select a profile with: |
| 834 | |
| 835 | - CLI: `codewhale --profile work` |
| 836 | - Env: `DEEPSEEK_PROFILE=work` |
| 837 | |
| 838 | If a profile is selected but missing, codewhale exits with an error listing available profiles. |
| 839 | |
| 840 | ## Harness Profiles |
| 841 | |
| 842 | v0.9 adds a config data model for model-specific harness posture. This is a |
| 843 | preview schema: it can be parsed and tested, but runtime provider/model |
| 844 | selection and prompt/tool behavior are wired in later v0.9 slices. |
| 845 | When no configured profile matches, the resolver falls back to built-in seed |
| 846 | profiles for the model families listed in the cutline doc. Configured profiles |
| 847 | always take precedence over those seeds. |
| 848 | |
| 849 | ```toml |
| 850 | [[harness_profiles]] |
| 851 | provider_route = "deepseek" |
| 852 | model_pattern = "deepseek-v4.*" |
| 853 | |
| 854 | [harness_profiles.posture] |
| 855 | kind = "cache-heavy" # standard | cache-heavy | lean | custom |
| 856 | max_subagents = 10 # 0 means runtime default |
| 857 | prefer_codebase_search = false |
| 858 | compaction_strategy = "prefix-cache" # default | prefix-cache | aggressive |
| 859 | tool_surface = "full" # full | read-only | auto |
| 860 | safety_posture = "standard" # standard | strict | permissive |
| 861 | ``` |
| 862 | |
| 863 | Unknown posture names or unknown keys inside a harness profile fail config |
| 864 | deserialization instead of silently becoming `custom`. That is intentional: |
| 865 | once runtime wiring consumes these profiles, a typo should be visible. |
| 866 | The v0.9 implementation order and automatic-creator boundary are documented in |
| 867 | [`HARNESS_PROFILE_CUTLINE.md`](rfcs/HARNESS_PROFILE_CUTLINE.md). |
| 868 | |
| 869 | ## Environment Variables |
| 870 | |
| 871 | Most runtime environment variables override config values. API-key variables are |
| 872 | fallbacks after saved config and keyring credentials. |
| 873 | |
| 874 | The three user-facing slots — provider, model, base URL — expose `CODEWHALE_*` |
| 875 | aliases. When both forms are set the `CODEWHALE_*` value wins; the |
| 876 | `DEEPSEEK_*` form is kept for older shells: |
| 877 | |
| 878 | - `CODEWHALE_PROVIDER` (preferred) / `DEEPSEEK_PROVIDER` (legacy alias) — |
| 879 | `deepseek|deepseek-anthropic|nvidia-nim|openai|atlascloud|wanjie-ark|volcengine|openrouter|xiaomi-mimo|novita|fireworks|siliconflow|arcee|siliconflow-CN|moonshot|sglang|vllm|ollama|huggingface|together|qianfan|openai-codex|anthropic|openmodel|zai|stepfun|minimax|deepinfra` |
| 880 | - `CODEWHALE_MODEL` (preferred) / `DEEPSEEK_MODEL` (legacy alias) — default model for the active provider |
| 881 | - `CODEWHALE_BASE_URL` (preferred) / `DEEPSEEK_BASE_URL` (legacy alias) — base URL for the active provider |
| 882 | |
| 883 | `CODEWHALE_BASE_URL` applies to the **active** route only. A request pinned to |
| 884 | another provider — a subagent or fleet child, a routed tool, the per-turn |
| 885 | auto-router, a picker preview — resolves its endpoint from that provider's own |
| 886 | `[providers.<table>]`, then its provider-scoped variable (`MOONSHOT_BASE_URL`, |
| 887 | `OPENAI_BASE_URL`, …), then that provider's default. It never inherits the |
| 888 | active session's host, and a custom route with no configured `base_url` fails |
| 889 | closed on a loopback placeholder rather than borrowing another provider's |
| 890 | endpoint. The legacy root `base_url` behaves the same way: written in your |
| 891 | config file it stays shared by the DeepSeek and DeepSeek-CN identities as it |
| 892 | always has, but a value the environment wrote belongs to the identity it was |
| 893 | addressed to. A managed-config overlay that supplies or reselects the effective |
| 894 | route's endpoint takes the generic override away from every route. |
| 895 | |
| 896 | Remaining variables: |
| 897 | |
| 898 | - `DEEPSEEK_API_KEY` |
| 899 | - `DEEPSEEK_ANTHROPIC_BASE_URL` |
| 900 | - `DEEPSEEK_HTTP_HEADERS` (custom model request headers, comma-separated `name=value` pairs) |
| 901 | - `DEEPSEEK_DEFAULT_TEXT_MODEL` (extra legacy alias of `DEEPSEEK_MODEL`) |
| 902 | - `DEEPSEEK_STREAM_IDLE_TIMEOUT_SECS` (stream idle timeout in seconds; default `900`, clamped to `1..=3600`) |
| 903 | - `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) |
| 904 | - `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. |
| 905 | - `NVIDIA_API_KEY` or `NVIDIA_NIM_API_KEY` (preferred when provider is `nvidia-nim`; falls back to `DEEPSEEK_API_KEY`) |
| 906 | - `NVIDIA_NIM_BASE_URL`, `NIM_BASE_URL`, or `NVIDIA_BASE_URL` |
| 907 | - `NVIDIA_NIM_MODEL` |
| 908 | - `OPENAI_API_KEY` |
| 909 | - `OPENAI_BASE_URL` |
| 910 | - `OPENAI_MODEL` |
| 911 | - `ATLASCLOUD_API_KEY` |
| 912 | - `ATLASCLOUD_BASE_URL` |
| 913 | - `ATLASCLOUD_MODEL` |
| 914 | - `WANJIE_ARK_API_KEY`, `WANJIE_API_KEY`, or `WANJIE_MAAS_API_KEY` |
| 915 | - `WANJIE_ARK_BASE_URL`, `WANJIE_BASE_URL`, or `WANJIE_MAAS_BASE_URL` |
| 916 | - `WANJIE_ARK_MODEL`, `WANJIE_MODEL`, or `WANJIE_MAAS_MODEL` |
| 917 | - `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, or `ARK_API_KEY` |
| 918 | - `VOLCENGINE_BASE_URL`, `VOLCENGINE_ARK_BASE_URL`, or `ARK_BASE_URL` |
| 919 | - `VOLCENGINE_MODEL` or `VOLCENGINE_ARK_MODEL` |
| 920 | - `OPENROUTER_API_KEY` |
| 921 | - `OPENROUTER_BASE_URL` |
| 922 | - `OPENROUTER_MODEL` |
| 923 | - `XIAOMI_MIMO_TOKEN_PLAN_API_KEY`, `MIMO_TOKEN_PLAN_API_KEY`, `XIAOMI_MIMO_API_KEY`, `XIAOMI_API_KEY`, or `MIMO_API_KEY` |
| 924 | - `XIAOMI_MIMO_BASE_URL` or `MIMO_BASE_URL` |
| 925 | - `XIAOMI_MIMO_MODEL` or `MIMO_MODEL` |
| 926 | - `XIAOMI_MIMO_MODE` or `MIMO_MODE` (`token-plan-sgp`, `token-plan-cn`, |
| 927 | `token-plan-ams`, or `pay-as-you-go`) |
| 928 | - `NOVITA_API_KEY` |
| 929 | - `NOVITA_BASE_URL` |
| 930 | - `NOVITA_MODEL` |
| 931 | - `FIREWORKS_API_KEY` |
| 932 | - `FIREWORKS_BASE_URL` |
| 933 | - `FIREWORKS_MODEL` |
| 934 | - `HUGGINGFACE_API_KEY` or `HF_TOKEN` (`HF_TOKEN` is a fallback alias accepted when provider is `huggingface`) |
| 935 | - `HUGGINGFACE_BASE_URL` or `HF_BASE_URL` |
| 936 | - `HUGGINGFACE_MODEL` or `HF_MODEL` |
| 937 | - `SILICONFLOW_API_KEY` |
| 938 | - `SILICONFLOW_BASE_URL` |
| 939 | - `SILICONFLOW_MODEL` |
| 940 | - `ARCEE_API_KEY` |
| 941 | - `ARCEE_BASE_URL` |
| 942 | - `ARCEE_MODEL` |
| 943 | - `TOGETHER_API_KEY` |
| 944 | - `TOGETHER_BASE_URL` |
| 945 | - `TOGETHER_MODEL` |
| 946 | - `QIANFAN_API_KEY` or `BAIDU_QIANFAN_API_KEY` |
| 947 | - `QIANFAN_BASE_URL` or `BAIDU_QIANFAN_BASE_URL` |
| 948 | - `QIANFAN_MODEL` or `BAIDU_QIANFAN_MODEL` |
| 949 | - `OPENAI_CODEX_ACCESS_TOKEN` or `CODEX_ACCESS_TOKEN` |
| 950 | - `OPENAI_CODEX_BASE_URL` or `CODEX_BASE_URL` |
| 951 | - `OPENAI_CODEX_MODEL` or `CODEX_MODEL` |
| 952 | - `OPENAI_CODEX_ACCOUNT_ID` or `CODEX_ACCOUNT_ID` |
| 953 | - `ANTHROPIC_API_KEY` |
| 954 | - `ANTHROPIC_BASE_URL` |
| 955 | - `ANTHROPIC_MODEL` |
| 956 | - `ZAI_API_KEY` or `Z_AI_API_KEY` |
| 957 | - `ZAI_BASE_URL` or `Z_AI_BASE_URL` |
| 958 | - `ZAI_MODEL` or `Z_AI_MODEL` |
| 959 | - `STEPFUN_API_KEY` or `STEP_API_KEY` |
| 960 | - `STEPFUN_BASE_URL` or `STEP_BASE_URL` |
| 961 | - `STEPFUN_MODEL` or `STEP_MODEL` |
| 962 | - `MINIMAX_API_KEY` |
| 963 | - `MINIMAX_BASE_URL` |
| 964 | - `MINIMAX_MODEL` |
| 965 | - `DEEPINFRA_API_KEY` or `DEEPINFRA_TOKEN` |
| 966 | - `DEEPINFRA_BASE_URL` |
| 967 | - `DEEPINFRA_MODEL` |
| 968 | - `MOONSHOT_API_KEY` or `KIMI_API_KEY` |
| 969 | - `MOONSHOT_BASE_URL` or `KIMI_BASE_URL` |
| 970 | - `MOONSHOT_MODEL`, `KIMI_MODEL_NAME`, or `KIMI_MODEL` |
| 971 | - `SGLANG_BASE_URL` |
| 972 | - `SGLANG_MODEL` |
| 973 | - `SGLANG_API_KEY` (optional; many localhost SGLang servers do not require auth) |
| 974 | - `VLLM_BASE_URL` |
| 975 | - `VLLM_MODEL` |
| 976 | - `VLLM_API_KEY` (optional; many localhost vLLM servers do not require auth) |
| 977 | - `OLLAMA_BASE_URL` |
| 978 | - `OLLAMA_MODEL` |
| 979 | - `OLLAMA_API_KEY` (optional; many localhost Ollama servers do not require auth) |
| 980 | For every product-level `CODEWHALE_*` variable below, the matching legacy |
| 981 | `DEEPSEEK_*` name is still read as a compatibility fallback; when both are set, |
| 982 | the `CODEWHALE_*` value wins. |
| 983 | |
| 984 | - `CODEWHALE_LOG_LEVEL` or `RUST_LOG` (`info`/`debug`/`trace` enables lightweight verbose logs) |
| 985 | - `CODEWHALE_SKILLS_DIR` |
| 986 | - `CODEWHALE_MCP_CONFIG` |
| 987 | - `CODEWHALE_NOTES_PATH` |
| 988 | - `CODEWHALE_MEMORY` (`1|on|true|yes|y|enabled` turns user memory on) |
| 989 | - `CODEWHALE_MEMORY_PATH` |
| 990 | - `CODEWHALE_TELEMETRY` / `DEEPSEEK_TELEMETRY` (legacy alias) — opt-in product |
| 991 | telemetry, off by default. Accepts `0|1|true|false|yes|no|on|off|enabled| |
| 992 | disabled`. An explicit "off" is a **floor**: it beats `--telemetry true` and |
| 993 | `telemetry = true` in config, and a value this list cannot read also resolves |
| 994 | to off, because a typo in a kill switch must never resolve to "on". See |
| 995 | [`TELEMETRY.md`](TELEMETRY.md). |
| 996 | - `CODEWHALE_TELEMETRY_ENDPOINT` / `DEEPSEEK_TELEMETRY_ENDPOINT` (legacy alias) |
| 997 | — `https://`, or plain `http://` only for loopback. Overrides the config file. |
| 998 | Unset selects the shipped default, |
| 999 | `https://telemetry.codewhale.net/v1/telemetry`; setting it to the **empty |
| 1000 | string** routes batches to a local dry-run file and contacts nobody. Either |
| 1001 | way it only decides where an already-enabled session sends — it cannot turn |
| 1002 | telemetry on. |
| 1003 | - `CODEWHALE_ALLOW_SHELL` (`1`/`true` enables) |
| 1004 | - `CODEWHALE_APPROVAL_POLICY` (`on-request|untrusted|never`) |
| 1005 | - `CODEWHALE_SANDBOX_MODE` (`read-only|workspace-write|danger-full-access|external-sandbox`) |
| 1006 | - `CODEWHALE_MANAGED_CONFIG_PATH` |
| 1007 | - `CODEWHALE_REQUIREMENTS_PATH` |
| 1008 | - `CODEWHALE_MAX_SUBAGENTS` (clamped to `1..=128`) |
| 1009 | - `CODEWHALE_TASKS_DIR` (runtime task queue/artifact storage, default |
| 1010 | `~/.codewhale/tasks`, with legacy `~/.deepseek/tasks` fallback when only the |
| 1011 | legacy directory exists) |
| 1012 | - `CODEWHALE_ALLOW_INSECURE_HTTP` (`1`/`true` allows non-local `http://` base URLs; default is reject) |
| 1013 | - `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) |
| 1014 | - `CODEWHALE_HOME` (override the base data directory; defaults to `~/.codewhale`). |
| 1015 | If you previously exported `DEEPSEEK_HOME`, rename it to `CODEWHALE_HOME`; |
| 1016 | the old env var is not used for new Codewhale state paths. |
| 1017 | - `CODEWHALE_RELEASE_BASE_URL` (release asset mirror used by `codewhale update` |
| 1018 | and by TUI startup update checks when `[update].update_uri` is not set, or as |
| 1019 | a fallback when that configured URI cannot be fetched) |
| 1020 | - `CODEWHALE_AUTOMATIONS_DIR` (override the automations storage directory; uses |
| 1021 | `~/.codewhale/automations` by default, with legacy `~/.deepseek/automations` |
| 1022 | fallback when only the legacy directory exists) |
| 1023 | - `NO_ANIMATIONS` (`1|true|yes|on` forces `low_motion = true` and |
| 1024 | `fancy_animations = false` at startup, regardless of the saved |
| 1025 | settings; see [`docs/ACCESSIBILITY.md`](./ACCESSIBILITY.md)). |
| 1026 | - `SSL_CERT_FILE` — corporate-proxy / TLS-inspecting MITM users |
| 1027 | point this at a PEM bundle (or single DER cert) and the cert(s) |
| 1028 | get added alongside the platform's system trust store. Failures |
| 1029 | log a warning and continue — the existing system roots still |
| 1030 | apply. |
| 1031 | |
| 1032 | ### Instruction sources (`instructions = [...]`, #454) |
| 1033 | |
| 1034 | Add a list of additional system-prompt sources that get |
| 1035 | concatenated, in declared order, alongside the auto-loaded |
| 1036 | `AGENTS.md`: |
| 1037 | |
| 1038 | ```toml |
| 1039 | instructions = [ |
| 1040 | "./AGENTS.md", |
| 1041 | "~/.codewhale/global.md", |
| 1042 | "~/team/agents-shared.md", |
| 1043 | ] |
| 1044 | ``` |
| 1045 | |
| 1046 | Rules: |
| 1047 | |
| 1048 | - Paths run through `expand_path` so `~` and env vars work. |
| 1049 | - Each file is capped at 100 KiB; oversized files are |
| 1050 | truncated with a `[…elided]` marker rather than skipped. |
| 1051 | - Missing files are skipped with a tracing warning so a stale |
| 1052 | entry doesn't fail the launch. |
| 1053 | - Only user-owned config, profiles, and managed config may set this array. |
| 1054 | Project config (`<workspace>/.codewhale/config.toml`, or legacy |
| 1055 | `<workspace>/.deepseek/config.toml`) ignores `instructions` so a cloned repo |
| 1056 | cannot choose arbitrary local files to place into the prompt. |
| 1057 | |
| 1058 | ### Hooks |
| 1059 | |
| 1060 | Hooks are a **TUI runtime feature**. They fire from the interactive TUI and |
| 1061 | the engine turn loop it drives; `codewhale exec`, the CLI subcommands, the |
| 1062 | app-server / ACP surfaces, and the `workflow` tool do not fire them. |
| 1063 | |
| 1064 | [`docs/HOOKS.md`](HOOKS.md) is the authoritative reference for all eleven hook |
| 1065 | events — their firing points, environment variables, stdin payloads, timeout |
| 1066 | and background semantics, and which three of them can steer Codewhale. The |
| 1067 | sections below cover the configuration surface and the steering contracts in |
| 1068 | more depth. |
| 1069 | |
| 1070 | Two contract points worth reading there before writing a hook: |
| 1071 | |
| 1072 | - `background = true` means **submitted and never awaited**. The hook still |
| 1073 | gets the documented stdin payload and the same timeout, but it has no exit |
| 1074 | code and cannot steer. |
| 1075 | - A condition that references context its event never carries (an `exit_code` |
| 1076 | condition outside `tool_call_after` / `on_error`, a `mode` condition on |
| 1077 | `shell_env`, a tool condition on a non-tool event) is **rejected at load**, |
| 1078 | logged, and shown in `/hooks list`. It does not silently never match. |
| 1079 | Rejection is per entry, so a broken hook never drops another one that merely |
| 1080 | shares its `name` or is likewise unnamed. |
| 1081 | |
| 1082 | ### `/hooks` listing |
| 1083 | |
| 1084 | Run `/hooks` (or `/hooks list`) inside the TUI to see every |
| 1085 | configured lifecycle hook grouped by event, including each |
| 1086 | hook's name, command preview, effective timeout, and condition. When |
| 1087 | `[hooks].default_timeout_secs` is set it replaces every per-hook |
| 1088 | `timeout_secs`, and the listing shows that effective value and names the |
| 1089 | override rather than echoing the per-hook number. A |
| 1090 | `default_timeout_secs = 0` is rejected at load — it would expire every hook |
| 1091 | in the config immediately — so the override is ignored, per-hook |
| 1092 | `timeout_secs` applies, the listing shows that per-hook value with no |
| 1093 | override provenance, and the rejection appears under `configuration |
| 1094 | problems`. The |
| 1095 | `[hooks].enabled` flag's state is shown at the top so it's |
| 1096 | obvious when hooks are globally suppressed, and any entry rejected |
| 1097 | at load is listed under `configuration problems` with the reason. |
| 1098 | Hooks are configured under `[[hooks.hooks]]` entries — see |
| 1099 | [`docs/HOOKS.md`](HOOKS.md) for the full schema. |
| 1100 | |
| 1101 | ### Mutable `message_submit` hooks |
| 1102 | |
| 1103 | `message_submit` hooks run before a submitted message is added to |
| 1104 | history or sent to the model. Unlike observer-only lifecycle hooks, |
| 1105 | non-background `message_submit` hooks can replace or block the |
| 1106 | submitted text. |
| 1107 | |
| 1108 | ```toml |
| 1109 | [[hooks.hooks]] |
| 1110 | event = "message_submit" |
| 1111 | command = "~/.codewhale/hooks/inject-context.sh" |
| 1112 | timeout_secs = 2 |
| 1113 | continue_on_error = true |
| 1114 | ``` |
| 1115 | |
| 1116 | The hook receives JSON on stdin: |
| 1117 | |
| 1118 | ```json |
| 1119 | { |
| 1120 | "event": "message_submit", |
| 1121 | "text": "original user text", |
| 1122 | "text_bytes": 18, |
| 1123 | "text_original_bytes": 18, |
| 1124 | "text_truncated": false, |
| 1125 | "session_id": "sess_12345678", |
| 1126 | "workspace": "/path/to/workspace", |
| 1127 | "mode": "agent", |
| 1128 | "model": "deepseek-chat", |
| 1129 | "total_tokens": 1234 |
| 1130 | } |
| 1131 | ``` |
| 1132 | |
| 1133 | The entire serialized document is capped at 32 KiB. Codewhale retains the |
| 1134 | largest UTF-8-safe `text` prefix that fits after JSON escaping and bounded |
| 1135 | metadata, and the three `text_*` fields make truncation explicit. Immediate |
| 1136 | messages, restored queue entries, merged steers, and prior-hook replacements |
| 1137 | all cross this same serialization boundary. |
| 1138 | |
| 1139 | If the hook exits `0` and prints JSON with a non-empty string `text` field, |
| 1140 | that value replaces the submitted text: |
| 1141 | |
| 1142 | ```json |
| 1143 | { "text": "replacement user text" } |
| 1144 | ``` |
| 1145 | |
| 1146 | Exit `0` with empty stdout, or stdout JSON without `text`, leaves |
| 1147 | the current text unchanged. A JSON `text` field must not be empty; |
| 1148 | `{"text":""}` is treated as invalid stdout and ignored. Exit `2` |
| 1149 | blocks the submission before the turn starts; a structured `reason` field can |
| 1150 | provide the bounded, redacted status message shown in the TUI. Raw stdout, |
| 1151 | stderr, and process-error text are not copied into denial receipts. |
| 1152 | Other non-zero exits follow the hook's `continue_on_error` setting. |
| 1153 | Timeouts and spawn failures are also surfaced as transient TUI status |
| 1154 | messages when `continue_on_error = true` lets submission continue. |
| 1155 | |
| 1156 | Multiple `message_submit` hooks run in config order, and each hook |
| 1157 | receives the text produced by the previous hook. Hooks marked |
| 1158 | `background = true` are observer-only and cannot transform or block |
| 1159 | the message — they still receive the same stdin payload and the same |
| 1160 | environment, they are simply never awaited. Existing environment |
| 1161 | variables remain available. |
| 1162 | `shell_env` hooks keep their existing `KEY=VALUE` stdout contract; |
| 1163 | JSON stdout contracts exist for `message_submit` (above) and |
| 1164 | `tool_call_before` (below). |
| 1165 | |
| 1166 | ### `tool_call_before` decision hooks |
| 1167 | |
| 1168 | `tool_call_before` hooks run before each tool call executes. In |
| 1169 | addition to the legacy hard deny (exit code `2`, which always wins |
| 1170 | regardless of stdout), a foreground hook may print a JSON decision on |
| 1171 | stdout with exit code `0`: |
| 1172 | |
| 1173 | ```json |
| 1174 | { |
| 1175 | "decision": "allow" | "deny" | "ask", |
| 1176 | "reason": "human-readable explanation (used for deny)", |
| 1177 | "updatedInput": { "command": "ls -la" }, |
| 1178 | "additionalContext": "text appended to the tool result for the model" |
| 1179 | } |
| 1180 | ``` |
| 1181 | |
| 1182 | All fields are optional. Empty stdout, non-JSON stdout, and JSON |
| 1183 | without a `decision` field behave exactly as before (allow). An |
| 1184 | unrecognized `decision` string logs a fixed warning without echoing the |
| 1185 | untrusted value and is treated as allow. |
| 1186 | |
| 1187 | - `deny` blocks the tool; the model receives a permission-denied tool |
| 1188 | result containing `reason`. |
| 1189 | - `ask` forces the interactive approval prompt in Ask and Auto-Review even for |
| 1190 | tools that would otherwise auto-run. Full Access does not open tool-approval |
| 1191 | prompts, so hook `ask` does not downgrade that posture. |
| 1192 | - `updatedInput` must be a JSON object; it replaces the tool input |
| 1193 | before execution. When several hooks supply it, the last hook wins. |
| 1194 | - `additionalContext` is appended to the tool result sent back to the |
| 1195 | model as `[hook context] ...`. Multiple hooks' contexts are |
| 1196 | concatenated. |
| 1197 | |
| 1198 | When multiple hooks match, precedence is deny > ask > allow. Hooks |
| 1199 | marked `background = true` cannot steer tool calls — they are |
| 1200 | submitted and never awaited, so they have no verdict to contribute. |
| 1201 | |
| 1202 | A foreground hook that produced no verdict at all — it hit its timeout, the |
| 1203 | process could not be started, or a strict process exited non-zero without an |
| 1204 | explicit JSON decision — is not treated as permission. If |
| 1205 | *that* hook is configured with `continue_on_error = false`, the outcome |
| 1206 | denies the tool call and the denial names the hook and a bounded reason. |
| 1207 | Strictness is read off the hooks that actually matched this call, so a |
| 1208 | strict gate scoped to another tool cannot deny it, and a lenient hook's |
| 1209 | timeout does not deny merely because a strict hook exists elsewhere in |
| 1210 | config. Under the default `continue_on_error = true` the outcome is |
| 1211 | logged and the call proceeds. |
| 1212 | |
| 1213 | `reason` and `additionalContext` are capped (2 000 characters per field, |
| 1214 | 8 000 for the concatenated context of one call) and stripped of control |
| 1215 | characters before they reach the TUI or the model. |
| 1216 | |
| 1217 | Example deny hook: |
| 1218 | |
| 1219 | ```toml |
| 1220 | [[hooks.hooks]] |
| 1221 | event = "tool_call_before" |
| 1222 | command = '''echo '{"decision":"deny","reason":"blocked by project policy"}' ''' |
| 1223 | condition = { type = "tool_name", name = "exec_shell" } |
| 1224 | ``` |
| 1225 | |
| 1226 | Example ask hook (force approval for every MCP tool): |
| 1227 | |
| 1228 | ```toml |
| 1229 | [[hooks.hooks]] |
| 1230 | event = "tool_call_before" |
| 1231 | command = '''echo '{"decision":"ask"}' ''' |
| 1232 | condition = { type = "tool_name", name = "mcp__*" } |
| 1233 | ``` |
| 1234 | |
| 1235 | Example input rewrite: |
| 1236 | |
| 1237 | ```toml |
| 1238 | [[hooks.hooks]] |
| 1239 | event = "tool_call_before" |
| 1240 | command = "~/.codewhale/hooks/clamp-shell-timeout.sh" |
| 1241 | condition = { type = "tool_name", name = "exec_shell" } |
| 1242 | ``` |
| 1243 | |
| 1244 | where the script reads the hook context, then prints |
| 1245 | `{"updatedInput": {...}}` with the adjusted arguments. |
| 1246 | |
| 1247 | `tool_name` conditions support `*` globs: `mcp__*` matches every MCP |
| 1248 | tool (e.g. `mcp__github__create_issue`) but not built-ins like |
| 1249 | `read_file`; exact names keep matching exactly. Other regex |
| 1250 | metacharacters in the pattern are matched literally. |
| 1251 | |
| 1252 | ### Project-local hooks |
| 1253 | |
| 1254 | Repositories can ship policy in `<workspace>/.codewhale/hooks.toml`, |
| 1255 | using the same shape as the `[hooks]` table (top-level fields plus |
| 1256 | `[[hooks]]` entries). Project hooks are executable shell |
| 1257 | configuration, so Codewhale only loads them after the workspace has |
| 1258 | been trusted in user-owned config through the trust prompt or a |
| 1259 | `[projects."<workspace>"] trust_level = "trusted"` entry. Session |
| 1260 | `/trust on` mode does not enable repo-supplied hooks by itself, and |
| 1261 | repo-local legacy markers such as `.deepseek/trusted` do not enable |
| 1262 | project hooks. Once trusted, project hooks are appended after global |
| 1263 | hooks from `config.toml`, so they run last and, for `updatedInput`, |
| 1264 | win ties. A malformed trusted project file logs a warning and startup |
| 1265 | falls back to global hooks only. |
| 1266 | |
| 1267 | ```toml |
| 1268 | # .codewhale/hooks.toml |
| 1269 | [[hooks]] |
| 1270 | event = "tool_call_before" |
| 1271 | command = '''echo '{"decision":"deny","reason":"no shell in this repo"}' ''' |
| 1272 | condition = { type = "tool_name", name = "exec_shell" } |
| 1273 | ``` |
| 1274 | |
| 1275 | ### Turn-end observer hooks |
| 1276 | |
| 1277 | `turn_end` hooks observe the end of each model turn after post-turn |
| 1278 | state, usage totals, cost accounting, notifications, receipts, and |
| 1279 | queue recovery have been updated. They receive JSON on stdin and are |
| 1280 | observer-only: stdout is ignored, failures are logged as warnings, and |
| 1281 | the hook cannot block user input, mutate the transcript, or change the |
| 1282 | next queued follow-up. |
| 1283 | |
| 1284 | Observer-only UI events share one 32-entry queue and two persistent workers; |
| 1285 | the terminal loop uses non-blocking submission and does not create a thread per |
| 1286 | event. A full queue or unavailable dispatcher drops that observer event and is |
| 1287 | kept as an event-specific error toast, independent of ordinary agent/turn |
| 1288 | status text. |
| 1289 | |
| 1290 | ```toml |
| 1291 | [[hooks.hooks]] |
| 1292 | event = "turn_end" |
| 1293 | command = "~/.codewhale/hooks/turn-audit.sh" |
| 1294 | timeout_secs = 2 |
| 1295 | continue_on_error = true |
| 1296 | ``` |
| 1297 | |
| 1298 | The payload includes common hook metadata plus post-turn accounting: |
| 1299 | |
| 1300 | ```json |
| 1301 | { |
| 1302 | "event": "turn_end", |
| 1303 | "session_id": "sess_12345678", |
| 1304 | "workspace": "/path/to/workspace", |
| 1305 | "mode": "agent", |
| 1306 | "created_at": "2026-07-12T10:30:00+00:00", |
| 1307 | "model_backed": true, |
| 1308 | "provider": "deepseek", |
| 1309 | "model": "deepseek-chat", |
| 1310 | "billing_surface": null, |
| 1311 | "turn_id": "turn_12345678", |
| 1312 | "status": "completed", |
| 1313 | "error": null, |
| 1314 | "duration_ms": 1834, |
| 1315 | "usage": { |
| 1316 | "input_tokens": 1200, |
| 1317 | "output_tokens": 180, |
| 1318 | "prompt_cache_hit_tokens": 900, |
| 1319 | "prompt_cache_miss_tokens": 300, |
| 1320 | "prompt_cache_write_tokens": 0, |
| 1321 | "reasoning_tokens": null, |
| 1322 | "reasoning_replay_tokens": null |
| 1323 | }, |
| 1324 | "totals": { |
| 1325 | "session_tokens": 1380, |
| 1326 | "conversation_tokens": 1380, |
| 1327 | "input_tokens": 1200, |
| 1328 | "output_tokens": 180 |
| 1329 | }, |
| 1330 | "tool_count": 2, |
| 1331 | "queued_message_count": 1, |
| 1332 | "stop_hook_active": false |
| 1333 | } |
| 1334 | ``` |
| 1335 | |
| 1336 | `created_at` anchors time-window pricing; `provider` and `model` identify the |
| 1337 | effective route used for model-backed turns. `billing_surface` is an optional, |
| 1338 | non-secret classification derived from the endpoint that actually served the |
| 1339 | turn. Recognized StepFun routes emit `stepfun-payg` or `stepfun-plan`; the raw |
| 1340 | base URL is never written to hook or runtime records. Runtime `TurnRecord` |
| 1341 | exports call the same field `effective_billing_surface`, which `scorecard` |
| 1342 | accepts as an alias. This keeps subscription quota separate from token-priced |
| 1343 | usage. Unrecognized and custom endpoints remain `null` and unpriced. |
| 1344 | |
| 1345 | Shell-only lifecycle completions set `model_backed` to `false` and may report a |
| 1346 | `null` provider; offline scorecards exclude those records from model token and |
| 1347 | cost totals. Completion-only shell, manual-compaction, and purge events that do |
| 1348 | not have a matching `TurnStarted` retain the observer notification with a |
| 1349 | synthetic `lifecycle_<uuid>` turn id and the time the completion was observed. |
| 1350 | |
| 1351 | For `interrupted` or `failed` turns, `status` reflects that terminal |
| 1352 | state and `error` carries the engine error string when one is available. |
| 1353 | `stop_hook_active` is reserved for future re-entry protection and is |
| 1354 | currently always `false`. |
| 1355 | |
| 1356 | ### Sub-agent lifecycle hooks |
| 1357 | |
| 1358 | `subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle |
| 1359 | events. They receive bounded JSON metadata on stdin and are observer-only: |
| 1360 | hook failures are logged as warnings and do not block sub-agent scheduling, |
| 1361 | change prompts, or change results. For these observer events, |
| 1362 | `continue_on_error` has no effect: later matching hooks still run even when an |
| 1363 | earlier hook exits non-zero. |
| 1364 | |
| 1365 | ```toml |
| 1366 | [[hooks.hooks]] |
| 1367 | event = "subagent_complete" |
| 1368 | command = "~/.codewhale/hooks/subagent-audit.sh" |
| 1369 | timeout_secs = 2 |
| 1370 | continue_on_error = true |
| 1371 | ``` |
| 1372 | |
| 1373 | `subagent_spawn` receives: |
| 1374 | |
| 1375 | ```json |
| 1376 | { |
| 1377 | "event": "subagent_spawn", |
| 1378 | "agent_id": "agent_12345678", |
| 1379 | "session_id": "sess_12345678", |
| 1380 | "workspace": "/path/to/workspace", |
| 1381 | "mode": "agent", |
| 1382 | "model": "deepseek-chat", |
| 1383 | "total_tokens": 1234, |
| 1384 | "prompt_preview": "bounded prompt preview", |
| 1385 | "prompt_truncated": false |
| 1386 | } |
| 1387 | ``` |
| 1388 | |
| 1389 | `subagent_complete` receives the same common fields plus terminal metadata: |
| 1390 | |
| 1391 | ```json |
| 1392 | { |
| 1393 | "event": "subagent_complete", |
| 1394 | "agent_id": "agent_12345678", |
| 1395 | "session_id": "sess_12345678", |
| 1396 | "workspace": "/path/to/workspace", |
| 1397 | "mode": "agent", |
| 1398 | "model": "deepseek-chat", |
| 1399 | "total_tokens": 1234, |
| 1400 | "status": "completed", |
| 1401 | "result_preview": "bounded result preview", |
| 1402 | "result_truncated": false |
| 1403 | } |
| 1404 | ``` |
| 1405 | |
| 1406 | Previews are capped before delivery so lifecycle hooks do not receive full |
| 1407 | sub-agent prompts, transcripts, or unbounded results. Use the transcript handle |
| 1408 | returned by `agent` when full sub-agent details are needed. |
| 1409 | |
| 1410 | ### Running-turn input |
| 1411 | |
| 1412 | Composer shortcuts keep the same role throughout a session: |
| 1413 | |
| 1414 | - **Enter** sends when idle and queues a next-turn follow-up while busy. The |
| 1415 | behavior does not change before versus after the provider's first token. |
| 1416 | - With an empty composer and queued follow-ups visible, **Enter** sends the |
| 1417 | oldest queued follow-up into the active turn now. |
| 1418 | - **Ctrl+Enter** (or **Cmd+Enter** when the terminal forwards it) explicitly |
| 1419 | steers the active turn. It sends normally when idle. |
| 1420 | - **Shift+Enter**, **Alt+Enter**, and **Ctrl+J** always insert a newline. |
| 1421 | - **Ctrl+G** and **Ctrl+S** only stash drafts; they never send or steer. |
| 1422 | |
| 1423 | ### Composer stash (`/stash`, Ctrl+G / Ctrl+S) |
| 1424 | |
| 1425 | Press **Ctrl+G** in the composer to park the current draft to |
| 1426 | `~/.codewhale/composer_stash.jsonl`. `/stash list` shows parked |
| 1427 | drafts with one-line previews and timestamps; `/stash pop` |
| 1428 | restores the most recently parked draft (LIFO); `/stash clear` |
| 1429 | wipes the file. Capped at 200 entries; multiline drafts round-trip intact. |
| 1430 | **Ctrl+S** remains an alias in terminals that forward it; Cursor and VS Code |
| 1431 | reserve Ctrl+S for Save, so Ctrl+G is the portable default. |
| 1432 | |
| 1433 | ## Settings File (Persistent UI Preferences) |
| 1434 | |
| 1435 | codewhale also stores user preferences in: |
| 1436 | |
| 1437 | - `~/.codewhale/settings.toml` on new installs |
| 1438 | - `~/.deepseek/settings.toml` or the legacy platform config-dir |
| 1439 | `deepseek/settings.toml` when an existing settings file is present |
| 1440 | |
| 1441 | Notable settings include `auto_compact`, which uses a model-aware default-on |
| 1442 | policy for known context windows up to the 1M-token V4 class. Automatic |
| 1443 | compaction runs before the active model limit and carries the compacted summary |
| 1444 | forward into the next request. The trigger defaults to |
| 1445 | `auto_compact_threshold_percent = 80`. Users who prefer manual continuity can |
| 1446 | persist `auto_compact = false`; manual `/compact` / Ctrl+L remains available. |
| 1447 | You can inspect or update these from the TUI with `/settings` and `/config` |
| 1448 | (interactive editor). |
| 1449 | |
| 1450 | Common settings keys: |
| 1451 | |
| 1452 | - `theme` (`system`, `terminal`, `dark`, `light`, `grayscale`, |
| 1453 | `catppuccin-mocha`, `tokyo-night`, `dracula`, `gruvbox-dark`, `claude`, |
| 1454 | `matrix`, `solarized-light`; default `system`): `system` follows terminal |
| 1455 | background detection, `dark`/`light` use the Codewhale Whale pair, |
| 1456 | `terminal` inherits the host terminal, `grayscale` is the low-opinion |
| 1457 | black/white theme, and the named community presets apply across the TUI. |
| 1458 | Aliases such as `whale`, `mono`, `black-white`, `tokyonight`, and `gruvbox` |
| 1459 | are accepted. In Whale, cobalt blue owns action/focus, seafoam owns live |
| 1460 | work, Signal Gold owns human decisions and the whale, coral owns warnings, |
| 1461 | rose owns danger, violet owns Operate, and green remains completed/verified. |
| 1462 | Text labels, markers, and motion policy carry the same states when color is |
| 1463 | unavailable; color is never the only cue. |
| 1464 | User-authored overlays live only at `~/.codewhale/themes/<name>.json` (or |
| 1465 | `$CODEWHALE_HOME/themes/<name>.json`) and are selected with |
| 1466 | `/theme custom:<name>`. The filename is a bounded slug, symlinks and files |
| 1467 | over 64 KiB are refused, colors must be `#RRGGBB`, and unknown fields fail |
| 1468 | validation. `/theme schema` prints the embedded JSON Schema and `/theme path` |
| 1469 | shows the exact directory. An overlay names one compiled `base` theme and |
| 1470 | changes only listed semantic colors; it cannot include or read another file. |
| 1471 | - `auto_compact` (on/off, model-aware default on for known context windows |
| 1472 | unless explicitly configured) |
| 1473 | - `auto_compact_threshold_percent` (10-100, default `80`): pre-send |
| 1474 | auto-compaction threshold used only when `auto_compact` is enabled. |
| 1475 | - `paste_burst_detection` (on/off, default on): fallback rapid-key paste |
| 1476 | detection for terminals that do not emit bracketed-paste events. This is |
| 1477 | independent of terminal bracketed-paste mode. |
| 1478 | - `work_surface_placement` (`top`, `left`, `right`, or `off`; default `top`): |
| 1479 | places the work bar — Tasks / To-do / Workers — above the transcript (the |
| 1480 | default top bar), in a side rail, or hides it entirely (`off`). Side |
| 1481 | choices fall back to the top layout on narrow terminals without changing |
| 1482 | the saved preference. Set it live with |
| 1483 | `/config work_surface_placement right --save` (or `left` / `top` / `off`). |
| 1484 | - `rail_panel` (`tasks`, `agents`, `context`, `pinned`; default `tasks`, alias |
| 1485 | key `rail`): which panel the work bar shows. Panel selection is orthogonal |
| 1486 | to placement. `tasks` is the full live work list (to-dos, then sub-agents); |
| 1487 | `agents` narrows to the sub-agent rows; `pinned` shows the goal plus the |
| 1488 | to-do checklist; `context` is a read-only session-facts list. In every |
| 1489 | panel except `context`, rows are selectable and clickable and open their |
| 1490 | detail surface. `Alt+!`/`Alt+@`/`Alt+#`/`Alt+$` switch panels live. |
| 1491 | - `work_surface_top_height` (2–16) and `work_surface_side_width` (26–80): |
| 1492 | ceilings for the top strip's height and a side rail's width. Both are |
| 1493 | normally persisted by dragging the divider rather than edited by hand; the |
| 1494 | strip still auto-fits its content below the ceiling. |
| 1495 | - `focus_texture` (`off`, `scrim`, or `grain`; default `off`): focus-context |
| 1496 | texture for modal views. `scrim` dims the already-rendered background |
| 1497 | outside the focused modal toward the theme surface; `grain` sprinkles |
| 1498 | sparse dots over blank cells there. The texture is static (no time |
| 1499 | component, so it is unaffected by `low_motion`), never writes over a cell |
| 1500 | that carries text, and preserves the 4.5:1 body-text contrast floor |
| 1501 | wherever both colors are resolvable. It is skipped entirely on frames |
| 1502 | below the ambient-life minimum size and when the focused modal already |
| 1503 | covers 90% or more of the frame. Set it live with |
| 1504 | `/config focus_texture scrim --save`. |
| 1505 | - `mention_menu_limit` (integer, default `128`): maximum number of |
| 1506 | `@`-mention popup candidates retained before the composer renders the |
| 1507 | visible window. The visible rows still depend on terminal height. |
| 1508 | - `mention_walk_depth` (integer, default `6`): maximum workspace depth for |
| 1509 | `@`-mention completion walks. Set to `0` for unlimited depth in deeply |
| 1510 | nested workspaces; keep the default in very large repos unless needed. |
| 1511 | - `mention_menu_behavior` (`fuzzy`, `browser`; default `fuzzy`): controls how |
| 1512 | `@`-mention completions are populated. `fuzzy` searches the workspace and |
| 1513 | applies mention frecency. `browser` lists only the immediate children of the |
| 1514 | currently typed directory segment in deterministic alphabetical order. |
| 1515 | - `show_thinking` (on/off) |
| 1516 | - `thinking_default_expanded` (on/off, default off): renders thinking blocks |
| 1517 | expanded initially when `show_thinking` is enabled. Space still toggles the |
| 1518 | selected block, so setting this to `true` inverts the default without |
| 1519 | removing per-block folding. This is useful in SSH/tmux environments where |
| 1520 | the Space binding may be intercepted. |
| 1521 | - `show_tool_details` (on/off) |
| 1522 | - `inline_diffs` (`full`, `summary`, or `off`; default `full`): controls the |
| 1523 | inline presentation of successful structured File mutations. `full` shows a |
| 1524 | bounded red/green diff and semantic statistics, `summary` keeps only the |
| 1525 | statistics, and `off` keeps the calm changed-file outcome. All three retain |
| 1526 | the exact applied change in the selected File receipt's Alt/Option+V detail. |
| 1527 | Failure and cancellation never render a successful diff. Save the choice |
| 1528 | with `/config inline_diffs <mode> --save`. |
| 1529 | - `locale` (`auto`, `en`, `ja`, `zh-Hans`, `zh-Hant`, `pt-BR`, `es-419`, `vi`, |
| 1530 | `ko`; default `auto`): UI chrome locale. `auto` checks `LC_ALL`, |
| 1531 | `LC_MESSAGES`, then `LANG`; unsupported locale selections resolve to English. |
| 1532 | `zh-Hant` is a shipped partial pack, so strings it does not yet provide fall |
| 1533 | back to English. The runtime also exposes the resolved locale in the system |
| 1534 | prompt as the fallback natural language for V4 reasoning and replies when the |
| 1535 | latest user message is ambiguous. Clear user language still takes priority; |
| 1536 | Chinese turns should produce Chinese `reasoning_content` and Chinese final |
| 1537 | replies even when the resolved locale is English. |
| 1538 | - `background_color` (`#RRGGBB`, `RRGGBB`, or `default`): optional main TUI |
| 1539 | background color applied to the root, header, transcript, and footer |
| 1540 | surfaces while preserving panel contrast. |
| 1541 | - `cost_currency` (`usd`, `cny`; default `usd`): currency used by the footer, |
| 1542 | context panel, `/cost`, `/tokens`, and long-turn notification summaries. The |
| 1543 | aliases `rmb` and `yuan` normalize to `cny`. |
| 1544 | - `default_mode` (`agent`, `plan`, or `operate`; legacy values are accepted for migration but are not live mode vocabulary) |
| 1545 | - `launch_screen` (`on`/`off`; default `off`): show the pre-session New/ |
| 1546 | Resume/Worktree menu. With it off, Codewhale enters a new session directly; |
| 1547 | resume remains available in-session. |
| 1548 | - `sidebar_focus` (legacy, migration-only): the classic right sidebar this key |
| 1549 | configured was removed in the 0.9.4 rail unification. The key is still read |
| 1550 | once so old settings carry forward, then folds into the live keys: |
| 1551 | `pinned`/`work`/`plan`/`todos` become `rail_panel = "pinned"`, |
| 1552 | `agents`/`subagents` become `rail_panel = "agents"`, `context`/`session` |
| 1553 | become `rail_panel = "context"`, `tasks`/`auto` (the old default) become the |
| 1554 | `tasks` panel, `sessions` enables `sessions_rail`, and `hidden` turns the |
| 1555 | work bar off via `work_surface_placement = "off"`. An explicit `rail_panel` |
| 1556 | in the file always wins over the migrated value. Configure the work bar with |
| 1557 | `rail_panel` and `work_surface_placement`, not this key. |
| 1558 | - `sessions_rail` (`on`/`off`; default `off`): show the persistent Sessions |
| 1559 | rail in the sidebar panel stack. Rows list this workspace's recent |
| 1560 | non-archived sessions, newest first, with the active one marked; activating a |
| 1561 | row opens the session picker preselected on it (`/sessions open <id>`), so |
| 1562 | resume keeps its single implementation. Rows are projected from cached |
| 1563 | session metadata — the rail never reads a transcript per frame, and never |
| 1564 | contacts a provider. |
| 1565 | - `session_auto_resume` (`on`/`off`; default `off`): reattach to this |
| 1566 | workspace's most recent session when Codewhale starts. Off by default so |
| 1567 | plain `codewhale` keeps starting fresh. `--resume`, `--continue`, and |
| 1568 | `--fresh` always take precedence. When it is on, startup still refuses to |
| 1569 | resume a session that is archived, fails to load, or is recorded against a |
| 1570 | different workspace; each of those falls back to a fresh transcript and says |
| 1571 | which session was skipped and why. It applies to the interactive launch only |
| 1572 | — `codewhale "<prompt>"` and `codewhale exec` are never silently prefixed |
| 1573 | with a prior conversation. |
| 1574 | - `max_input_history` (number of submitted input history entries; cleared |
| 1575 | drafts are also kept locally for composer history search). Note the spelling: |
| 1576 | the serde field on disk is `max_input_history` |
| 1577 | (`crates/tui/src/settings.rs:426`, default 100). `max_history` is the key |
| 1578 | name accepted by `/config set` and `settings.set()` (`settings.rs:1388`), not |
| 1579 | a settings.toml key — writing `max_history` into the file is silently |
| 1580 | ignored. |
| 1581 | - `default_model` (model name override) |
| 1582 | |
| 1583 | `/task digest` (alias `/tasks digest`) renders the canonical Work Graph |
| 1584 | operations and four-state To-do list as plain text, running work first. It |
| 1585 | reads the same snapshots as the styled Work surface and owns no parallel |
| 1586 | progress state. |
| 1587 | |
| 1588 | Plan and Act are the everyday visible modes in the UI; Operate is an explicit |
| 1589 | preview entry while its Workflow control surface is still being built. Switch |
| 1590 | between them with `/mode`. For compatibility, older settings files with |
| 1591 | `default_mode = "normal"` still load as `agent`. |
| 1592 | |
| 1593 | Localization scope is tracked in [LOCALIZATION.md](LOCALIZATION.md). The v0.7.6 |
| 1594 | core pack covers high-visibility TUI chrome only; provider/tool schemas, |
| 1595 | personality prompts, and full documentation remain English unless explicitly |
| 1596 | translated later. |
| 1597 | |
| 1598 | Readability semantics: |
| 1599 | |
| 1600 | - Selection uses a unified style across transcript, composer menus, and modals. |
| 1601 | - Footer hints use a dedicated semantic role (`FOOTER_HINT`) so hint text stays readable across themes. |
| 1602 | |
| 1603 | ### Token Quantities and Drivers |
| 1604 | |
| 1605 | DeepSeek V4 prefix caching makes token labels matter. These quantities are kept |
| 1606 | separate: |
| 1607 | |
| 1608 | | Quantity | Meaning | Allowed to drive | |
| 1609 | |---|---|---| |
| 1610 | | 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. | |
| 1611 | | Reserved response headroom | The internal turn budget plus safety headroom. v0.8.16 keeps normal turns at `262144` reserved output tokens and adds `1024` safety tokens for context-window checks, even though V4 capability metadata reports the official `384000` max output. | Emergency overflow budget checks only. | |
| 1612 | | 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. | |
| 1613 | | 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. | |
| 1614 | | Context percent | Active request input estimate divided by the model context window. | Display only; it mirrors the active-input basis used by context safeguards. | |
| 1615 | | Cost estimate | Approximate spend from provider usage and configured DeepSeek rates. | Display only. | |
| 1616 | |
| 1617 | For known context-window models, including 1M-class V4 models, replacement |
| 1618 | compaction is enabled by default unless the user explicitly configures |
| 1619 | `auto_compact = false`. It fires at the active model's compaction threshold and |
| 1620 | replays the generated summary through the stable system prompt on the next |
| 1621 | request. Unknown model ids remain opt-in. |
| 1622 | |
| 1623 | ### Command Migration Notes |
| 1624 | |
| 1625 | If you are upgrading from older releases: |
| 1626 | |
| 1627 | - Old: `/codewhale` |
| 1628 | New: `/links` (aliases: `/dashboard`, `/api`) |
| 1629 | - Old: `/set model deepseek-reasoner` |
| 1630 | New: `/config` and edit the `model` row to `deepseek-v4-pro` or `deepseek-v4-flash` |
| 1631 | - Old: visible `Normal` mode or `default_mode = "normal"` |
| 1632 | New: use `Agent` / `default_mode = "agent"`; legacy `normal` still maps to `agent` |
| 1633 | - Old: discover `/set` in slash UX/help |
| 1634 | New: use `/config` for editing and `/settings` for read-only inspection |
| 1635 | |
| 1636 | ## Key Reference |
| 1637 | |
| 1638 | ### Core keys (used by the TUI/engine) |
| 1639 | |
| 1640 | - `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`, `huggingface`, `together`, `qianfan`, `openai-codex`, `anthropic`, `openmodel`, `zai`, `stepfun`, `minimax`, `deepinfra`, `sakana`, `longcat`, `opencode-go`, `meta`, `telecomjs`, or `xai`. 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`; `together` targets Together AI at `https://api.together.xyz/v1`; `qianfan` targets Baidu Qianfan at `https://api.baiduqianfan.ai/v1`; `openai-codex` targets ChatGPT/Codex OAuth; `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 Chat Completions route at `https://opencode.ai/zen/go/v1`; `meta` targets Meta Model API; `telecomjs` targets TelecomJS TokenHub at `https://aigw.telecomjs.com/v1`; and `xai` targets xAI's API-key or OAuth route. |
| 1641 | - `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.5`, 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 is resolved through the curated Zen catalog: GPT uses Responses, Claude/Qwen use Anthropic Messages, and the documented DeepSeek/MiniMax/GLM/Kimi/Grok/free rows use Chat Completions. Gemini and unknown models 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). |
| 1642 | - `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. |
| 1643 | - `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 Ollama can omit it. |
| 1644 | - `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. |
| 1645 | - `base_url` (string, optional): 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.together.xyz/v1` for `together`, `https://api.baiduqianfan.ai/v1` for `qianfan`, `https://chatgpt.com/backend-api` for `openai-codex`, `https://api.anthropic.com` for `anthropic`, `http://localhost:30000/v1` for `sglang`, `http://localhost:8000/v1` for `vllm`, and `http://localhost:11434/v1` for `ollama`. 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. Set `https://api.deepseek.com` or `https://api.deepseek.com/v1` explicitly to opt out of DeepSeek beta features. |
| 1646 | - `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. |
| 1647 | - `context_window` (integer, optional provider-table key): override the total context window for the active `[providers.<name>]` 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). |
| 1648 | - `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. |
| 1649 | - `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 `<think>...</think>` inside `delta.content`, or `none` to render incoming content exactly as answer text. |
| 1650 | - `[providers.<name>.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. |
| 1651 | - `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. |
| 1652 | - `default_text_model` (string, optional): defaults to `deepseek-v4-pro` for DeepSeek, `deepseek-anthropic`, and generic OpenAI-compatible endpoints, `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.5-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.2` 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, and `deepseek-v4-flash` for Ollama. Hugging Face and Together AI both default to `deepseek-ai/DeepSeek-V4-Pro`; `openai-codex` defaults to `gpt-5.5`; `anthropic` defaults to `claude-sonnet-4-6`; `openmodel` defaults to `deepseek-v4-flash`. Current public DeepSeek IDs are `deepseek-v4-pro` and `deepseek-v4-flash`, both with 1M context windows, 384K max output, and thinking mode enabled by default. DeepSeek retires `deepseek-chat` and `deepseek-reasoner` on July 24, 2026; direct first-party routes migrate both to `deepseek-v4-flash`, with omitted reasoning settings preserving their former non-thinking (`off`) and thinking (`high`) intent. 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.2`, `GLM-5.1`, `GLM-5.3`, 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` / `z-ai/glm-5.3` are recognized ids only. Z.ai had not released GLM-5.3 as of 2026-08-03, so those routes fail upstream until it ships; they inherit their catalog metadata from `GLM-5.2` pending official Z.ai release metadata, carry no price, and do not change the Z.ai default; direct Sakana recognizes `fugu` and `fugu-ultra-20260615`; direct Xiaomi MiMo recognizes chat IDs `mimo-v2.5-pro`, `mimo-v2.5-pro-ultraspeed`, and `mimo-v2.5`, while TTS IDs are selected through `codewhale speech` / `tts`. Generic `openai`, `atlascloud`, `wanjie-ark`, `xiaomi-mimo`, `arcee`, `moonshot`, `minimax`, `openmodel`, `zai`, `stepfun`, `qianfan`, `sakana`, and Ollama 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. |
| 1653 | - 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. |
| 1654 | - `reasoning_effort` (string, optional): `off`, `low`, `medium`, `high`, `max`, `xhigh`, or `ultracode`; defaults to the configured UI tier. DeepSeek Platform receives top-level `thinking` / `reasoning_effort` fields. 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`. |
| 1655 | - `verbosity` (string, optional): `normal` or `concise`. `normal` keeps the |
| 1656 | default conversational prompt. `concise` appends a prompt discipline block |
| 1657 | for direct, low-chatter output; CLI noninteractive commands (`exec` and |
| 1658 | `eval`) default to `concise` unless config/env/CLI overrides it. |
| 1659 | Override per process with `CODEWHALE_VERBOSITY` or the legacy |
| 1660 | `DEEPSEEK_VERBOSITY` alias. |
| 1661 | - `telemetry` (bool, optional): opt-in product telemetry, **`false` by |
| 1662 | default**. Setting it to `true` is only half of the switch: nothing is |
| 1663 | collected unless the first-run notice has also been answered with "Enable" on |
| 1664 | this machine, so a `telemetry = true` written before 0.9.4 stays inert. An |
| 1665 | explicit `false` here is the *opt-out* — it deletes the random install id, |
| 1666 | truncates every buffered event, and leaves a tombstone that every later run |
| 1667 | re-asserts for as long as the key says `false` — while the unset default |
| 1668 | simply never collects. It is also a floor: `--telemetry true` and |
| 1669 | `CODEWHALE_TELEMETRY=1` both lose to it, and turning telemetry back on means |
| 1670 | writing `true` here. Override per process with `CODEWHALE_TELEMETRY` (legacy |
| 1671 | alias `DEEPSEEK_TELEMETRY`), where an explicit "off" is a hard floor that |
| 1672 | beats both this key and `--telemetry true` — but is a *kill switch*, not an |
| 1673 | opt-out: it stops the run and erases nothing, so a harness disabling |
| 1674 | telemetry for one command never discards the machine owner's install id or |
| 1675 | dry-run records. A repo-local `.codewhale/config.toml` cannot set it. Full |
| 1676 | schema and red lines: |
| 1677 | [`TELEMETRY.md`](TELEMETRY.md). |
| 1678 | - `telemetry_endpoint` (string, optional): where batches are POSTed. Leaving it |
| 1679 | unset selects the shipped default, |
| 1680 | **`https://telemetry.codewhale.net/v1/telemetry`** — the first-party ingest |
| 1681 | service described in [`TELEMETRY.md`](TELEMETRY.md), whose source is in |
| 1682 | `telemetry-ingest/`. This key decides only *where* an enabled session sends; |
| 1683 | it cannot turn telemetry on, and it is read only after `telemetry` above |
| 1684 | resolved true and the first-run notice was answered with "Enable". Setting it |
| 1685 | to the **empty string** is how you stay enabled and contact nobody: each batch |
| 1686 | is then written to `$CODEWHALE_HOME/telemetry/dryrun.jsonl` and no HTTP client |
| 1687 | is constructed at all, so you can read exactly what would have been sent. Any |
| 1688 | other value replaces the default outright. `https://` is required; plain |
| 1689 | `http://` is accepted only for loopback hosts, and there is no environment |
| 1690 | variable that overrides that refusal. A rejected endpoint turns telemetry off |
| 1691 | for the run rather than falling back to plaintext or to the default. Override |
| 1692 | per process with `CODEWHALE_TELEMETRY_ENDPOINT` (legacy alias |
| 1693 | `DEEPSEEK_TELEMETRY_ENDPOINT`), where an empty value means the same "contact |
| 1694 | nobody". A repo-local `.codewhale/config.toml` cannot set it. |
| 1695 | - `allow_shell` (bool, optional): in interactive TUI Agent sessions, omitting |
| 1696 | this keeps shell tools available with approval prompts; setting it to `false` |
| 1697 | hides shell tools. Headless, durable-task, and other noninteractive profiles |
| 1698 | keep the conservative omitted-field default and require `allow_shell = true` |
| 1699 | to expose shell. Plan mode always hides shell; Full Access enables shell and |
| 1700 | auto-approval. |
| 1701 | - `approval_policy` (string, optional): `on-request`, `untrusted`, or `never`. Runtime `approval_mode` editing in `/config` also accepts `on-request` and `untrusted` aliases. |
| 1702 | - `sandbox_mode` (string, optional): `read-only`, `workspace-write`, `danger-full-access`, `external-sandbox`. |
| 1703 | Platform support is not identical. macOS uses Seatbelt when its runtime |
| 1704 | probe succeeds. Linux uses bubblewrap only when `prefer_bwrap = true` and |
| 1705 | `/usr/bin/bwrap` is executable; without that opt-in it reports no OS command |
| 1706 | sandbox. Windows does not currently advertise an OS sandbox; its planned helper contract starts |
| 1707 | with process-tree containment only and must not be described as read-only |
| 1708 | filesystem isolation, workspace-write enforcement, network blocking, |
| 1709 | registry isolation, or AppContainer isolation until those are implemented. |
| 1710 | - The cross-layer relationship between mode admission, hooks, registered tool |
| 1711 | requirements, typed rules, auto-review, repo law, human approval, and the |
| 1712 | execution sandbox is defined in |
| 1713 | [Authorization order](AUTHORIZATION_ORDER.md). |
| 1714 | - `permissions.toml` (sibling file, optional): typed permission rule records |
| 1715 | loaded next to `config.toml`, for example `~/.codewhale/permissions.toml`. |
| 1716 | This active user file is the only permission-rule source today; project |
| 1717 | config overlays do not load a project-local `permissions.toml`. A rule's |
| 1718 | optional `workspace` field is its repository scope, not a second source. |
| 1719 | Manually authored `[[rules]]` entries accept `tool`, optional `command` or |
| 1720 | `path`, optional absolute `workspace`, optional `command_exact = true`, and |
| 1721 | optional `action = "deny" | "ask" | "allow"`; omitted `action` defaults to |
| 1722 | `"ask"`. `workspace` limits a rule to that repository, while |
| 1723 | `command_exact = true` changes a command rule from the historical |
| 1724 | arity-aware prefix match to a complete-command match. `deny` blocks matching |
| 1725 | invocations before mode-based |
| 1726 | approval handling, `allow` skips approval for matching invocations, and |
| 1727 | `ask` forces approval only in modes that can prompt. Outside the TUI |
| 1728 | auto-approve path, a matching `ask` rule under `approval_policy = "never"` |
| 1729 | is rejected because no prompt can be shown. In Full Access / auto-approval sessions, |
| 1730 | `ask` rules do not downgrade the session into prompting or blocking; explicit |
| 1731 | `deny` rules still block according to the current execution-policy logic. |
| 1732 | |
| 1733 | In a supported approval card, press `S` to allow the request once and append |
| 1734 | exact `action = "ask"` rules to this file. For eligible safe requests, choose |
| 1735 | **Always allow this exact rule in this repo** (shortcut `P`) to append an |
| 1736 | `action = "allow"` rule with the current absolute `workspace` scope. |
| 1737 | Remembered shell grants set `command_exact = true`, so later commands with |
| 1738 | extra arguments do not inherit the grant. File and patch grants retain the |
| 1739 | exact workspace-relative paths produced by the existing validation path. |
| 1740 | Supported saves are intentionally narrow: |
| 1741 | `exec_shell` stores the exact approved command string; `write_file` and |
| 1742 | `edit_file` store the exact workspace-relative file path; `apply_patch` |
| 1743 | stores one exact workspace-relative `path` rule per validated touched file |
| 1744 | from apply-patch preflight. Existing exec command matching remains |
| 1745 | arity-aware for manually authored prefix rules; approval-card allow grants |
| 1746 | use complete-command matching. File paths are normalized to the same |
| 1747 | workspace-relative form used by runtime matching. |
| 1748 | |
| 1749 | `read_file` rules can still be authored manually when you want future reads |
| 1750 | of a specific path to ask, allow, or deny, but the approval UI does not save |
| 1751 | `read_file` rules. Commands classified as requiring approval or dangerous, |
| 1752 | critical approval cards, and repo-law prompts cannot save allow grants and |
| 1753 | continue to require review. |
| 1754 | |
| 1755 | `/permissions` (or `/permissions list`) is the narrow rule-management |
| 1756 | surface. It lists each numbered rule with the active user-file source, its |
| 1757 | exact effective matcher (tool-wide, command prefix, exact command, or exact |
| 1758 | normalized path), global or repository scope, and whether that scope |
| 1759 | applies in the current workspace. `/config ask-rules` remains a compatibility |
| 1760 | entry to the same list. |
| 1761 | |
| 1762 | Deletion is review-gated: `/permissions remove <number>` only previews the |
| 1763 | selected rule and prints a confirmation command. That command carries an |
| 1764 | opaque token bound to the exact file bytes and rule index; if another writer |
| 1765 | changes `permissions.toml`, confirmation fails instead of deleting a rule |
| 1766 | that moved into the old position. Confirmed removal and approval-card appends |
| 1767 | share the adjacent `permissions.toml.lock`, preserve unrelated TOML comments |
| 1768 | and formatting, and atomically replace the file. The running TUI reloads the |
| 1769 | user ruleset without clearing session-only approvals. |
| 1770 | |
| 1771 | This editor intentionally does not create or rewrite rules, persist deny |
| 1772 | choices from approval cards, expand globs, or create broad |
| 1773 | directory/recursive rules. Author those supported exact/prefix records |
| 1774 | manually when needed. |
| 1775 | - `[[hotbar]]` (array of tables, optional): user-owned 1-8 slot bindings for |
| 1776 | the TUI hotbar. Each entry has `slot`, `action`, and optional `label`. |
| 1777 | Omitting `hotbar` uses the built-in default eight slots. Setting |
| 1778 | `hotbar = []` disables all default slots. When one or more `[[hotbar]]` |
| 1779 | tables are present, that list replaces the defaults; missing slots stay |
| 1780 | empty. Invalid slots outside `1..=8` are skipped with a warning, duplicate |
| 1781 | slots use the later entry, and unknown action IDs are kept so the UI can show |
| 1782 | a disabled/unknown cell instead of silently deleting user config. Trusted |
| 1783 | user config, profiles, and managed config replace the whole list; project |
| 1784 | overlays cannot change hotbar bindings. Setup or wizard flows that persist |
| 1785 | hotbar bindings write this same schema to the resolved `~/.codewhale/config.toml` |
| 1786 | path, preserving legacy `~/.deepseek/config.toml` only when that fallback file |
| 1787 | is already the active config. |
| 1788 | |
| 1789 | ```toml |
| 1790 | [[hotbar]] |
| 1791 | slot = 1 |
| 1792 | action = "mode.plan" |
| 1793 | label = "Plan" |
| 1794 | |
| 1795 | [[hotbar]] |
| 1796 | slot = 2 |
| 1797 | action = "session.compact" |
| 1798 | ``` |
| 1799 | - `[auto_review]` (table, optional): deterministic tool-call review policy. |
| 1800 | This layer sits on top of the existing permission posture; it can hold or block a |
| 1801 | tool call, but it is not an auto-push, auto-merge, or hosted review service. |
| 1802 | Block rules are checked first, then the built-in safety floor, then allow |
| 1803 | rules. In Ask and Auto-Review, a safety hold opens approval; in Full Access |
| 1804 | or a non-interactive `never` posture it fails closed as a hard block. The |
| 1805 | safety floor still covers publish-like actions and destructive |
| 1806 | background/headless actions even if an allow rule matches. |
| 1807 | |
| 1808 | ```toml |
| 1809 | [auto_review] |
| 1810 | natural_language_guidance = "Prefer read-only inspection until the user asks for writes." |
| 1811 | |
| 1812 | [[auto_review.allow]] |
| 1813 | id = "read-only-inspection" |
| 1814 | action_kind = "read" |
| 1815 | reason = "Read-only inspection is safe to run automatically." |
| 1816 | |
| 1817 | [[auto_review.block]] |
| 1818 | id = "no-release-publish" |
| 1819 | action_kind = "publish" |
| 1820 | reason = "Release and publish actions require maintainer review." |
| 1821 | ``` |
| 1822 | |
| 1823 | Rule matchers are exact `tool`, `action_kind`, and/or |
| 1824 | `text_contains` against the current user intent. At least one matcher is |
| 1825 | required. `action_kind` accepts `read`, `write`, `shell`, `network`, `git`, |
| 1826 | `mcp_read`, `mcp_action`, `browser`, `secret`, `publish`, `destructive`, or |
| 1827 | `unknown`; invalid names fail config validation instead of becoming broad |
| 1828 | rules. `natural_language_guidance` is recorded on the runtime policy and audit |
| 1829 | event, but deterministic rules and the built-in safety floor are the enforced |
| 1830 | behavior in current builds. |
| 1831 | |
| 1832 | Auto-review decisions emit `tool.auto_review_decision` audit events when tool |
| 1833 | audit logging is enabled. Future PreToolUse/PostToolUse hooks can add |
| 1834 | observer input around this layer, but the configured auto-review policy is |
| 1835 | evaluated before a tool call is allowed to proceed. |
| 1836 | - `managed_config_path` (string, optional): managed config file loaded after user/env config. |
| 1837 | - `requirements_path` (string, optional): requirements file used to enforce allowed approval/sandbox values. |
| 1838 | - `max_subagents` (int, optional): defaults to `64` and is clamped to `1..=128`. |
| 1839 | - `subagents.*` (optional compatibility table): per-Fleet-role model defaults |
| 1840 | for `agent`. Explicit tool `model` values win, then role |
| 1841 | overrides, then the parent runtime model. Supported convenience keys are |
| 1842 | `default_model`, `worker_model`, `scout_model`, `planner_model`, |
| 1843 | `reviewer_model`, `custom_model`, `max_concurrent`, `max_admitted`, |
| 1844 | `launch_concurrency`, `token_budget`, `api_timeout_secs`, and |
| 1845 | `heartbeat_timeout_secs`. The v0.9.x keys `explorer_model`, `awaiter_model`, |
| 1846 | and `review_model` remain accepted as aliases. The `[subagents] |
| 1847 | max_concurrent` value overrides |
| 1848 | top-level `max_subagents` and is also clamped to `1..=128`. `[subagents] |
| 1849 | max_admitted` (aliases: `max_total`, `admission_limit`) is the bounded total |
| 1850 | of queued plus running sub-agents; it defaults to `1024` |
| 1851 | (`MAX_SUBAGENT_ADMISSION`, `crates/tui/src/config/subagent_limits.rs:21`, |
| 1852 | applied at `config.rs:6400`) so high-fanout turns can queue and drain while |
| 1853 | runtime launch pressure remains bounded, and is clamped to |
| 1854 | `max_concurrent..=1024`. `[subagents] |
| 1855 | launch_concurrency` sets how many direct children start at once before the |
| 1856 | rest queue for a launch slot; it defaults to the resolved `max_subagents` cap |
| 1857 | and is clamped to `1..=max_subagents` (the deprecated |
| 1858 | `interactive_max_launch` key is accepted as an alias, with the new key |
| 1859 | winning when both are set). `[subagents] token_budget` is an optional |
| 1860 | aggregate token ceiling for each root `agent` run and its descendants; unset |
| 1861 | or `0` preserves unlimited legacy behavior. `[subagents] api_timeout_secs` |
| 1862 | controls the per-step API timeout for sub-agent model calls and is clamped to |
| 1863 | `1..=3600`, with `0` or unset preserving the 600 second default; a timed-out |
| 1864 | attempt is retried with exponential backoff (up to 5 retries) before the |
| 1865 | step interrupts with a preserved checkpoint. |
| 1866 | `[subagents] heartbeat_timeout_secs` controls stale running agent cleanup, |
| 1867 | defaults to `300`, and is clamped to `30..=3600` while staying above the |
| 1868 | resolved API timeout. `[subagents.providers.<provider>]` accepts the same |
| 1869 | fanout, depth, budget, and timeout knobs (`enabled`, `max_concurrent`, |
| 1870 | `max_admitted`, `launch_concurrency`, `max_depth`, `token_budget`, |
| 1871 | `api_timeout_secs`, `heartbeat_timeout_secs`) and inherits the global |
| 1872 | `[subagents]` value for any key you omit. Provider keys accept canonical |
| 1873 | names such as `deepseek`, `zai`, `openrouter`, `anthropic`, plus convenience |
| 1874 | aliases such as `glm` for Z.ai and `deepseek_api` for direct DeepSeek: |
| 1875 | |
| 1876 | ```toml |
| 1877 | [subagents] |
| 1878 | max_concurrent = 20 |
| 1879 | launch_concurrency = 20 |
| 1880 | max_admitted = 200 |
| 1881 | max_depth = 6 |
| 1882 | |
| 1883 | [subagents.providers.deepseek] |
| 1884 | max_concurrent = 20 |
| 1885 | launch_concurrency = 20 |
| 1886 | max_admitted = 200 |
| 1887 | |
| 1888 | [subagents.providers.glm] |
| 1889 | max_concurrent = 4 |
| 1890 | launch_concurrency = 3 |
| 1891 | max_admitted = 12 |
| 1892 | max_depth = 2 |
| 1893 | |
| 1894 | [subagents.providers.openrouter] |
| 1895 | max_concurrent = 5 |
| 1896 | launch_concurrency = 3 |
| 1897 | max_admitted = 20 |
| 1898 | ``` |
| 1899 | |
| 1900 | `/config subagents status` prints both global values and the active |
| 1901 | provider's resolved profile so rate-limit tuning is visible in the TUI. |
| 1902 | `[subagents.models]` accepts lower-case Fleet role keys such as `worker`, |
| 1903 | `scout`, `planner`, `reviewer`, `builder`, and `verifier`; legacy type keys |
| 1904 | remain accepted during v0.9.x. Values are validated |
| 1905 | against the active provider at spawn time; direct DeepSeek requires DeepSeek |
| 1906 | IDs, while OpenAI-compatible/custom provider routes pass explicit model IDs |
| 1907 | through to that provider. To route a child to a different provider than the |
| 1908 | parent session, save a Fleet/AgentProfile with explicit `provider` and |
| 1909 | `model` fields (including user-named custom providers such as `lm-studio`) |
| 1910 | and call `agent(profile: "...")`; see [SUBAGENTS.md](SUBAGENTS.md). |
| 1911 | - `skills_dir` (string, optional): defaults to `~/.codewhale/skills` (each skill is |
| 1912 | a directory containing `SKILL.md`). Workspace-local `.agents/skills` or |
| 1913 | `./skills` are preferred when present; the runtime also discovers global |
| 1914 | agentskills.io-compatible `~/.agents/skills` and the broader Claude-ecosystem |
| 1915 | `~/.claude/skills`. First launch installs versioned bundled skills for common |
| 1916 | workflows including skill creation, delegation, MCP/plugin scaffolding, |
| 1917 | documents, presentations, spreadsheets, PDFs, and Feishu/Lark. Only |
| 1918 | CodeWhale-owned roots (`<workspace>/.codewhale/skills` and |
| 1919 | `~/.codewhale/skills`) are writable install/import targets; compatible harness |
| 1920 | roots stay read-only. Bare `/skills` opens the Skills Manager (owned-only, |
| 1921 | zero network). See [SKILLS.md](SKILLS.md) for the manager, audit statuses, |
| 1922 | provenance markers, and mutation rules, and |
| 1923 | [CLAUDE_PLUGIN_COMPAT.md](CLAUDE_PLUGIN_COMPAT.md) for the supported boundary |
| 1924 | between portable `SKILL.md` bundles and Claude Code plugin runtimes. |
| 1925 | - `[skills].scan_codewhale_only` (bool, default `false`): when `true`, session |
| 1926 | skill discovery ignores cross-tool roots such as `.claude/skills`, |
| 1927 | `.opencode/skills`, `.cursor/skills`, and `~/.agents/skills`. Codewhale still |
| 1928 | scans `<workspace>/.codewhale/skills`, `~/.codewhale/skills`, and any explicit |
| 1929 | `skills_dir` override. The Skills Manager can still toggle a local compatible |
| 1930 | audit scan independently of this runtime knob — see [SKILLS.md](SKILLS.md). |
| 1931 | - `[skills].registry_url` / `[skills].max_install_size_bytes` (optional): used by |
| 1932 | `/skills --remote`, `/skills suggest <task>`, `/skills sync`, and `/skill |
| 1933 | install|update`. The default manager open path does not contact the registry. |
| 1934 | - `[verifier].enabled` (bool, default `false`): enables automatic |
| 1935 | claim-of-done verifier preview once that runtime trigger is active. The |
| 1936 | manual `run_verifiers` tool is still available when this is false. |
| 1937 | - `[verifier].verdict_policy` (string, default `"hunt"`): maps verifier |
| 1938 | `pass` / `partial` / `fail` into the goal verdict vocabulary |
| 1939 | `hunted` / `wounded` / `escaped`. `"hunt"` is the only shipped policy today; |
| 1940 | unknown values are rejected so future policies can be added deliberately. |
| 1941 | - `mcp_config_path` (string, optional): defaults to `~/.codewhale/mcp.json`, with |
| 1942 | legacy `~/.deepseek/mcp.json` fallback when the Codewhale path is absent. |
| 1943 | It is visible in `/config` and can be changed from the TUI. The new path is |
| 1944 | used immediately by `/mcp`, but rebuilding the model-visible MCP tool pool |
| 1945 | requires restarting the TUI. |
| 1946 | - `notes_path` (string, optional): defaults to `~/.codewhale/notes.txt`, with |
| 1947 | legacy `~/.deepseek/notes.txt` fallback when the Codewhale path is absent, and |
| 1948 | is used by the model-visible `note` tool. |
| 1949 | - `[memory].enabled` (bool, optional): defaults to `false`. When `true`, |
| 1950 | the TUI loads the user memory file into a `<user_memory>` prompt block, |
| 1951 | enables `# foo` quick-capture in the composer, surfaces the `/memory` |
| 1952 | slash command, and registers the `remember` tool. The same toggle is |
| 1953 | available via `DEEPSEEK_MEMORY=on`. |
| 1954 | - `memory_path` (string, optional): anchors the native memory store. The |
| 1955 | configured filename is **not** the file that is written. Under the Native |
| 1956 | backend (the only backend) the store is re-rooted to |
| 1957 | `<parent-of-memory_path>/memory/global/MEMORY.md` — so the default |
| 1958 | `~/.codewhale/memory.md` yields `~/.codewhale/memory/global/MEMORY.md` |
| 1959 | (plus workspace-scoped files and a rebuildable SQLite FTS5 index). See |
| 1960 | [`MEMORY.md`](MEMORY.md) for the full feature surface (`# foo` composer |
| 1961 | prefix, `/memory` slash command, `remember` tool, opt-in toggle). |
| 1962 | - `snapshots.*` (optional): side-git workspace snapshots for file rollback: |
| 1963 | - `[snapshots].enabled` (bool, default `true`) |
| 1964 | - `[snapshots].max_age_days` (int, default `7`) |
| 1965 | - snapshots live under |
| 1966 | `~/.codewhale/snapshots/<project_hash>/<worktree_hash>/.git`, with legacy |
| 1967 | `~/.deepseek/snapshots/...` fallback when only the legacy state exists, and |
| 1968 | never use the workspace's own `.git` directory |
| 1969 | - `context.*` (optional): |
| 1970 | - `[context].enabled` (bool, default `false`) |
| 1971 | - `[context].project_pack` (bool, default `false`): include a deterministic |
| 1972 | project context pack (a large pretty-printed directory listing) in the |
| 1973 | stable prompt prefix (#4781). Useful for weak tool-calling models; the |
| 1974 | model can rebuild the same information with one `File` call. |
| 1975 | - The former seam-manager keys (`verbatim_window_turns`, `l1_threshold`, |
| 1976 | `l2_threshold`, `l3_threshold`, `seam_model`) are **ignored** — parsed |
| 1977 | for backward compatibility but read nowhere since 2026-07-23. |
| 1978 | - `retry.*` (optional): retry/backoff settings for API requests: |
| 1979 | - `[retry].enabled` (bool, default `true`) |
| 1980 | - `[retry].max_retries` (int, default `3`) |
| 1981 | - `[retry].initial_delay` (float seconds, default `1.0`) |
| 1982 | - `[retry].max_delay` (float seconds, default `60.0`) |
| 1983 | - `[retry].exponential_base` (float, default `2.0`) |
| 1984 | - `[notifications].method` (string, optional): `auto`, `osc9`, `bel`, or |
| 1985 | `off`. Defaults to `auto`. The TUI fires this on completed (successful) |
| 1986 | turns whose elapsed time meets `threshold_secs`; failed and cancelled |
| 1987 | turns are silent. `auto` resolves to `osc9` for `iTerm.app`, `Ghostty`, |
| 1988 | and `WezTerm` (detected via `$TERM_PROGRAM`). Otherwise the fallback is |
| 1989 | `bel`; on Windows the BEL path is routed through `MessageBeep(MB_OK)`. |
| 1990 | - `[notifications].threshold_secs` (int, optional): defaults to `30`. |
| 1991 | Only completed turns whose elapsed time meets or exceeds this fire a |
| 1992 | notification. |
| 1993 | - `[notifications].include_summary` (bool, optional): defaults to |
| 1994 | `false`. When `true`, the notification body includes the elapsed |
| 1995 | duration and the turn's cost in the configured display currency. |
| 1996 | - `[notifications].completion_sound` (string, optional): `off`, `beep`, |
| 1997 | `bell`, or `file`. Defaults to `beep`. `file` plays the WAV path from |
| 1998 | `[notifications].sound_file` on Windows. |
| 1999 | - `[notifications].sound_file` (path, optional): path to a custom WAV file |
| 2000 | used when `completion_sound = "file"`. |
| 2001 | - `[notifications].quiet` (bool, optional): defaults to `false`. Quiet |
| 2002 | mode — suppresses every desktop notification (all categories, all |
| 2003 | delivery methods) and the paired `event_sound` cues, without changing |
| 2004 | `method` or the per-category switches. The turn-completion chime |
| 2005 | (`completion_sound`) is governed separately. |
| 2006 | - `[notifications.events]` (table, optional): per-category |
| 2007 | desktop-notification switches; every key defaults to `true`. Keys: |
| 2008 | `turn-complete`, `subagent-terminal`, `approval-needed`, |
| 2009 | `input-needed`, `elevation-needed`, `model-notify`. A disabled |
| 2010 | category is suppressed on every delivery mechanism (OSC 9, Kitty, |
| 2011 | Ghostty, BEL, macOS Notification Center). |
| 2012 | - `[notifications.event_sound]` (table, optional): opt-in, deterministic |
| 2013 | per-event sound cues. Keys: `enabled` (bool, default `false`), `events` |
| 2014 | (array of kebab-case event names, default `["turn-complete", |
| 2015 | "approval-needed"]`), `min_interval_ms` (int, default `2000`), `quiet` |
| 2016 | (bool, default `false`). See "Event sound cues" below. |
| 2017 | - `tui.alternate_screen` (string, optional): `auto`, `always`, or `never`. This is retained for config compatibility, but interactive sessions now always use the TUI-owned alternate screen so host terminal scrollback cannot hijack the viewport. |
| 2018 | - `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 only transcript text, removes visual wrap-column line breaks from paragraphs, and keeps selection scoped to the transcript pane. 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 sidebar and include visual wraps because the terminal, not the TUI, owns the selection. |
| 2019 | - `tui.terminal_probe_timeout_ms` (int, optional, default `500`): startup terminal-mode probe timeout in milliseconds. Values are clamped to `100..=5000`; timeout emits a warning and aborts startup instead of hanging indefinitely. |
| 2020 | - `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 <seconds>`; `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. |
| 2021 | - `tui.header_items` (array of strings, optional, default `[]`): opt-in header chips. Set `header_items = ["tokens"]` under `[tui]` to show the session input, cache-hit, and output token counts. Narrow terminals elide the optional chip; wide terminals show it alongside context utilization. |
| 2022 | - `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`. |
| 2023 | - `hooks` (optional): lifecycle hooks configuration (see `config.example.toml`). |
| 2024 | - `features.*` (optional): feature flag overrides (see below). |
| 2025 | |
| 2026 | ### Workspace notes |
| 2027 | |
| 2028 | `/note` manages a simple notes file in the current workspace at |
| 2029 | `.deepseek/notes.md`. Existing `/note <text>` usage still appends a note. |
| 2030 | The management forms are: |
| 2031 | |
| 2032 | | Command | Action | |
| 2033 | |---|---| |
| 2034 | | `/note <text>` | Append a note (legacy shorthand) | |
| 2035 | | `/note add <text>` | Append a note explicitly | |
| 2036 | | `/note list` | List notes with temporary 1-based numbers | |
| 2037 | | `/note show <n>` | Show the full note at number `n` | |
| 2038 | | `/note edit <n> <text>` | Replace note `n` with new text | |
| 2039 | | `/note remove <n>` | Delete note `n`; `rm` and `delete` are aliases | |
| 2040 | | `/note clear` | Empty the workspace notes file | |
| 2041 | | `/note path` | Show the resolved workspace notes path | |
| 2042 | |
| 2043 | The numbers shown by `/note list` are not stored in the file; they are derived |
| 2044 | from the current order each time notes are read. This keeps the file format |
| 2045 | compatible with the existing `---`-separated notes. |
| 2046 | |
| 2047 | ### User memory |
| 2048 | |
| 2049 | User memory is split across one top-level path setting and one opt-in |
| 2050 | toggle table: |
| 2051 | |
| 2052 | ```toml |
| 2053 | # Anchors the store only — actual writes go to |
| 2054 | # ~/.codewhale/memory/global/MEMORY.md (see MEMORY.md). |
| 2055 | memory_path = "~/.codewhale/memory.md" |
| 2056 | |
| 2057 | [memory] |
| 2058 | enabled = true |
| 2059 | ``` |
| 2060 | |
| 2061 | Notes: |
| 2062 | |
| 2063 | - `memory_path` stays at the top level beside `notes_path` and |
| 2064 | `skills_dir`; it is not nested under `[memory]`. |
| 2065 | - The configured path is an **anchor**: its parent directory gains |
| 2066 | `memory/global/MEMORY.md`, workspace-scoped files, and `index.db`. |
| 2067 | Pointing `memory_path` at the native layout path itself would double-nest |
| 2068 | (`…/memory/global/memory/global/MEMORY.md`); keep the legacy-style |
| 2069 | anchor filename. |
| 2070 | - `DEEPSEEK_MEMORY_PATH` overrides the anchor path from the environment. |
| 2071 | - `DEEPSEEK_MEMORY=on` (also `1`, `true`, `yes`, `y`, or `enabled`) |
| 2072 | flips the feature on without editing `config.toml`. |
| 2073 | - The feature is inert when disabled: no file is injected, `# foo` |
| 2074 | falls through to normal message submission, and the model does not |
| 2075 | see the `remember` tool. |
| 2076 | - See [`MEMORY.md`](MEMORY.md) for examples and the full `/memory` |
| 2077 | command surface. |
| 2078 | |
| 2079 | ### Goal loop (`[goal]`) |
| 2080 | |
| 2081 | Operate-mode goals run to their completion gate: the only terminal stops are a |
| 2082 | verified completion, a blocked report, or an exhausted configured token budget |
| 2083 | (#5052). The decision core also accepts an optional time budget, but the TUI |
| 2084 | does not currently configure or expose one. A configurable safety backstop |
| 2085 | still halts a pathological loop that never emits a terminal signal: |
| 2086 | |
| 2087 | ```toml |
| 2088 | [goal] |
| 2089 | # Safety backstop on automatic goal continuation passes. |
| 2090 | # Default: 100. Set 0 to disable the backstop entirely so only |
| 2091 | # completion/blocked or budget exhaustion stop the run. |
| 2092 | max_continuations = 100 |
| 2093 | ``` |
| 2094 | |
| 2095 | When the backstop fires, the goal pauses with a status message naming |
| 2096 | `[goal] max_continuations` and a warning is logged; resume the goal after |
| 2097 | inspecting progress, or raise/disable the backstop. |
| 2098 | |
| 2099 | ### Notifications |
| 2100 | |
| 2101 | The TUI can emit a desktop notification (OSC 9 escape or plain BEL) when a turn **completes successfully** and took longer than a threshold, so you can tab away while a long task runs. Failed or cancelled turns are intentionally silent — the notification is a "your task is ready" cue, not a generic ping. Configuration lives under `[notifications]`: |
| 2102 | |
| 2103 | ```toml |
| 2104 | [notifications] |
| 2105 | method = "auto" # auto | osc9 | bel | off |
| 2106 | threshold_secs = 30 # only notify when the turn took >= this many seconds |
| 2107 | include_summary = false # include elapsed time + cost in the notification body |
| 2108 | completion_sound = "beep" # off | beep | bell | file |
| 2109 | sound_file = "E:\\google\\downloads\\notify.wav" # for completion_sound = "file" |
| 2110 | quiet = false # true suppresses every desktop notification |
| 2111 | |
| 2112 | [notifications.events] # per-category switches; all default to true |
| 2113 | turn-complete = true # an agent turn finished |
| 2114 | subagent-terminal = true # a sub-agent reached a terminal status |
| 2115 | approval-needed = true # a tool call is blocked on your approval |
| 2116 | input-needed = true # the agent asked a question and is blocked |
| 2117 | elevation-needed = true # the sandbox denied a tool and needs a decision |
| 2118 | model-notify = true # the model called the `notify` tool |
| 2119 | ``` |
| 2120 | |
| 2121 | `quiet = true` is the one-flag "stop interrupting me" switch: it silences |
| 2122 | every category on every delivery mechanism while leaving the rest of your |
| 2123 | notification configuration intact, so flipping it back restores your exact |
| 2124 | previous policy. `[notifications.events]` disables single categories the |
| 2125 | same way — a disabled category is suppressed at the emission path, so it |
| 2126 | cannot leak through one specific protocol. A suppressed notification also |
| 2127 | suppresses its paired `[notifications.event_sound]` cue (no orphaned bells |
| 2128 | for events you turned off); the turn-completion chime (`completion_sound`) |
| 2129 | is governed separately. |
| 2130 | |
| 2131 | Method semantics: |
| 2132 | |
| 2133 | - `auto` (default) — picks `osc9` for `iTerm.app`, `Ghostty`, and `WezTerm` (detected via `$TERM_PROGRAM`). Otherwise it falls back to `bel`; on Windows that BEL path is routed through `MessageBeep(MB_OK)`. |
| 2134 | - `osc9` — emit `\x1b]9;<msg>\x07`. Inside tmux the sequence is wrapped in DCS passthrough so it reaches the outer terminal. |
| 2135 | - `bel` — emit a single `\x07` byte. Use this on Windows only if you actively want the chime back. |
| 2136 | - `off` — disable post-turn notifications entirely. |
| 2137 | |
| 2138 | Windows users who run inside a known OSC-9 terminal (e.g. WezTerm on Windows) keep getting OSC-9 notifications. Set `method = "off"` to disable threshold-based desktop notifications entirely. |
| 2139 | |
| 2140 | `completion_sound = "file"` is for Windows users who want a per-application |
| 2141 | completion sound without changing the global Windows sound scheme. It plays the |
| 2142 | configured WAV `sound_file` asynchronously via the native Windows audio API. |
| 2143 | |
| 2144 | #### Event sound cues |
| 2145 | |
| 2146 | `[notifications.event_sound]` is an opt-in, deterministic policy that emits a |
| 2147 | terminal-bell-level cue when specific notification events fire (approval |
| 2148 | prompts, blocked-on-input, sub-agent completion, and so on). It is **off by |
| 2149 | default**; with `enabled = false` nothing is emitted, which is the |
| 2150 | platform-safe no-op fallback. |
| 2151 | |
| 2152 | ```toml |
| 2153 | [notifications.event_sound] |
| 2154 | enabled = false # default: off (opt-in) |
| 2155 | events = ["turn-complete", "approval-needed"] # default allow-list |
| 2156 | min_interval_ms = 2000 # per-event rate limit |
| 2157 | quiet = false # true silences everything without editing the allow-list |
| 2158 | ``` |
| 2159 | |
| 2160 | The cue table is fixed — cues are functional BEL-based signals, not |
| 2161 | designed-for-pleasantness audio, and every cue is one or two `\x07` bytes |
| 2162 | (inert on terminals that ignore BEL, so this is a platform-safe no-op |
| 2163 | everywhere): |
| 2164 | |
| 2165 | | Event | Cue | |
| 2166 | |---|---| |
| 2167 | | `turn-complete` | BEL (`\x07`) | |
| 2168 | | `subagent-terminal` | BEL (`\x07`) | |
| 2169 | | `approval-needed` | double BEL (`\x07\x07`) | |
| 2170 | | `input-needed` | BEL (`\x07`) | |
| 2171 | | `elevation-needed` | double BEL (`\x07\x07`) | |
| 2172 | | `model-notify` | BEL (`\x07`) | |
| 2173 | |
| 2174 | Decision order: disabled → quiet mode → event not in `events` → `turn-complete` |
| 2175 | deferred to the `completion_sound` channel when that is active (so the two |
| 2176 | never double-ding) → per-event rate limit (`min_interval_ms` since the last |
| 2177 | play of that event) → play. Unknown strings in `events` are ignored. |
| 2178 | |
| 2179 | #### What a notification can contain |
| 2180 | |
| 2181 | A desktop notification is a glance surface: on macOS it can appear on the |
| 2182 | lock screen, and on every platform it is visible to anyone near the machine. |
| 2183 | Codewhale therefore builds notifications from a typed payload with a fixed |
| 2184 | per-event disclosure policy rather than from whatever text was on hand: |
| 2185 | |
| 2186 | | Event | Shown | Never shown | |
| 2187 | |---|---|---| |
| 2188 | | Turn complete | status line (+ elapsed/cost when `include_summary`), preview of the assistant's reply | — | |
| 2189 | | Sub-agent finished | status line, agent id, preview of the child's summary line | — | |
| 2190 | | Approval needed | the tool name | the tool description, the command, the arguments | |
| 2191 | | Input needed | "Answer the question in the terminal to continue" | the question | |
| 2192 | | Sandbox elevation needed | the tool name and the denial reason | the command | |
| 2193 | | `notify` tool | model-supplied title and body | — | |
| 2194 | |
| 2195 | Every field is capped (80 characters for the status line, 120 for the |
| 2196 | identifier, 200 for the preview), stripped of control bytes and escape |
| 2197 | sequences, and passed through a redactor that replaces credential-shaped |
| 2198 | strings with `[redacted]`, reduces absolute local paths to `…/basename`, |
| 2199 | and replaces raw tool JSON with `[details hidden]`. The redactor is |
| 2200 | deliberately over-eager: an unbroken 40-character run has no word |
| 2201 | structure, so it is redacted even when it is not a secret. |
| 2202 | |
| 2203 | #### macOS: why the banner says "Script Editor" |
| 2204 | |
| 2205 | On macOS terminals that provide no notification escape of their own — |
| 2206 | Apple Terminal, the VS Code and JetBrains embedded terminals, plain tmux |
| 2207 | without `LC_TERMINAL` — `method = "auto"` falls back to `osascript`'s |
| 2208 | `display notification`. That command posts on behalf of the *bundled* |
| 2209 | host process, and `/usr/bin/osascript` is unbundled, so macOS attributes |
| 2210 | the banner to `com.apple.ScriptEditor2`. That attribution supplies the |
| 2211 | Script Editor icon and owns the System Settings → Notifications entry |
| 2212 | (alert style, previews, Do Not Disturb). `display notification` has no |
| 2213 | icon parameter, so this cannot be fixed from the notification code; it |
| 2214 | needs Codewhale to ship a real `.app` bundle. Tracked in |
| 2215 | [#4834](https://github.com/Hmbown/CodeWhale/issues/4834). In the meantime, |
| 2216 | iTerm2, WezTerm, Ghostty, and kitty are matched first and use their own |
| 2217 | notification protocols, and `method = "osc9"` / `"bel"` / `"off"` opt out |
| 2218 | of the `osascript` path explicitly. |
| 2219 | |
| 2220 | ## Tool Catalog |
| 2221 | |
| 2222 | Codewhale loads a small core native tool catalog by default and leaves less |
| 2223 | common native tools discoverable through ToolSearch. To keep specific native |
| 2224 | tools loaded on every request, add them to `[tools].always_load`: |
| 2225 | |
| 2226 | ```toml |
| 2227 | [tools] |
| 2228 | always_load = ["Git", "notify"] |
| 2229 | ``` |
| 2230 | |
| 2231 | ## Feature Flags |
| 2232 | |
| 2233 | Feature flags live under the `[features]` table and are merged across profiles. |
| 2234 | Defaults are enabled for built-in tooling, so you only need to set entries you |
| 2235 | want to force on or off. |
| 2236 | |
| 2237 | ```toml |
| 2238 | [features] |
| 2239 | shell_tool = true |
| 2240 | subagents = true |
| 2241 | web_search = true # enables deferred Web; the flag name is retained for config compatibility |
| 2242 | apply_patch = true |
| 2243 | mcp = true |
| 2244 | exec_policy = true |
| 2245 | ``` |
| 2246 | |
| 2247 | You can also override features for a single run: |
| 2248 | |
| 2249 | - `codewhale-tui --enable web_search` |
| 2250 | - `codewhale-tui --disable subagents` |
| 2251 | |
| 2252 | Use `codewhale-tui features list` to inspect known flags and their effective state. |
| 2253 | The native `/config` view also includes a read-only **Experimental** section |
| 2254 | for experimental feature flags. It shows each flag's effective enabled/disabled |
| 2255 | state and whether that state comes from the default or a configured override. |
| 2256 | Change feature flags in `[features]` or with `--enable` / `--disable`; the |
| 2257 | `/config` section is an audit surface, not a stability promise. Goal and |
| 2258 | Workflow preview rows may appear there as reserved entries until those workflows |
| 2259 | graduate behind real gated flags. |
| 2260 | |
| 2261 | ## Web Search Provider |
| 2262 | |
| 2263 | `web_search` uses DuckDuckGo by default and does not require an API key. The |
| 2264 | DuckDuckGo path keeps a Bing fallback when DDG returns a bot challenge or no |
| 2265 | parseable results. Bing remains selectable for users who explicitly want it, |
| 2266 | and Tavily, Bocha, Metaso, SearXNG, Baidu, Volcengine, or Sofya can be selected |
| 2267 | when an API-backed provider is preferred. |
| 2268 | |
| 2269 | Configured API providers are attempted first. Runtime failure or an empty |
| 2270 | result visibly degrades through DuckDuckGo and then Bing; the structured search |
| 2271 | receipt records every hop. Missing configuration and network-policy denials |
| 2272 | fail closed without sending the query to another provider. |
| 2273 | |
| 2274 | For a private/internal search service that serves DuckDuckGo-compatible HTML, |
| 2275 | keep `provider = "duckduckgo"` and set `base_url`; Codewhale appends the `q` |
| 2276 | query parameter to that endpoint and applies network policy to its host. |
| 2277 | Custom endpoints do not fall back to public Bing. `CODEWHALE_SEARCH_BASE_URL` |
| 2278 | can override this per process; `DEEPSEEK_SEARCH_BASE_URL` remains accepted as |
| 2279 | the legacy alias. |
| 2280 | |
| 2281 | **SearXNG** ([docs](https://docs.searxng.org/dev/search_api.html)) uses the |
| 2282 | configured instance's JSON API. Set `provider = "searxng"` and |
| 2283 | `base_url = "https://your-searxng.example"`; Codewhale calls |
| 2284 | `/search?q=...&format=json`. Codewhale does not use a public SearXNG instance |
| 2285 | by default because public instances often disable JSON output or rate-limit API |
| 2286 | traffic. |
| 2287 | |
| 2288 | **Metaso** ([metaso.cn](https://metaso.cn)) requires a user-supplied key. Set |
| 2289 | `METASO_API_KEY` or `[search] api_key`; Codewhale does not ship a shared key. |
| 2290 | |
| 2291 | **Baidu** uses Baidu AI Search at |
| 2292 | `https://qianfan.baidubce.com/v2/ai_search/web_search`. Set |
| 2293 | `BAIDU_SEARCH_API_KEY` or `[search] api_key`. This is a search-tool backend |
| 2294 | only; it does not add a Baidu model provider. |
| 2295 | |
| 2296 | **Sofya** ([sofya.co](https://sofya.co)) returns full extracted page content |
| 2297 | rather than snippets. Set `[search] api_key` to your `ay_live_...` key, or the |
| 2298 | `SOFYA_API_KEY` env var. This is a search-tool backend only; it does not add a |
| 2299 | Sofya model provider. |
| 2300 | |
| 2301 | ```toml |
| 2302 | [search] |
| 2303 | provider = "searxng" # duckduckgo | bing | tavily | bocha | metaso | searxng | baidu | volcengine | sofya |
| 2304 | # base_url = "https://search.example/" # optional with provider = "duckduckgo"; required with "searxng" |
| 2305 | # api_key = "YOUR_KEY" # required for tavily, bocha, metaso, baidu, volcengine, and sofya; unused by searxng |
| 2306 | ``` |
| 2307 | |
| 2308 | ## Local Media Attachments |
| 2309 | |
| 2310 | Use `@path/to/file` in the composer to add local text file or directory context |
| 2311 | to the next message. Use `/attach <path>` for local image/video media paths, or |
| 2312 | `Ctrl+V` to attach an image from a local clipboard or an explicitly forwarded |
| 2313 | X11/Wayland clipboard. SSH terminal paste without a forwarded graphical display |
| 2314 | is text-only; use the local terminal's paste command (`Cmd+V` on macOS or |
| 2315 | `Ctrl+Shift+V` on Linux/Windows), and use `/attach <path>` for remote image |
| 2316 | files. OpenSSH loopback X11 displays are detected automatically. For an |
| 2317 | explicitly forwarded Wayland or non-loopback X11 display, set |
| 2318 | `CODEWHALE_SSH_CLIPBOARD=graphical`; set it to `terminal` to force terminal |
| 2319 | transfer instead of an ambient remote display. DeepSeek's public Chat |
| 2320 | Completions API currently accepts text message |
| 2321 | content, so media attachments are sent as explicit local path references instead |
| 2322 | of native image/video payloads. |
| 2323 | Attachment rows appear above the composer before submit; move to the start of |
| 2324 | the composer, press `↑` to select an attachment row, then press `Backspace` or |
| 2325 | `Delete` to remove it without editing the sample text by hand. |
| 2326 | |
| 2327 | ## Managed Configuration and Requirements |
| 2328 | |
| 2329 | codewhale supports a policy layering model: |
| 2330 | |
| 2331 | 1. user config + profile + env overrides |
| 2332 | 2. managed config (if present) |
| 2333 | 3. requirements validation (if present) |
| 2334 | |
| 2335 | By default on Unix: |
| 2336 | - managed config: `/etc/deepseek/managed_config.toml` |
| 2337 | - requirements: `/etc/deepseek/requirements.toml` |
| 2338 | |
| 2339 | Requirements file shape: |
| 2340 | |
| 2341 | ```toml |
| 2342 | allowed_approval_policies = ["on-request", "untrusted", "never"] |
| 2343 | allowed_sandbox_modes = ["read-only", "workspace-write"] |
| 2344 | ``` |
| 2345 | |
| 2346 | If configured values violate requirements, startup fails with a descriptive error. |
| 2347 | |
| 2348 | ## Notes On `codewhale-tui doctor` |
| 2349 | |
| 2350 | `codewhale-tui doctor` follows the same config resolution rules as the rest of the |
| 2351 | TUI. That means `--config`, `CODEWHALE_CONFIG_PATH`, and the legacy |
| 2352 | `DEEPSEEK_CONFIG_PATH` are respected, and MCP/skills |
| 2353 | checks use the resolved `mcp_config_path` / `skills_dir` (including env overrides). |
| 2354 | |
| 2355 | To bootstrap missing MCP/skills paths, run `codewhale-tui setup --all`. You can |
| 2356 | also run `codewhale-tui setup --skills --local` to create a workspace-local |
| 2357 | `./skills` dir. |
| 2358 | |
| 2359 | Both plain `codewhale-tui doctor` and `doctor --json` are structural and |
| 2360 | offline by default. They do not check the release service, hosted provider |
| 2361 | APIs, local provider endpoints, or MCP processes, and they do not load a |
| 2362 | workspace credential `.env`. Use `--check-updates`, |
| 2363 | `--probe-api`, `--probe-local`, or `--probe-mcp` to opt into the corresponding |
| 2364 | live boundary; `--probe-local` may start a desktop-managed service such as |
| 2365 | Ollama. Only the explicit API/local probe paths may load workspace credential |
| 2366 | `.env` values. Live flags conflict with `--json`, so machine-readable doctor |
| 2367 | output is always offline. Top-level keys include `version`, `paths`, `secret_backend`, |
| 2368 | `config_path`, `config_present`, `workspace`, `api_key.source`, |
| 2369 | `api_key.availability`, `base_url`, |
| 2370 | `default_text_model`, `mcp`, `skills`, `tools`, `plugins`, `sandbox`, |
| 2371 | `platform`, `api_connectivity`, and `capability`. CI consumers should rely on |
| 2372 | `api_key.source` (`config_declared`/`env_declared`/`external_auth_declared`/ |
| 2373 | `secret_store_unprobed`/`secret_store_unavailable`/`oauth_unprobed`/ |
| 2374 | `external_consent`/`none`/`local_runtime`/`unknown`) and |
| 2375 | `api_key.availability` |
| 2376 | (`present`/`not_required`/`not_probed`/`unavailable`/`unknown`) rather than parsing the |
| 2377 | human-readable `doctor` text. Source is declaration metadata, not proof that a |
| 2378 | credential exists or works. Only a non-empty, non-sentinel literal config value |
| 2379 | is structurally `present`; no-auth and local routes are `not_required`. Environment, |
| 2380 | external-auth, OAuth, consent, and secret-store declarations remain `not_probed` |
| 2381 | and cannot make structural Setup or Fleet readiness true. A secret-store sentinel |
| 2382 | on a named/custom endpoint that is prohibited from using the shared store is |
| 2383 | `secret_store_unavailable`/`unavailable`, while `unknown` remains reserved for |
| 2384 | the absence of a supported structural conclusion. Exact and whitespace-wrapped |
| 2385 | legacy sentinels are never treated as literal credentials. The structural |
| 2386 | loader still honors safe environment routing/model/policy fields, but it never |
| 2387 | materializes environment HTTP headers, sandbox API keys, or search API keys; |
| 2388 | only an explicit API/local probe switches to the normal credential-loading |
| 2389 | path. An opted-in update check also emits only typed generic failures: untrusted |
| 2390 | release metadata and transport errors are not echoed. |
| 2391 | |
| 2392 | If configuration loading or validation fails, `doctor --json` returns nonzero |
| 2393 | and prints a bounded JSON error envelope with |
| 2394 | `status = "error"` and `error.kind = "config_validation"`. It does not emit a |
| 2395 | normal route or capability report—or the underlying possibly sensitive error— |
| 2396 | for an invalid configuration. |
| 2397 | |
| 2398 | MCP entries are configuration diagnostics unless an explicit MCP command is |
| 2399 | run. `mcp.probe_scope` is `configuration`, `mcp.live_health_checked` is false, |
| 2400 | and each server separates `checks.configuration` / `checks.command` from |
| 2401 | `checks.process_reachable`, `checks.protocol_initialized`, and |
| 2402 | `checks.backend_tool_health`. The latter three remain `not_checked` in doctor |
| 2403 | output. Run `codewhale mcp validate` to explicitly start enabled servers and |
| 2404 | verify protocol initialization/discovery; backend health still requires an |
| 2405 | appropriate explicit tool call. Doctor reports only safe structural MCP fields: |
| 2406 | URL userinfo/path/query/fragment and raw command arguments, environment values, |
| 2407 | header values, and token material are omitted. Provider URLs follow the same |
| 2408 | rule and expose only `scheme://host[:explicit-port]`. |
| 2409 | |
| 2410 | The `capability` key contains per-provider capability info derived from |
| 2411 | static knowledge (release docs, API guides) rather than live API probes. |
| 2412 | Top-level sub-keys: `resolved_provider`, `resolved_model`, `context_window`, |
| 2413 | `max_output`, `thinking_supported`, `cache_telemetry_supported`, |
| 2414 | and `request_payload_mode`. |
| 2415 | |
| 2416 | Use `capability.context_window` and `capability.max_output` for model-limit |
| 2417 | checks in CI scripts; do not treat `capability.max_output` as the per-turn |
| 2418 | request budget. Use `capability.thinking_supported` to decide whether to |
| 2419 | configure reasoning effort. |
| 2420 | |
| 2421 | ## Setup status, clean, and extension dirs |
| 2422 | |
| 2423 | `codewhale-tui setup` accepts a few flags beyond the existing `--mcp`, |
| 2424 | `--skills`, `--local`, `--all`, and `--force`: |
| 2425 | |
| 2426 | - `--status` — print a compact one-screen status (api key, base URL, model, |
| 2427 | MCP/skills/tools/plugins counts, sandbox, `.env` presence). Read-only and |
| 2428 | network-free; safe to run in CI. If `.env` is missing and `.env.example` is |
| 2429 | present in the workspace, the status output points at `cp .env.example .env`. |
| 2430 | - `--tools` — scaffold `~/.codewhale/tools/` with a `README.md` describing the |
| 2431 | self-describing frontmatter convention (`# name:` / `# description:` / |
| 2432 | `# usage:`) and an `example.sh` that follows it. The directory is |
| 2433 | intentionally not auto-loaded; wire individual scripts into the agent via |
| 2434 | MCP, hooks, or skills. |
| 2435 | - `--plugins` — scaffold `~/.codewhale/plugins/` with a `README.md` and an |
| 2436 | `example/plugin.toml` plus a namespaced example Skill. Bundles are discovered |
| 2437 | read-only, untrusted, and disabled; review them through `/plugin` before |
| 2438 | enabling. v0.9.1 activates only declared Skills and MCP servers. See |
| 2439 | [PLUGIN_BUNDLES.md](PLUGIN_BUNDLES.md). |
| 2440 | - `--all` now scaffolds MCP + skills + tools + plugins together. |
| 2441 | - `--clean` — list `~/.codewhale/sessions/checkpoints/latest.json` and |
| 2442 | `offline_queue.json` if they exist. Legacy |
| 2443 | `~/.deepseek/sessions/checkpoints/` files are not scanned automatically; set |
| 2444 | `CODEWHALE_HOME=~/.deepseek` for a one-off legacy cleanup. Pass `--force` to |
| 2445 | actually remove matched files. This never touches real session history or the |
| 2446 | task queue. |
| 2447 | |
| 2448 | `--status` and `--clean` are mutually exclusive with the scaffold flags. |
| 2449 | |
| 2450 | ## Why the engine strips XML/`[TOOL_CALL]` text |
| 2451 | |
| 2452 | codewhale sends and receives tool calls only over the API tool channel |
| 2453 | (structured `tool_use` / `tool_call` items). The streaming loop in |
| 2454 | `crates/tui/src/core/engine.rs` recognizes a fixed set of fake-wrapper start |
| 2455 | markers — `[TOOL_CALL]`, `<codewhale:tool_call`, `<tool_call`, `<invoke `, |
| 2456 | `<function_calls>` — and scrubs them from visible assistant text without ever |
| 2457 | turning them into structured tool calls. When a wrapper is stripped, the loop |
| 2458 | emits one compact `status` notice per turn so the user can see why their |
| 2459 | visible text shrank. Treat any change that re-enables text-based tool |
| 2460 | execution as a regression; the protocol-recovery tests in |
| 2461 | `crates/tui/tests/protocol_recovery.rs` lock the contract. |
| 2462 |